95 Commits
0.0.1 ... 0.0.2

Author SHA1 Message Date
James Shubin
4e9ab3ca4d Add COPR badge 2016-02-12 14:39:42 -05:00
James Shubin
99b058e5e8 Add build dependencies for remote COPR builder 2016-02-12 14:16:20 -05:00
James Shubin
fc14e5c70e Make an initial RPM package for COPR
I'm not a RPM pro, so patches welcome! I'm surely doing something wrong.
2016-02-12 14:00:49 -05:00
James Shubin
e921dfa498 Improve shell scripts 2016-02-10 22:20:45 -05:00
James Shubin
40476a66c2 Pave the way for Debian
Unfortunately Debian 8 has version 1.3.3 of golang, so it will be up to
someone else to test these things for now.
2016-02-10 19:43:37 -05:00
James Shubin
89182521de Fix up path issues with vtest+ 2016-02-10 19:00:45 -05:00
James Shubin
ead025cbe7 Path fixes to avoid overwriting each other 2016-02-10 18:40:57 -05:00
James Shubin
83caea1bdc Add some special path variables and add centos-ci dependencies
Now we'll really fix the centos-ci build I think :)
2016-02-10 18:09:44 -05:00
James Shubin
f4da8756bd Actually fix the CentOS-CI builds (I hope)
It would be great to figure out how to get them to build a branch, so as
to avoid disrupting git master. Sorry!
2016-02-10 17:52:21 -05:00
James Shubin
acff20c54e Will this fix Jenkins? 2016-02-10 17:35:51 -05:00
James Shubin
bbb35fa12c Update README 2016-02-10 14:59:25 -05:00
James Shubin
2896775f77 Add a TODO list to serve as a near term roadmap
Any help with these issues or linked bugs is appreciated! Please let
someone know if you're interested on working on something here. New
programmers are welcome. Join #mgmtconfig on IRC to see if someone can
mentor you.
2016-02-10 14:32:37 -05:00
James Shubin
625ae31f63 Binary is not reproducible on travis! Why not? 2016-02-10 10:38:42 -05:00
James Shubin
72681349e5 Add a simple bashfmt test
If someone ever made a bashfmt script, that would be a lovely hack!
2016-02-08 12:06:46 -05:00
James Shubin
e342c5a06a Only format check tracked files 2016-02-08 11:59:21 -05:00
James Shubin
028fb1c258 Add a simple test for reproducibility
It probably needs environment changes and other differences to be more
effective, but if anything it adds a placeholder for improvement, and
shows some solidarity with the reproducible builds project that was
started in debian.
2016-02-08 11:50:08 -05:00
Felix Frank
6e6614808b README: also list stringer as a dependency 2016-02-03 15:11:18 +01:00
Felix Frank
c47418b02d README: specify minimum golang version 2016-02-03 11:02:30 +01:00
Felix Frank
97fda59999 README: make code actually display as multi line 2016-02-03 11:02:30 +01:00
James Shubin
b3e5f77d5d Make it easier to use converged-timeout 2016-02-02 11:45:23 -05:00
James Shubin
85f9db12f5 Cleanup the README file 2016-02-02 10:48:01 -05:00
James Shubin
655d527d5f Add a fan in, fan out example and test 2016-02-02 08:52:32 -05:00
James Shubin
925811984e Allow unbound variables like "$1" 2016-02-02 05:00:41 -05:00
James Shubin
4cb76d3347 Add the ability to run individual shell tests manually 2016-02-02 04:37:55 -05:00
James Shubin
ff838700d0 Add a fan in example and test 2016-02-02 04:36:12 -05:00
James Shubin
3cf8c4a6e8 Add gobin to path in an attempt to make it easy to find go binaries 2016-01-29 12:01:01 -05:00
James Shubin
9e08de0bcf Try and pass through with export 2016-01-29 11:10:23 -05:00
James Shubin
8f0d3e3abe I give up, let's see some debug output 2016-01-29 10:53:51 -05:00
James Shubin
3870a2c781 Pass jenkins url through to child machine 2016-01-29 10:44:48 -05:00
James Shubin
cc9bc6ac75 Add missing ^ character 2016-01-29 10:15:01 -05:00
James Shubin
cd663d2384 Work around missing JENKINS_URL regression 2016-01-29 10:08:41 -05:00
James Shubin
dee8cd97c5 Be more specific in error messages for easier debugging
Yes, I'm looking at you, JENKINS!
2016-01-29 08:58:19 -05:00
James Shubin
a64b9f8e1a Skip yamlfmt on jenkins too 2016-01-29 06:49:50 -05:00
James Shubin
b3b78b9405 Fix typo 2016-01-29 06:42:31 -05:00
James Shubin
f4a86b2364 Add centos-ci script to mgmt for independence and for make gopath 2016-01-29 06:37:29 -05:00
James Shubin
8b0a078dac Add gopath Makefile target 2016-01-29 06:20:34 -05:00
James Shubin
fb8513094b Add CentOS jenkins ci hooks 2016-01-29 06:00:11 -05:00
James Shubin
08d5a3baae Work around old go versions not supporting equals sign 2016-01-28 09:56:43 -05:00
James Shubin
358604def2 Enable shell tests
We need to use sudo: required, and dist: trusty to avoid old versions of
bash in travis which don't support the -n argument to the `wait` shell
built-in.

We had to disable the -e checks in etcd.sh since the killall || killall
parts were causing those to trigger in travis.
2016-01-28 09:37:43 -05:00
James Shubin
0795cadad1 Travis: don't sully the homepage with broken test branches 2016-01-28 08:04:26 -05:00
James Shubin
0d8b4aa2bd Fix string issues in the build
Welcome back travis
2016-01-28 06:16:08 -05:00
James Shubin
2930985238 Avoid any possible errors with git describe 2016-01-21 00:40:04 -05:00
James Shubin
d5367b7a1c Add shell based test harness
This allows you to simulate one or more simultaneously running mgmt
processes. It should be easy to use by following the test cases provided.
2016-01-21 00:23:25 -05:00
James Shubin
820294cd9a Add golang stringer to deps 2016-01-21 00:23:11 -05:00
James Shubin
9ab746fbf3 I guess we'll have to stick with the name for now 2016-01-20 23:44:25 -05:00
James Shubin
d7903d8736 Update faq to add etcd vs. consul answer 2016-01-20 17:28:52 -05:00
James Shubin
0ca9351665 Don't generate file watch events if disabled
This previously ignored the events, but they were still generated!
2016-01-20 02:05:26 -05:00
James Shubin
491e9fd9bc Golint fixes
I used: `golint | grep -v comment | grep -v stringer` to avoid crap.
2016-01-19 23:35:33 -05:00
James Shubin
4599b393e9 Fix failure of go 1.4.3 due to missing go vet 2016-01-19 22:37:45 -05:00
James Shubin
30385c85f3 Bump golang versions in travis 2016-01-19 22:29:29 -05:00
James Shubin
8308680a50 Make sure to unpause all elements when resuming
The indegree code added a regression because elements with an indegree
would not be unpaused! This is now corrected. Time to add more tests :)
2016-01-19 22:01:51 -05:00
James Shubin
9c18972af4 Add information on providing good logs 2016-01-18 12:14:17 -05:00
James Shubin
79a5e0972f Improve wording in README.md for clarification 2016-01-18 12:13:47 -05:00
James Shubin
304b48265f Many examples now exist 2016-01-18 12:08:03 -05:00
James Shubin
c0d3678b79 Remove useless noop types
These aren't relevant to the example
2016-01-18 04:50:47 -05:00
James Shubin
74baa032b5 Add link to first blog post 2016-01-18 00:59:10 -05:00
James Shubin
61c668edd3 Limit the number of initial start poke's required
Every graph needs each vertex to have a change to run initially (after
it has started up) so that initial state detection can be applied to
fix anything that happened while the program was not running. We used to
poke every vertex which was unnecessary, when in fact we only need to
poke the set of vertices that are the minimum set of ultimate
pre-requisites for every other vertex in the graph. That way, you're
either poked directly, or poked by someone who was, etc...

It turns out we don't need Dilworth's theorem, and that looking at
vertices with an indegree of 0 is enough (I think it is a special case
when we have a DAG).

This also fixes a goroutine start scheduling race by ensuring the
initial pokes are received!
2016-01-15 17:01:27 -05:00
James Shubin
8db5d630d5 Exit if program was not compiled correctly
Catch the missing injection of program name.
2016-01-15 16:49:50 -05:00
James Shubin
6e9439f4e3 Avoid panic's when referencing non-existing objects
No idea why wrapping the cmd in a function avoids a panic. Probably
something about the gc...
2016-01-15 00:03:22 -05:00
James Shubin
f7858b8e9b Add state caching and invalidation to service type
This required a change in the event system to add an "activity" field.
This is meant to be generic in the case that there is more than one need
for it, but at the moment, allows a poke to tell that it is a poke in
response to an apply that just finished, instead of a regular poke or
backpoke in which all that matters is timestamp updates, because there
wasn't any actual work done (since that state was okay).
2016-01-15 00:02:45 -05:00
James Shubin
935805aeda Add state caching for most types
This adds state caching to avoid repeated execution when not necessary.
2016-01-14 23:17:26 -05:00
James Shubin
4c6647d807 Fixup state related items
* Fixup graph state readability
* Rename original SetState() to SetConvergedState() and friends...
* Add type state management for proper BackPoke() commands...
* Add better DEBUG logging

This is an important optimization that prevents running a BackPoke on a
parent which is in the process of running and will most certainly poke
the caller back in a moment. This avoids unnecessary roundtrips.
Unfortunately, there are still other algorithms required so that races
can't cause the graph to run for longer than necessary.
2016-01-12 04:57:05 -05:00
James Shubin
c57946e29b Fix dependency issue
* Fix Process() object calling
* Add PokeParent() to poke upwards
* Break linear exec chains :(

This was the issue where in a graph f1 -> f2, if you were to rm f2 &&
cat f2, then f2 would not come back because we didn't poke upwards to
refresh the timestamp. Unfortunately this adds another bug which we
solve in a later patch.
2016-01-12 04:20:47 -05:00
James Shubin
48eddc3721 Catch a different form of etcd disconnect 2016-01-10 04:09:28 -05:00
James Shubin
8ea8ef8d0e Simplify converge checker
Not sure why I didn't write it this way before...
2016-01-10 02:40:31 -05:00
James Shubin
1c49bbc487 Clean up the distributed example for clarity 2016-01-10 02:30:05 -05:00
James Shubin
ebc1c60063 The noop type is not useful in this example 2016-01-10 01:42:42 -05:00
James Shubin
590394b2be Clean up better 2016-01-10 01:42:25 -05:00
James Shubin
97664c3b13 Fix effective off-by-one error in dependency processing
Graph vertices have initial values of 0 for their timestamps, which led
to the need of a >= comparison instead.
2016-01-09 22:16:37 -05:00
James Shubin
ea7fd76f93 Add exec type and fix up a few other things
* Add exec type
* Switch erroneous use of fmt to log instead
* Check for edge existence for safety before using
* Avoid recalling etcd channel maker
* Clean up logging output
2016-01-09 21:50:21 -05:00
James Shubin
45ff3b6aa4 Merge type comparison into a single function call 2016-01-08 04:13:42 -05:00
James Shubin
d769309cc0 Hello 2016! 2016-01-08 02:43:38 -05:00
James Shubin
d2bcfdc7aa Fix go vet error 2016-01-07 00:52:42 -05:00
James Shubin
72525d30b1 Refactor etcd into object and add exit timers
This refactors my etcd use into a struct (object) wrapper, which makes
it easier to add an exit on converged timer.
2016-01-06 19:40:09 -05:00
James Shubin
95489b9c07 Add information on which libraries are being used 2016-01-05 03:14:22 -05:00
James Shubin
0bbfd1d071 Add missing stringer dependency
This is used during the `go generate` pre-processor.
2016-01-05 03:03:30 -05:00
James Shubin
904ace8027 Fix up go vet errors and integrate with ci 2016-01-04 21:02:22 -05:00
James Shubin
d8cbeb56f9 Support N distributed agents
This is the third main feature of this system. The code needs a bunch of
polish, but it actually all works :)

I've tested this briefly with N <= 3.

Currently you have to build your own etcd cluster. It's quite easy, just
run `etcd` and it will be ready. I usually run it in a throw away /tmp/
dir so that I can blow away the stored data easily.
2016-01-04 21:00:13 -05:00
James Shubin
72a8027b7f Update README 2015-12-29 01:45:26 -05:00
James Shubin
39f7c305f1 Ira deserves to be mentioned in the THANKS list 2015-12-29 01:23:48 -05:00
James Shubin
1ba6be2957 Add graphviz generation and visualization
This requires graphviz to be installed on your machine. If you run the
command with sudo, it will create the files with the original user
ownership to make it easier to remove them without root.
2015-12-29 01:04:03 -05:00
James Shubin
6b4fa21074 Mega patch
This is still a dirty prototype, so please excuse the mess. Please
excuse the fact that this is a mega patch. Once things settle down this
won't happen any more.

Some of the changes squashed into here include:
* Merge vertex loop with type loop
(The file watcher seems to cache events anyways)
* Improve pgraph library
* Add indegree, outdegree, and topological sort with tests
* Add reverse function for vertex list
* Tons of additional cleanup!

Amazingly, on my first successful compile, this seemed to run!

A special thanks to Ira Cooper who helped me talk through some of the
algorithmic decisions and for his help in finding better ones!
2015-12-21 03:27:25 -05:00
James Shubin
0ea6f30ef2 Reorganize testing for developer efficiency 2015-10-12 19:26:58 -04:00
James Shubin
4f6605b3d1 Don't format or check omv.yaml syntax
Different versions of ruby format differently, so don't do this check
since it will invariably fail for someone. If there is a general
deterministic fix, please let me know :)
2015-10-10 18:14:05 -04:00
James Shubin
e44da9578e Add equals sign to pass in variables
This is something that is required in future go versions.
2015-10-06 23:56:45 -04:00
James Shubin
dd3759ae38 Update gofmt test to allow version 1.5 2015-10-06 20:34:56 -04:00
James Shubin
3e4709d9da Add tag script 2015-10-02 11:05:19 -04:00
James Shubin
66e030a175 Add more shields! 2015-09-29 03:47:58 -04:00
James Shubin
451fb35f93 Add missing watch event for files
If a file was supposed to exist in a directory, and it didn't exist yet,
when it gets created, we should notice, and cause an event so that we
wake up and actually see about then creating that file!
2015-09-26 03:28:05 -04:00
James Shubin
8dbca80853 Add omv support 2015-09-26 00:25:41 -04:00
James Shubin
6150c2ccb9 Small grep flag fix so command is idempotent 2015-09-25 12:38:24 -04:00
James Shubin
2708223ab5 Put all the deps in one script 2015-09-25 12:29:36 -04:00
James Shubin
327c5fb6fb Fix small typo 2015-09-25 12:25:52 -04:00
James Shubin
f789cf1403 Add travis-ci integration 2015-09-25 02:21:36 -04:00
James Shubin
19a909001b Add better reporting of errors in yaml formatting test 2015-09-25 02:06:14 -04:00
73 changed files with 4542 additions and 513 deletions

6
.gitignore vendored
View File

@@ -1,3 +1,9 @@
.omv/
.ssh/
.vagrant/
mgmt-documentation.pdf
old/
tmp/
*_stringer.go
mgmt
rpmbuild/

25
.travis.yml Normal file
View File

@@ -0,0 +1,25 @@
language: go
go:
- 1.4.3
- 1.5.2
- tip
dist: trusty
sudo: required
install: 'make deps'
script: 'make test'
matrix:
allow_failures:
- go: tip
notifications:
irc:
channels:
- "irc.freenode.net#mgmtconfig"
template:
- "%{repository} (%{commit}: %{author}): %{message}"
- "More info : %{build_url}"
on_success: always
on_failure: always
use_notice: false
skip_join: false
email:
- travis-ci@shubin.ca

View File

@@ -1,5 +1,5 @@
Mgmt
Copyright (C) 2013-2015+ James Shubin and the project contributors
Copyright (C) 2013-2016+ James Shubin and the project contributors
Written by James Shubin <james@shubin.ca> and the project contributors
This program is free software: you can redistribute it and/or modify

View File

@@ -2,7 +2,7 @@
<!--
Mgmt
Copyright (C) 2013-2015+ James Shubin and the project contributors
Copyright (C) 2013-2016+ James Shubin and the project contributors
Written by James Shubin <james@shubin.ca> and the project contributors
This program is free software: you can redistribute it and/or modify
@@ -67,6 +67,14 @@ I wanted a next generation config management solution that didn't have all of
the design flaws or limitations that the current generation of tools do, and no
tool existed!
###Why did you use etcd? What about consul?
Etcd and consul are both written in golang, which made them the top two
contenders for my prototype. Ultimately a choice had to be made, and etcd was
chosen, but it was also somewhat arbitrary. If there is available interest,
good reasoning, *and* patches, then we would consider either switching or
supporting both, but this is not a high priority at this time.
###You didn't answer my question, or I have a question!
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
@@ -99,6 +107,13 @@ documentation, please run `mgmt --help`.
####`--file <graph.yaml>`
Point to a graph file to run.
####`--converged-timeout <seconds>`
Exit if the machine has converged for approximately this many seconds.
####`--max-runtime <seconds>`
Exit when the agent has run for approximately this many seconds. This is not
generally recommended, but may be useful for users who know what they're doing.
##Examples
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git
source repository. It is available from:
@@ -117,7 +132,7 @@ To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt
##Authors
Copyright (C) 2013-2015+ James Shubin and the project contributors
Copyright (C) 2013-2016+ James Shubin and the project contributors
Please see the
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file

206
Makefile
View File

@@ -1,9 +1,41 @@
# Mgmt
# Copyright (C) 2013-2016+ James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SHELL = /bin/bash
.PHONY: all version run race build clean test format docs
.PHONY: all version program path deps run race build clean test format docs rpmbuild rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
.SILENT: clean
VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty)
PROGRAM := $(notdir $(CURDIR))
SVERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)
VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)
PROGRAM := $(shell basename --suffix=-$(VERSION) $(notdir $(CURDIR)))
ifeq ($(VERSION),$(SVERSION))
RELEASE = 1
else
RELEASE = untagged
endif
ARCH = $(shell arch)
SPEC = rpmbuild/SPECS/mgmt.spec
SOURCE = rpmbuild/SOURCES/mgmt-$(VERSION).tar.bz2
SRPM = rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).src.rpm
SRPM_BASE = mgmt-$(VERSION)-$(RELEASE).src.rpm
RPM = rpmbuild/RPMS/mgmt-$(VERSION)-$(RELEASE).$(ARCH).rpm
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
SERVER = 'dl.fedoraproject.org'
REMOTE_PATH = 'pub/alt/$(USERNAME)/mgmt'
all: docs
@@ -11,35 +43,179 @@ all: docs
version:
@echo $(VERSION)
run:
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)"
program:
@echo $(PROGRAM)
# include race test
path:
./misc/make-path.sh
deps:
./misc/make-deps.sh
run:
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
# include race flag
race:
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)"
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
build: mgmt
mgmt: main.go
go build -ldflags "-X main.version $(VERSION) -X main.program $(PROGRAM)"
@echo "Building: $(PROGRAM), version: $(SVERSION)."
go generate
# avoid equals sign in old golang versions eg in: -X foo=bar
if go version | grep -qE 'go1.3|go1.4'; then \
go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o mgmt; \
else \
go build -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o mgmt; \
fi
clean:
[ ! -e mgmt ] || rm mgmt
rm -f *_stringer.go # generated by `go generate`
test:
./test.sh
./test/test-gofmt.sh
./test/test-yamlfmt.sh
go test
#go test ./pgraph
go test -race
#go test -race ./pgraph
format:
find -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
find -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml; File.open('{}', 'w').write x" \;
find -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \;
docs: mgmt-documentation.pdf
mgmt-documentation.pdf: DOCUMENTATION.md
pandoc DOCUMENTATION.md -o 'mgmt-documentation.pdf'
#
# build aliases
#
# TODO: does making an rpm depend on making a .srpm first ?
rpm: $(SRPM) $(RPM)
# do nothing
srpm: $(SRPM)
# do nothing
spec: $(SPEC)
# do nothing
tar: $(SOURCE)
# do nothing
rpmbuild/SOURCES/: tar
rpmbuild/SRPMS/: srpm
rpmbuild/RPMS/: rpm
upload: upload-sources upload-srpms upload-rpms
# do nothing
#
# rpmbuild
#
$(RPM): $(SPEC) $(SOURCE)
@echo Running rpmbuild -bb...
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \
mv rpmbuild/RPMS/$(ARCH)/mgmt-$(VERSION)-$(RELEASE).*.rpm $(RPM)
$(SRPM): $(SPEC) $(SOURCE)
@echo Running rpmbuild -bs...
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC)
# renaming is not needed because we aren't using the dist variable
#mv rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM)
#
# spec
#
$(SPEC): rpmbuild/ mgmt.spec.in
@echo Running templater...
#cat mgmt.spec.in > $(SPEC)
sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < mgmt.spec.in > $(SPEC)
# append a changelog to the .spec file
git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC)
#
# archive
#
$(SOURCE): rpmbuild/
@echo Running git archive...
# use HEAD if tag doesn't exist yet, so that development is easier...
git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist. Using HEAD instead.' && git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) HEAD)
# TODO: if git archive had a --submodules flag this would easier!
@echo Running git archive submodules...
# i thought i would need --ignore-zeros, but it doesn't seem necessary!
p=`pwd` && (echo .; git submodule foreach) | while read entering path; do \
temp="$${path%\'}"; \
temp="$${temp#\'}"; \
path=$$temp; \
[ "$$path" = "" ] && continue; \
(cd $$path && git archive --prefix=mgmt-$(VERSION)/$$path/ HEAD > $$p/rpmbuild/tmp.tar && tar --concatenate --file=$$p/$(SOURCE) $$p/rpmbuild/tmp.tar && rm $$p/rpmbuild/tmp.tar); \
done
# TODO: ensure that each sub directory exists
rpmbuild/:
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
rpmbuild:
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
#
# sha256sum
#
rpmbuild/SOURCES/SHA256SUMS: rpmbuild/SOURCES/ $(SOURCE)
@echo Running SOURCES sha256sum...
cd rpmbuild/SOURCES/ && sha256sum *.tar.bz2 > SHA256SUMS; cd -
rpmbuild/SRPMS/SHA256SUMS: rpmbuild/SRPMS/ $(SRPM)
@echo Running SRPMS sha256sum...
cd rpmbuild/SRPMS/ && sha256sum *src.rpm > SHA256SUMS; cd -
rpmbuild/RPMS/SHA256SUMS: rpmbuild/RPMS/ $(RPM)
@echo Running RPMS sha256sum...
cd rpmbuild/RPMS/ && sha256sum *.rpm > SHA256SUMS; cd -
#
# gpg
#
rpmbuild/SOURCES/SHA256SUMS.asc: rpmbuild/SOURCES/SHA256SUMS
@echo Running SOURCES gpg...
# the --yes forces an overwrite of the SHA256SUMS.asc if necessary
gpg2 --yes --clearsign rpmbuild/SOURCES/SHA256SUMS
rpmbuild/SRPMS/SHA256SUMS.asc: rpmbuild/SRPMS/SHA256SUMS
@echo Running SRPMS gpg...
gpg2 --yes --clearsign rpmbuild/SRPMS/SHA256SUMS
rpmbuild/RPMS/SHA256SUMS.asc: rpmbuild/RPMS/SHA256SUMS
@echo Running RPMS gpg...
gpg2 --yes --clearsign rpmbuild/RPMS/SHA256SUMS
#
# upload
#
# upload to public server
upload-sources: rpmbuild/SOURCES/ rpmbuild/SOURCES/SHA256SUMS rpmbuild/SOURCES/SHA256SUMS.asc
if [ "`cat rpmbuild/SOURCES/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SOURCES/ && cat SHA256SUMS'`" ]; then \
echo Running SOURCES upload...; \
rsync -avz rpmbuild/SOURCES/ $(SERVER):$(REMOTE_PATH)/SOURCES/; \
fi
upload-srpms: rpmbuild/SRPMS/ rpmbuild/SRPMS/SHA256SUMS rpmbuild/SRPMS/SHA256SUMS.asc
if [ "`cat rpmbuild/SRPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/SRPMS/ && cat SHA256SUMS'`" ]; then \
echo Running SRPMS upload...; \
rsync -avz rpmbuild/SRPMS/ $(SERVER):$(REMOTE_PATH)/SRPMS/; \
fi
upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.asc
if [ "`cat rpmbuild/RPMS/SHA256SUMS`" != "`ssh $(SERVER) 'cd $(REMOTE_PATH)/RPMS/ && cat SHA256SUMS'`" ]; then \
echo Running RPMS upload...; \
rsync -avz --prune-empty-dirs rpmbuild/RPMS/ $(SERVER):$(REMOTE_PATH)/RPMS/; \
fi
#
# copr build
#
copr: upload-srpms
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
# vim: ts=8

View File

@@ -1,35 +1,65 @@
# *mgmt*: This is: mgmt!
[![Build Status](https://secure.travis-ci.org/purpleidea/mgmt.png)](http://travis-ci.org/purpleidea/mgmt)
[![Build Status](https://secure.travis-ci.org/purpleidea/mgmt.png?branch=master)](http://travis-ci.org/purpleidea/mgmt)
[![Documentation](https://img.shields.io/docs/markdown.png)](DOCUMENTATION.md)
[![IRC](https://img.shields.io/irc/%23mgmtconfig.png)](https://webchat.freenode.net/?channels=#mgmtconfig)
[![Jenkins](https://img.shields.io/jenkins/status.png)](https://ci.centos.org/job/purpleidea-mgmt/)
[![COPR](https://img.shields.io/copr/builds.png)](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
## Community:
Come join us on IRC in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode!
You may like the [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) hashtag if you're on [Twitter](https://twitter.com/#!/purpleidea).
## Questions:
Please join the [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) IRC community!
If you have a well phrased question that might benefit others, consider asking it by sending a patch to the documentation [FAQ](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md#usage-and-frequently-asked-questions) section. I'll merge your question, and a patch with the answer!
## Quick start:
* Either get the golang dependencies on your own, or run `make deps` if you're comfortable with how we install them.
* Run `make build` to get a fresh built `mgmt` binary.
* Run `cd $(mktemp --tmpdir -d tmp.XXX) && etcd` to get etcd running. The `mgmt` software will do this automatically for you in the future.
* Run `time ./mgmt run --file examples/graph0.yaml --converged-timeout=1` to try out a very simple example!
* To run continuously in the default mode of operation, omit the `--converged-timeout` option.
* Have fun hacking on our future technology!
## Examples:
Please look in the [examples/](examples/) folder for more examples!
## Documentation:
Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md).
## Questions:
Come join us in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode!
## Roadmap:
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
Please get involved by working on one of these items or by suggesting something else!
## Examples:
Please look in the [examples/](examples/) folder for usage. If none exist, please contribute one!
## Notes:
* This is currently a research project into next generation config management technologies!
* This is my first complex project in golang, please notify me of any issues.
* I have some well thought out designs for the future of this project, which I'll try and write up clearly and publish as soon as possible.
* Please don't expect stable interfaces, code, or any data safety.
* This design is the result of ideas I've had from hacking on advanced config management projects.
* I first started hacking on this in ~2013, even though I had very little time for it.
* I couldn't think of a good name for the project, so it's now being called `mgmt` until someone contributes a better one!
* I've published a number of articles about this tool:
* TODO
* There are some screencasts available:
* TODO
## Bugs:
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go) to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell) or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible test case.
## Dependencies:
* golang (available in most distros)
* pandoc (for building a pdf of the documentation)
* golang 1.4 or higher (required, available in most distros)
* golang libraries (required, available with `go get`)
go get github.com/coreos/etcd/client
go get gopkg.in/yaml.v2
go get gopkg.in/fsnotify.v1
go get github.com/codegangsta/cli
go get github.com/coreos/go-systemd/dbus
go get github.com/coreos/go-systemd/util
* stringer (required for building), available as a package on some platforms, otherwise via `go get`
go get golang.org/x/tools/cmd/stringer
* pandoc (optional, for building a pdf of the documentation)
* graphviz (optional, for building a visual representation of the graph)
## Patches:
We'd love to have your patch! Please send it by email, or as a pull request.
We'd love to have your patches! Please send them by email, or as a pull request.
## On the web:
* Introductory blog post: [https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
* Julian Dunn at Cfgmgmtcamp 2016 [https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1)
##

2
THANKS
View File

@@ -9,6 +9,8 @@ Chris Wright - For encouraging me to continue work on my prototype.
Daniel Riek - For supporting and sheltering this project from bureaucracy.
Ira Cooper - For having an algorithmic design discussion with me.
Jeff Darcy - For some algorithm recommendations, and NACKing my TopoSort idea!
Red Hat, inc. - For paying my salary, thus financially supporting my hacking.

43
TODO.md Normal file
View File

@@ -0,0 +1,43 @@
# TODO
If you're looking for something to do, look here!
Let us know if you're working on one of the items.
## Package resource
- [ ] base type [bug](https://github.com/purpleidea/mgmt/issues/11)
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
- [ ] install signal blocker [bug](https://github.com/hughsie/PackageKit/issues/109)
## File resource
- [ ] ability to make/delete folders
- [ ] recursive argument (can recursively watch/modify contents)
- [ ] force argument (can cause switch from file <-> folder)
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
## Exec resource
- [ ] base resource improvements
## Timer resource
- [ ] base resource
- [ ] reset on recompile
- [ ] increment algorithm (linear, exponential, etc...)
## Etcd improvements
- [ ] embedded etcd master
- [ ] capnslog fixes [bug](https://github.com/coreos/etcd/issues/4115)
## Language improvements
- [ ] language design
- [ ] lexer/parser
- [ ] automatic language formatter, ala `gofmt`
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
- [ ] vim syntax highlighting
- [ ] emacs syntax highlighting
## Other
- [ ] better error/retry handling
- [ ] resource grouping
- [ ] automatic dependency adding (eg: packagekit file dependencies)
- [ ] rpm package target in Makefile
- [ ] deb package target in Makefile
- [ ] reproducible builds
- [ ] add your suggestions!

207
config.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -22,23 +22,12 @@ import (
"gopkg.in/yaml.v2"
"io/ioutil"
"log"
"strings"
)
type noopTypeConfig struct {
Name string `yaml:"name"`
}
type fileTypeConfig struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
Content string `yaml:"content"`
State string `yaml:"state"`
}
type serviceTypeConfig struct {
Name string `yaml:"name"`
State string `yaml:"state"`
Startup string `yaml:"startup"`
type collectorTypeConfig struct {
Type string `yaml:"type"`
Pattern string `yaml:"pattern"` // XXX: Not Implemented
}
type vertexConfig struct {
@@ -52,18 +41,20 @@ type edgeConfig struct {
To vertexConfig `yaml:"to"`
}
type graphConfig struct {
type GraphConfig struct {
Graph string `yaml:"graph"`
Types struct {
Noop []noopTypeConfig `yaml:"noop"`
File []fileTypeConfig `yaml:"file"`
Service []serviceTypeConfig `yaml:"service"`
Noop []NoopType `yaml:"noop"`
File []FileType `yaml:"file"`
Service []ServiceType `yaml:"service"`
Exec []ExecType `yaml:"exec"`
} `yaml:"types"`
Edges []edgeConfig `yaml:"edges"`
Comment string `yaml:"comment"`
Collector []collectorTypeConfig `yaml:"collect"`
Edges []edgeConfig `yaml:"edges"`
Comment string `yaml:"comment"`
}
func (c *graphConfig) Parse(data []byte) error {
func (c *GraphConfig) Parse(data []byte) error {
if err := yaml.Unmarshal(data, c); err != nil {
return err
}
@@ -73,54 +64,166 @@ func (c *graphConfig) Parse(data []byte) error {
return nil
}
func GraphFromConfig(filename string) *Graph {
func ParseConfigFromFile(filename string) *GraphConfig {
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Printf("Error: Config: ParseConfigFromFile: File: %v", err)
return nil
}
var NoopMap map[string]*Vertex = make(map[string]*Vertex)
var FileMap map[string]*Vertex = make(map[string]*Vertex)
var ServiceMap map[string]*Vertex = make(map[string]*Vertex)
var config GraphConfig
if err := config.Parse(data); err != nil {
log.Printf("Error: Config: ParseConfigFromFile: Parse: %v", err)
return nil
}
var lookup map[string]map[string]*Vertex = make(map[string]map[string]*Vertex)
return &config
}
// XXX: we need to fix this function so that it either fails without modifying
// the graph, passes successfully and modifies it, or basically panics i guess
// this way an invalid compilation can leave the old graph running, and we we
// don't modify a partial graph. so we really need to validate, and then perform
// whatever actions are necessary
// finding some way to do this on a copy of the graph, and then do a graph diff
// and merge the new data into the old graph would be more appropriate, in
// particular if we can ensure the graph merge can't fail. As for the putting
// of stuff into etcd, we should probably store the operations to complete in
// the new graph, and keep retrying until it succeeds, thus blocking any new
// etcd operations until that time.
func UpdateGraphFromConfig(config *GraphConfig, hostname string, g *Graph, etcdO *EtcdWObject) bool {
var NoopMap = make(map[string]*Vertex)
var FileMap = make(map[string]*Vertex)
var ServiceMap = make(map[string]*Vertex)
var ExecMap = make(map[string]*Vertex)
var lookup = make(map[string]map[string]*Vertex)
lookup["noop"] = NoopMap
lookup["file"] = FileMap
lookup["service"] = ServiceMap
lookup["exec"] = ExecMap
data, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatal(err)
}
//log.Printf("%+v", config) // debug
var config graphConfig
if err := config.Parse(data); err != nil {
log.Fatal(err)
}
//fmt.Printf("%+v\n", config) // debug
g.SetName(config.Graph) // set graph name
g := NewGraph(config.Graph)
var keep []*Vertex // list of vertex which are the same in new graph
for _, t := range config.Types.Noop {
NoopMap[t.Name] = NewVertex(t.Name, "noop")
// FIXME: duplicate of name stored twice... where should it go?
NoopMap[t.Name].Associate(NewNoopType(t.Name))
g.AddVertex(NoopMap[t.Name]) // call standalone in case not part of an edge
obj := NewNoopType(t.Name)
v := g.GetVertexMatch(obj)
if v == nil { // no match found
v = NewVertex(obj)
g.AddVertex(v) // call standalone in case not part of an edge
}
NoopMap[obj.Name] = v // used for constructing edges
keep = append(keep, v) // append
}
for _, t := range config.Types.File {
FileMap[t.Name] = NewVertex(t.Name, "file")
// FIXME: duplicate of name stored twice... where should it go?
FileMap[t.Name].Associate(NewFileType(t.Name, t.Path, t.Content, t.State))
g.AddVertex(FileMap[t.Name]) // call standalone in case not part of an edge
// XXX: should we export based on a @@ prefix, or a metaparam
// like exported => true || exported => (host pattern)||(other pattern?)
if strings.HasPrefix(t.Name, "@@") { // exported resource
// add to etcd storage...
t.Name = t.Name[2:] //slice off @@
if !etcdO.EtcdPut(hostname, t.Name, "file", t) {
log.Printf("Problem exporting file resource %v.", t.Name)
continue
}
} else {
obj := NewFileType(t.Name, t.Path, t.Dirname, t.Basename, t.Content, t.State)
v := g.GetVertexMatch(obj)
if v == nil { // no match found
v = NewVertex(obj)
g.AddVertex(v) // call standalone in case not part of an edge
}
FileMap[obj.Name] = v // used for constructing edges
keep = append(keep, v) // append
}
}
for _, t := range config.Types.Service {
ServiceMap[t.Name] = NewVertex(t.Name, "service")
// FIXME: duplicate of name stored twice... where should it go?
ServiceMap[t.Name].Associate(NewServiceType(t.Name, t.State, t.Startup))
g.AddVertex(ServiceMap[t.Name]) // call standalone in case not part of an edge
obj := NewServiceType(t.Name, t.State, t.Startup)
v := g.GetVertexMatch(obj)
if v == nil { // no match found
v = NewVertex(obj)
g.AddVertex(v) // call standalone in case not part of an edge
}
ServiceMap[obj.Name] = v // used for constructing edges
keep = append(keep, v) // append
}
for _, t := range config.Types.Exec {
obj := NewExecType(t.Name, t.Cmd, t.Shell, t.Timeout, t.WatchCmd, t.WatchShell, t.IfCmd, t.IfShell, t.PollInt, t.State)
v := g.GetVertexMatch(obj)
if v == nil { // no match found
v = NewVertex(obj)
g.AddVertex(v) // call standalone in case not part of an edge
}
ExecMap[obj.Name] = v // used for constructing edges
keep = append(keep, v) // append
}
// lookup from etcd graph
// do all the graph look ups in one single step, so that if the etcd
// database changes, we don't have a partial state of affairs...
nodes, ok := etcdO.EtcdGet()
if ok {
for _, t := range config.Collector {
// XXX: use t.Type and optionally t.Pattern to collect from etcd storage
log.Printf("Collect: %v; Pattern: %v", t.Type, t.Pattern)
for _, x := range etcdO.EtcdGetProcess(nodes, "file") {
var obj *FileType
if B64ToObj(x, &obj) != true {
log.Printf("Collect: File: %v not collected!", x)
continue
}
if t.Pattern != "" { // XXX: currently the pattern for files can only override the Dirname variable :P
obj.Dirname = t.Pattern
}
log.Printf("Collect: File: %v collected!", obj.GetName())
// XXX: similar to file add code:
v := g.GetVertexMatch(obj)
if v == nil { // no match found
obj.Init() // initialize go channels or things won't work!!!
v = NewVertex(obj)
g.AddVertex(v) // call standalone in case not part of an edge
}
FileMap[obj.GetName()] = v // used for constructing edges
keep = append(keep, v) // append
}
}
}
// get rid of any vertices we shouldn't "keep" (that aren't in new graph)
for _, v := range g.GetVertices() {
if !HasVertex(v, keep) {
// wait for exit before starting new graph!
v.Type.SendEvent(eventExit, true, false)
g.DeleteVertex(v)
}
}
for _, e := range config.Edges {
if _, ok := lookup[e.From.Type]; !ok {
return false
}
if _, ok := lookup[e.To.Type]; !ok {
return false
}
if _, ok := lookup[e.From.Type][e.From.Name]; !ok {
return false
}
if _, ok := lookup[e.To.Type][e.To.Name]; !ok {
return false
}
g.AddEdge(lookup[e.From.Type][e.From.Name], lookup[e.To.Type][e.To.Name], NewEdge(e.Name))
}
return g
return true
}

155
configwatch.go Normal file
View File

@@ -0,0 +1,155 @@
// Mgmt
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"gopkg.in/fsnotify.v1"
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
"log"
"math"
"path"
"strings"
"syscall"
)
// XXX: it would be great if we could reuse code between this and the file type
// XXX: patch this to submit it as part of go-fsnotify if they're interested...
func ConfigWatch(file string) chan bool {
ch := make(chan bool)
go func() {
var safename = path.Clean(file) // no trailing slash
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
patharray := PathSplit(safename) // tokenize the path
var index = len(patharray) // starting index
var current string // current "watcher" location
var deltaDepth int // depth delta between watcher and event
var send = false // send event?
for {
current = strings.Join(patharray[0:index], "/")
if current == "" { // the empty string top is the root dir ("/")
current = "/"
}
log.Printf("Watching: %v", current) // attempting to watch...
// initialize in the loop so that we can reset on rm-ed handles
err = watcher.Add(current)
if err != nil {
if err == syscall.ENOENT {
index-- // usually not found, move up one dir
} else if err == syscall.ENOSPC {
// XXX: occasionally: no space left on device,
// XXX: probably due to lack of inotify watches
log.Printf("Lack of watches for config(%v) error: %+v", file, err.Error) // 0x408da0
log.Fatal(err)
} else {
log.Printf("Unknown config(%v) error:", file)
log.Fatal(err)
}
index = int(math.Max(1, float64(index)))
continue
}
select {
case event := <-watcher.Events:
// the deeper you go, the bigger the deltaDepth is...
// this is the difference between what we're watching,
// and the event... doesn't mean we can't watch deeper
if current == event.Name {
deltaDepth = 0 // i was watching what i was looking for
} else if HasPathPrefix(event.Name, current) {
deltaDepth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
} else if HasPathPrefix(current, event.Name) {
deltaDepth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
} else {
// TODO different watchers get each others events!
// https://github.com/go-fsnotify/fsnotify/issues/95
// this happened with two values such as:
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
continue
}
//log.Printf("The delta depth is: %v", deltaDepth)
// if we have what we wanted, awesome, send an event...
if event.Name == safename {
//log.Println("Event!")
send = true
// file removed, move the watch upwards
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
//log.Println("Removal!")
watcher.Remove(current)
index--
}
// we must be a parent watcher, so descend in
if deltaDepth < 0 {
watcher.Remove(current)
index++
}
// if safename starts with event.Name, we're above, and no event should be sent
} else if HasPathPrefix(safename, event.Name) {
//log.Println("Above!")
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
log.Println("Removal!")
watcher.Remove(current)
index--
}
if deltaDepth < 0 {
log.Println("Parent!")
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
//send = true
}
watcher.Remove(current)
index++
}
// if event.Name startswith safename, send event, we're already deeper
} else if HasPathPrefix(event.Name, safename) {
//log.Println("Event2!")
//send = true
}
case err := <-watcher.Errors:
log.Println("error:", err)
log.Fatal(err)
}
// do our event sending all together to avoid duplicate msgs
if send {
send = false
ch <- true
}
}
//close(ch)
}()
return ch
}

300
etcd.go Normal file
View File

@@ -0,0 +1,300 @@
// Mgmt
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
etcd "github.com/coreos/etcd/client"
"log"
"math"
"strings"
"time"
)
//go:generate stringer -type=etcdMsg -output=etcdmsg_stringer.go
type etcdMsg int
const (
etcdStart etcdMsg = iota
etcdEvent
etcdFoo
etcdBar
)
//go:generate stringer -type=etcdConvergedState -output=etcdconvergedstate_stringer.go
type etcdConvergedState int
const (
etcdConvergedNil etcdConvergedState = iota
//etcdConverged
etcdConvergedTimeout
)
type EtcdWObject struct { // etcd wrapper object
seed string
ctimeout int
converged chan bool
kapi etcd.KeysAPI
convergedState etcdConvergedState
}
func (etcdO *EtcdWObject) GetConvergedState() etcdConvergedState {
return etcdO.convergedState
}
func (etcdO *EtcdWObject) SetConvergedState(state etcdConvergedState) {
etcdO.convergedState = state
}
func (etcdO *EtcdWObject) GetKAPI() etcd.KeysAPI {
if etcdO.kapi != nil { // memoize
return etcdO.kapi
}
cfg := etcd.Config{
Endpoints: []string{etcdO.seed},
Transport: etcd.DefaultTransport,
// set timeout per request to fail fast when the target endpoint is unavailable
HeaderTimeoutPerRequest: time.Second,
}
var c etcd.Client
var err error
c, err = etcd.New(cfg)
if err != nil {
// XXX: not sure if this ever errors
if cerr, ok := err.(*etcd.ClusterError); ok {
// XXX: not sure if this part ever matches
// not running or disconnected
if cerr == etcd.ErrClusterUnavailable {
log.Fatal("XXX: etcd: ErrClusterUnavailable")
} else {
log.Fatal("XXX: etcd: Unknown")
}
}
log.Fatal(err) // some unhandled error
}
etcdO.kapi = etcd.NewKeysAPI(c)
return etcdO.kapi
}
type EtcdChannelWatchResponse struct {
resp *etcd.Response
err error
}
// wrap the etcd watcher.Next blocking function inside of a channel
func (etcdO *EtcdWObject) EtcdChannelWatch(watcher etcd.Watcher, context etcd_context.Context) chan *EtcdChannelWatchResponse {
ch := make(chan *EtcdChannelWatchResponse)
go func() {
for {
resp, err := watcher.Next(context) // blocks here
ch <- &EtcdChannelWatchResponse{resp, err}
}
}()
return ch
}
func (etcdO *EtcdWObject) EtcdWatch() chan etcdMsg {
kapi := etcdO.GetKAPI()
ctimeout := etcdO.ctimeout
converged := etcdO.converged
// XXX: i think we need this buffered so that when we're hanging on the
// channel, which is inside the EtcdWatch main loop, we still want the
// calls to Get/Set on etcd to succeed, so blocking them here would
// kill the whole thing
ch := make(chan etcdMsg, 1) // XXX: buffer of at least 1 is required
go func(ch chan etcdMsg) {
tmin := 500 // initial (min) delay in ms
t := tmin // current time
tmult := 2 // multiplier for exponential delay
tmax := 16000 // max delay
watcher := kapi.Watcher("/exported/", &etcd.WatcherOptions{Recursive: true})
etcdch := etcdO.EtcdChannelWatch(watcher, etcd_context.Background())
for {
log.Printf("Etcd: Watching...")
var resp *etcd.Response // = nil by default
var err error
select {
case out := <-etcdch:
etcdO.SetConvergedState(etcdConvergedNil)
resp, err = out.resp, out.err
case _ = <-TimeAfterOrBlock(ctimeout):
etcdO.SetConvergedState(etcdConvergedTimeout)
converged <- true
continue
}
if err != nil {
if err == etcd_context.Canceled {
// ctx is canceled by another routine
log.Fatal("Canceled")
} else if err == etcd_context.DeadlineExceeded {
// ctx is attached with a deadline and it exceeded
log.Fatal("Deadline")
} else if cerr, ok := err.(*etcd.ClusterError); ok {
// not running or disconnected
// TODO: is there a better way to parse errors?
for _, e := range cerr.Errors {
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
t = int(math.Min(float64(t*tmult), float64(tmax)))
log.Printf("Etcd: Waiting %d ms for connection...", t)
time.Sleep(time.Duration(t) * time.Millisecond) // sleep for t ms
} else if e.Error() == "unexpected EOF" {
log.Printf("Etcd: Unexpected disconnect...")
} else if e.Error() == "EOF" {
log.Printf("Etcd: Disconnected...")
} else if strings.HasPrefix(e.Error(), "unsupported protocol scheme") {
// usually a bad peer endpoint value
log.Fatal("Bad peer endpoint value?")
} else {
log.Fatal("Woops: ", e.Error())
}
}
} else {
// bad cluster endpoints, which are not etcd servers
log.Fatal("Woops: ", err)
}
} else {
//log.Print(resp)
//log.Printf("Watcher().Node.Value(%v): %+v", key, resp.Node.Value)
// FIXME: we should actually reset when the server comes back, not here on msg!
//XXX: can we fix this with one of these patterns?: https://blog.golang.org/go-concurrency-patterns-timing-out-and
t = tmin // reset timer
// don't trigger event if nothing changed
if n, p := resp.Node, resp.PrevNode; resp.Action == "set" && p != nil {
if n.Key == p.Key && n.Value == p.Value {
continue
}
}
// FIXME: we get events on key/type/value changes for
// each type directory... ignore the non final ones...
// IOW, ignore everything except for the value or some
// field which gets set last... this could be the max count field thing...
log.Printf("Etcd: Value: %v", resp.Node.Value) // event
ch <- etcdEvent // event
}
} // end for loop
//close(ch)
}(ch) // call go routine
return ch
}
// helper function to store our data in etcd
func (etcdO *EtcdWObject) EtcdPut(hostname, key, typ string, obj interface{}) bool {
kapi := etcdO.GetKAPI()
output, ok := ObjToB64(obj)
if !ok {
log.Printf("Etcd: Could not encode %v key.", key)
return false
}
path := fmt.Sprintf("/exported/%s/types/%s/type", hostname, key)
_, err := kapi.Set(etcd_context.Background(), path, typ, nil)
// XXX validate...
path = fmt.Sprintf("/exported/%s/types/%s/value", hostname, key)
resp, err := kapi.Set(etcd_context.Background(), path, output, nil)
if err != nil {
if cerr, ok := err.(*etcd.ClusterError); ok {
// not running or disconnected
for _, e := range cerr.Errors {
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
}
//if e == etcd.ErrClusterUnavailable
}
}
log.Printf("Etcd: Could not store %v key.", key)
return false
}
log.Print("Etcd: ", resp) // w00t... bonus
return true
}
// lookup /exported/ node hierarchy
func (etcdO *EtcdWObject) EtcdGet() (etcd.Nodes, bool) {
kapi := etcdO.GetKAPI()
// key structure is /exported/<hostname>/types/...
resp, err := kapi.Get(etcd_context.Background(), "/exported/", &etcd.GetOptions{Recursive: true})
if err != nil {
return nil, false // not found
}
return resp.Node.Nodes, true
}
func (etcdO *EtcdWObject) EtcdGetProcess(nodes etcd.Nodes, typ string) []string {
//path := fmt.Sprintf("/exported/%s/types/", h)
top := "/exported/"
log.Printf("Etcd: Get: %+v", nodes) // Get().Nodes.Nodes
var output []string
for _, x := range nodes { // loop through hosts
if !strings.HasPrefix(x.Key, top) {
log.Fatal("Error!")
}
host := x.Key[len(top):]
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", -1, host, x.Nodes)
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", i, x.Key, x.Nodes)
types, ok := EtcdGetChildNodeByKey(x, "types")
if !ok {
continue
}
for _, y := range types.Nodes { // loop through types
//key := y.Key # UUID?
//log.Printf("Get(%v): TYPE[%v]", host, y.Key)
t, ok := EtcdGetChildNodeByKey(y, "type")
if !ok {
continue
}
if typ != "" && typ != t.Value {
continue
} // filter based on type
v, ok := EtcdGetChildNodeByKey(y, "value") // B64ToObj this
if !ok {
continue
}
log.Printf("Etcd: Hostname: %v; Get: %v", host, t.Value)
output = append(output, v.Value)
}
}
return output
}
// TODO: wrap this somehow so it's a method of *etcd.Node
// helper function that returns the node for a particular key under a node
func EtcdGetChildNodeByKey(node *etcd.Node, key string) (*etcd.Node, bool) {
for _, x := range node.Nodes {
if x.Key == fmt.Sprintf("%s/%s", node.Key, key) {
return x, true
}
}
return nil, false // not found
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -17,20 +17,39 @@
package main
import (
"code.google.com/p/go-uuid/uuid"
//go:generate stringer -type=eventName -output=eventname_stringer.go
type eventName int
const (
eventExit eventName = iota
eventStart
eventPause
eventPoke
eventBackPoke
)
type Event struct {
uuid string
Name string
Type string
Name eventName
Resp chan bool // channel to send an ack response on, nil to skip
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
Msg string // some words for fun
Activity bool // did something interesting happen?
}
func NewEvent(name, t string) *Event {
return &Event{
uuid: uuid.New(),
Name: name,
Type: t,
// send a single acknowledgement on the channel if one was requested
func (event *Event) ACK() {
if event.Resp != nil { // if they've requested an ACK
event.Resp <- true // send ACK
}
}
func (event *Event) NACK() {
if event.Resp != nil { // if they've requested an ACK
event.Resp <- false // send NACK
}
}
// get the activity value
func (event *Event) GetActivity() bool {
return event.Activity
}

20
examples/graph0.yaml Normal file
View File

@@ -0,0 +1,20 @@
---
graph: mygraph
comment: hello world example
types:
noop:
- name: noop1
file:
- name: file1
path: "/tmp/mgmt-hello-world"
content: |
hello world from @purpleidea
state: exists
edges:
- name: e1
from:
type: noop
name: noop1
to:
type: file
name: file1

View File

@@ -5,22 +5,22 @@ types:
- name: noop1
file:
- name: file1
path: /tmp/mgmt/f1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists
- name: file2
path: /tmp/mgmt/f2
path: "/tmp/mgmt/f2"
content: |
i am f2
state: exists
- name: file3
path: /tmp/mgmt/f3
path: "/tmp/mgmt/f3"
content: |
i am f3
state: exists
- name: file4
path: /tmp/mgmt/f4
path: "/tmp/mgmt/f4"
content: |
i am f4 and i should not be here
state: absent

128
examples/graph10.yaml Normal file
View File

@@ -0,0 +1,128 @@
---
graph: mygraph
comment: simple exec fan in to fan out example to demonstrate optimization
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec3
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec4
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec5
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec6
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec7
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec8
cmd: sleep 15s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec4
- name: e2
from:
type: exec
name: exec2
to:
type: exec
name: exec4
- name: e3
from:
type: exec
name: exec3
to:
type: exec
name: exec4
- name: e4
from:
type: exec
name: exec4
to:
type: exec
name: exec5
- name: e5
from:
type: exec
name: exec4
to:
type: exec
name: exec6
- name: e6
from:
type: exec
name: exec4
to:
type: exec
name: exec7

22
examples/graph1a.yaml Normal file
View File

@@ -0,0 +1,22 @@
---
graph: mygraph
types:
file:
- name: file1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists
- name: file2
path: "/tmp/mgmt/f2"
content: |
i am f2
state: exists
edges:
- name: e1
from:
type: file
name: file1
to:
type: file
name: file2

22
examples/graph1b.yaml Normal file
View File

@@ -0,0 +1,22 @@
---
graph: mygraph
types:
file:
- name: file2
path: "/tmp/mgmt/f2"
content: |
i am f2
state: exists
- name: file3
path: "/tmp/mgmt/f3"
content: |
i am f3
state: exists
edges:
- name: e2
from:
type: file
name: file2
to:
type: file
name: file3

View File

@@ -5,7 +5,7 @@ types:
- name: noop1
file:
- name: file1
path: /tmp/mgmt/f1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists

28
examples/graph3a.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1a
path: "/tmp/mgmtA/f1a"
content: |
i am f1
state: exists
- name: file2a
path: "/tmp/mgmtA/f2a"
content: |
i am f2
state: exists
- name: "@@file3a"
path: "/tmp/mgmtA/f3a"
content: |
i am f3, exported from host A
state: exists
- name: "@@file4a"
path: "/tmp/mgmtA/f4a"
content: |
i am f4, exported from host A
state: exists
collect:
- type: file
pattern: "/tmp/mgmtA/"
edges: []

28
examples/graph3b.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1b
path: "/tmp/mgmtB/f1b"
content: |
i am f1
state: exists
- name: file2b
path: "/tmp/mgmtB/f2b"
content: |
i am f2
state: exists
- name: "@@file3b"
path: "/tmp/mgmtB/f3b"
content: |
i am f3, exported from host B
state: exists
- name: "@@file4b"
path: "/tmp/mgmtB/f4b"
content: |
i am f4, exported from host B
state: exists
collect:
- type: file
pattern: "/tmp/mgmtB/"
edges: []

28
examples/graph3c.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1c
path: "/tmp/mgmtC/f1c"
content: |
i am f1
state: exists
- name: file2c
path: "/tmp/mgmtC/f2c"
content: |
i am f2
state: exists
- name: "@@file3c"
path: "/tmp/mgmtC/f3c"
content: |
i am f3, exported from host C
state: exists
- name: "@@file4c"
path: "/tmp/mgmtC/f4c"
content: |
i am f4, exported from host C
state: exists
collect:
- type: file
pattern: "/tmp/mgmtC/"
edges: []

18
examples/graph4.yaml Normal file
View File

@@ -0,0 +1,18 @@
---
graph: mygraph
types:
file:
- name: file1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists
- name: "@@file3"
path: "/tmp/mgmt/f3"
content: |
i am f3, exported from host A
state: exists
collect:
- type: file
pattern: ''
edges:

13
examples/graph5.yaml Normal file
View File

@@ -0,0 +1,13 @@
---
graph: mygraph
types:
file:
- name: file1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists
collect:
- type: file
pattern: ''
edges:

6
examples/graph6.yaml Normal file
View File

@@ -0,0 +1,6 @@
---
graph: mygraph
types:
noop:
- name: noop1
edges:

17
examples/graph7.yaml Normal file
View File

@@ -0,0 +1,17 @@
---
graph: mygraph
types:
noop:
- name: noop1
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:

59
examples/graph8.yaml Normal file
View File

@@ -0,0 +1,59 @@
---
graph: mygraph
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec3
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec4
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec2
- name: e2
from:
type: exec
name: exec2
to:
type: exec
name: exec3

32
examples/graph8a.yaml Normal file
View File

@@ -0,0 +1,32 @@
---
graph: mygraph
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec2

32
examples/graph8b.yaml Normal file
View File

@@ -0,0 +1,32 @@
---
graph: mygraph
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: 'true'
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: 'true'
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec2

32
examples/graph8c.yaml Normal file
View File

@@ -0,0 +1,32 @@
---
graph: mygraph
types:
exec:
- name: exec1
cmd: echo hello from exec1
shell: ''
timeout: 0
watchcmd: sleep 10s
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: echo hello from exec2
shell: ''
timeout: 0
watchcmd: sleep 10s
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec2

15
examples/graph8d.yaml Normal file
View File

@@ -0,0 +1,15 @@
---
graph: mygraph
types:
exec:
- name: exec1
cmd: echo hello from exec1
shell: ''
timeout: 0
watchcmd: sleep 5s
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges: []

77
examples/graph9.yaml Normal file
View File

@@ -0,0 +1,77 @@
---
graph: mygraph
comment: simple exec fan in example to demonstrate optimization
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec3
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec4
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec5
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec5
- name: e2
from:
type: exec
name: exec2
to:
type: exec
name: exec5
- name: e3
from:
type: exec
name: exec3
to:
type: exec
name: exec5

View File

@@ -0,0 +1,8 @@
[Unit]
Description=Fake service for testing
[Service]
ExecStart=/usr/bin/sleep 8h
[Install]
WantedBy=multi-user.target

339
exec.go Normal file
View File

@@ -0,0 +1,339 @@
// Mgmt
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bufio"
"bytes"
"log"
"os/exec"
"strings"
)
type ExecType struct {
BaseType `yaml:",inline"`
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
Cmd string `yaml:"cmd"` // the command to run
Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd
Timeout int `yaml:"timeout"` // the cmd timeout in seconds
WatchCmd string `yaml:"watchcmd"` // the watch command to run
WatchShell string `yaml:"watchshell"` // the (optional) shell to use to run the watch cmd
IfCmd string `yaml:"ifcmd"` // the if command to run
IfShell string `yaml:"ifshell"` // the (optional) shell to use to run the if cmd
PollInt int `yaml:"pollint"` // the poll interval for the ifcmd
}
func NewExecType(name, cmd, shell string, timeout int, watchcmd, watchshell, ifcmd, ifshell string, pollint int, state string) *ExecType {
// FIXME if path = nil, path = name ...
return &ExecType{
BaseType: BaseType{
Name: name,
events: make(chan Event),
vertex: nil,
},
Cmd: cmd,
Shell: shell,
Timeout: timeout,
WatchCmd: watchcmd,
WatchShell: watchshell,
IfCmd: ifcmd,
IfShell: ifshell,
PollInt: pollint,
State: state,
}
}
func (obj *ExecType) GetType() string {
return "Exec"
}
// validate if the params passed in are valid data
// FIXME: where should this get called ?
func (obj *ExecType) Validate() bool {
if obj.Cmd == "" { // this is the only thing that is really required
return false
}
// if we have a watch command, then we don't poll with the if command!
if obj.WatchCmd != "" && obj.PollInt > 0 {
return false
}
return true
}
// wraps the scanner output in a channel
func (obj *ExecType) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan error) {
ch, errch := make(chan string), make(chan error)
go func() {
for scanner.Scan() {
ch <- scanner.Text() // blocks here ?
if e := scanner.Err(); e != nil {
errch <- e // send any misc errors we encounter
//break // TODO ?
}
}
close(ch)
errch <- scanner.Err() // eof or some err
close(errch)
}()
return ch, errch
}
// Exec watcher
func (obj *ExecType) Watch() {
if obj.IsWatching() {
return
}
obj.SetWatching(true)
defer obj.SetWatching(false)
var send = false // send event?
var exit = false
bufioch, errch := make(chan string), make(chan error)
//vertex := obj.GetVertex() // stored with SetVertex
if obj.WatchCmd != "" {
var cmdName string
var cmdArgs []string
if obj.WatchShell == "" {
// call without a shell
// FIXME: are there still whitespace splitting issues?
split := strings.Fields(obj.WatchCmd)
cmdName = split[0]
//d, _ := os.Getwd() // TODO: how does this ever error ?
//cmdName = path.Join(d, cmdName)
cmdArgs = split[1:len(split)]
} else {
cmdName = obj.Shell // usually bash, or sh
cmdArgs = []string{"-c", obj.WatchCmd}
}
cmd := exec.Command(cmdName, cmdArgs...)
//cmd.Dir = "" // look for program in pwd ?
cmdReader, err := cmd.StdoutPipe()
if err != nil {
log.Printf("%v[%v]: Error creating StdoutPipe for Cmd: %v", obj.GetType(), obj.GetName(), err)
log.Fatal(err) // XXX: how should we handle errors?
}
scanner := bufio.NewScanner(cmdReader)
defer cmd.Wait() // XXX: is this necessary?
defer func() {
// FIXME: without wrapping this in this func it panic's
// when running examples/graph8d.yaml
cmd.Process.Kill() // TODO: is this necessary?
}()
if err := cmd.Start(); err != nil {
log.Printf("%v[%v]: Error starting Cmd: %v", obj.GetType(), obj.GetName(), err)
log.Fatal(err) // XXX: how should we handle errors?
}
bufioch, errch = obj.BufioChanScanner(scanner)
}
for {
obj.SetState(typeWatching) // reset
select {
case text := <-bufioch:
obj.SetConvergedState(typeConvergedNil)
// each time we get a line of output, we loop!
log.Printf("%v[%v]: Watch output: %s", obj.GetType(), obj.GetName(), text)
if text != "" {
send = true
}
case err := <-errch:
obj.SetConvergedState(typeConvergedNil) // XXX ?
if err == nil { // EOF
// FIXME: add an "if watch command ends/crashes"
// restart or generate error option
log.Printf("%v[%v]: Reached EOF", obj.GetType(), obj.GetName())
return
}
log.Printf("%v[%v]: Error reading input?: %v", obj.GetType(), obj.GetName(), err)
log.Fatal(err)
// XXX: how should we handle errors?
case event := <-obj.events:
obj.SetConvergedState(typeConvergedNil)
if exit, send = obj.ReadEvent(&event); exit {
return // exit
}
case _ = <-TimeAfterOrBlock(obj.ctimeout):
obj.SetConvergedState(typeConvergedTimeout)
obj.converged <- true
continue
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
// it is okay to invalidate the clean state on poke too
obj.isStateOK = false // something made state dirty
Process(obj) // XXX: rename this function
}
}
}
// TODO: expand the IfCmd to be a list of commands
func (obj *ExecType) StateOK() bool {
// if there is a watch command, but no if command, run based on state
if b := obj.isStateOK; obj.WatchCmd != "" && obj.IfCmd == "" {
obj.isStateOK = true // reset
//if !obj.isStateOK { obj.isStateOK = true; return false }
return b
// if there is no watcher, but there is an onlyif check, run it to see
} else if obj.IfCmd != "" { // && obj.WatchCmd == ""
// there is a watcher, but there is also an if command
//} else if obj.IfCmd != "" && obj.WatchCmd != "" {
if obj.PollInt > 0 { // && obj.WatchCmd == ""
// XXX have the Watch() command output onlyif poll events...
// XXX we can optimize by saving those results for returning here
// return XXX
}
var cmdName string
var cmdArgs []string
if obj.IfShell == "" {
// call without a shell
// FIXME: are there still whitespace splitting issues?
split := strings.Fields(obj.IfCmd)
cmdName = split[0]
//d, _ := os.Getwd() // TODO: how does this ever error ?
//cmdName = path.Join(d, cmdName)
cmdArgs = split[1:len(split)]
} else {
cmdName = obj.IfShell // usually bash, or sh
cmdArgs = []string{"-c", obj.IfCmd}
}
err := exec.Command(cmdName, cmdArgs...).Run()
if err != nil {
// TODO: check exit value
return true // don't run
}
return false // just run
// if there is no watcher and no onlyif check, assume we should run
} else { // if obj.WatchCmd == "" && obj.IfCmd == "" {
b := obj.isStateOK
obj.isStateOK = true
return b // just run if state is dirty
}
}
func (obj *ExecType) Apply() bool {
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
var cmdName string
var cmdArgs []string
if obj.Shell == "" {
// call without a shell
// FIXME: are there still whitespace splitting issues?
// TODO: we could make the split character user selectable...!
split := strings.Fields(obj.Cmd)
cmdName = split[0]
//d, _ := os.Getwd() // TODO: how does this ever error ?
//cmdName = path.Join(d, cmdName)
cmdArgs = split[1:len(split)]
} else {
cmdName = obj.Shell // usually bash, or sh
cmdArgs = []string{"-c", obj.Cmd}
}
cmd := exec.Command(cmdName, cmdArgs...)
//cmd.Dir = "" // look for program in pwd ?
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Start(); err != nil {
log.Printf("%v[%v]: Error starting Cmd: %v", obj.GetType(), obj.GetName(), err)
return false
}
timeout := obj.Timeout
if timeout == 0 { // zero timeout means no timer, so disable it
timeout = -1
}
done := make(chan error)
go func() { done <- cmd.Wait() }()
select {
case err := <-done:
if err != nil {
log.Printf("%v[%v]: Error waiting for Cmd: %v", obj.GetType(), obj.GetName(), err)
return false
}
case <-TimeAfterOrBlock(timeout):
log.Printf("%v[%v]: Timeout waiting for Cmd", obj.GetType(), obj.GetName())
//cmd.Process.Kill() // TODO: is this necessary?
return false
}
// TODO: if we printed the stdout while the command is running, this
// would be nice, but it would require terminal log output that doesn't
// interleave all the parallel parts which would mix it all up...
if s := out.String(); s == "" {
log.Printf("Exec[%v]: Command output is empty!", obj.Name)
} else {
log.Printf("Exec[%v]: Command output is:", obj.Name)
log.Printf(out.String())
}
// XXX: return based on exit value!!
return true
}
func (obj *ExecType) Compare(typ Type) bool {
switch typ.(type) {
case *ExecType:
typ := typ.(*ExecType)
if obj.Name != typ.Name {
return false
}
if obj.Cmd != typ.Cmd {
return false
}
if obj.Shell != typ.Shell {
return false
}
if obj.Timeout != typ.Timeout {
return false
}
if obj.WatchCmd != typ.WatchCmd {
return false
}
if obj.WatchShell != typ.WatchShell {
return false
}
if obj.IfCmd != typ.IfCmd {
return false
}
if obj.PollInt != typ.PollInt {
return false
}
if obj.State != typ.State {
return false
}
default:
return false
}
return true
}

258
file.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -18,10 +18,8 @@
package main
import (
"code.google.com/p/go-uuid/uuid"
"crypto/sha256"
"encoding/hex"
"fmt"
"gopkg.in/fsnotify.v1"
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
"io"
@@ -34,39 +32,82 @@ import (
)
type FileType struct {
uuid string
Type string // always "file"
Name string // name variable
Events chan string // FIXME: eventually a struct for the event?
Path string // path variable (should default to name)
Content string
State string // state: exists/present?, absent, (undefined?)
BaseType `yaml:",inline"`
Path string `yaml:"path"` // path variable (should default to name)
Dirname string `yaml:"dirname"`
Basename string `yaml:"basename"`
Content string `yaml:"content"`
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
sha256sum string
}
func NewFileType(name, path, content, state string) *FileType {
func NewFileType(name, path, dirname, basename, content, state string) *FileType {
// FIXME if path = nil, path = name ...
return &FileType{
uuid: uuid.New(),
Type: "file",
Name: name,
Events: make(chan string, 1), // XXX: chan size?
BaseType: BaseType{
Name: name,
events: make(chan Event),
vertex: nil,
},
Path: path,
Dirname: dirname,
Basename: basename,
Content: content,
State: state,
sha256sum: "",
}
}
func (obj *FileType) GetType() string {
return "File"
}
// validate if the params passed in are valid data
func (obj *FileType) Validate() bool {
if obj.Dirname != "" {
// must end with /
if obj.Dirname[len(obj.Dirname)-1:] != "/" {
return false
}
}
if obj.Basename != "" {
// must not start with /
if obj.Basename[0:1] == "/" {
return false
}
}
return true
}
func (obj *FileType) GetPath() string {
d := Dirname(obj.Path)
b := Basename(obj.Path)
if !obj.Validate() || (obj.Dirname == "" && obj.Basename == "") {
return obj.Path
} else if obj.Dirname == "" {
return d + obj.Basename
} else if obj.Basename == "" {
return obj.Dirname + b
} else { // if obj.dirname != "" && obj.basename != "" {
return obj.Dirname + obj.Basename
}
}
// File watcher for files and directories
// Modify with caution, probably important to write some test cases first!
func (obj FileType) Watch(v *Vertex) {
// obj.Path: file or directory
//var recursive bool = false
//var isdir = (obj.Path[len(obj.Path)-1:] == "/") // dirs have trailing slashes
//fmt.Printf("IsDirectory: %v\n", isdir)
// obj.GetPath(): file or directory
func (obj *FileType) Watch() {
if obj.IsWatching() {
return
}
obj.SetWatching(true)
defer obj.SetWatching(false)
var safename = path.Clean(obj.Path) // no trailing slash
//var recursive bool = false
//var isdir = (obj.GetPath()[len(obj.GetPath())-1:] == "/") // dirs have trailing slashes
//log.Printf("IsDirectory: %v", isdir)
//vertex := obj.GetVertex() // stored with SetVertex
var safename = path.Clean(obj.GetPath()) // no trailing slash
watcher, err := fsnotify.NewWatcher()
if err != nil {
@@ -77,90 +118,84 @@ func (obj FileType) Watch(v *Vertex) {
patharray := PathSplit(safename) // tokenize the path
var index = len(patharray) // starting index
var current string // current "watcher" location
var delta_depth int // depth delta between watcher and event
var deltaDepth int // depth delta between watcher and event
var send = false // send event?
var extraCheck = false
var exit = false
var dirty = false
for {
current = strings.Join(patharray[0:index], "/")
if current == "" { // the empty string top is the root dir ("/")
current = "/"
}
log.Printf("Watching: %v\n", current) // attempting to watch...
if DEBUG {
log.Printf("File[%v]: Watching: %v", obj.GetName(), current) // attempting to watch...
}
// initialize in the loop so that we can reset on rm-ed handles
err = watcher.Add(current)
if err != nil {
if DEBUG {
log.Printf("File[%v]: watcher.Add(%v): Error: %v", obj.GetName(), current, err)
}
if err == syscall.ENOENT {
index-- // usually not found, move up one dir
} else if err == syscall.ENOSPC {
// XXX: i sometimes see: no space left on device
// XXX: why causes this to happen ?
log.Printf("Strange file[%v] error: %+v\n", obj.Name, err.Error) // 0x408da0
// XXX: occasionally: no space left on device,
// XXX: probably due to lack of inotify watches
log.Printf("Lack of watches for file[%v] error: %+v", obj.Name, err.Error) // 0x408da0
log.Fatal(err)
} else {
log.Printf("Unknown file[%v] error:\n", obj.Name)
log.Printf("Unknown file[%v] error:", obj.Name)
log.Fatal(err)
}
index = int(math.Max(1, float64(index)))
continue
}
// XXX: check state after inotify started
// SMALL RACE: after we terminate watch, till when it's started
// something could have gotten created/changed/etc... right?
if extraCheck {
extraCheck = false
// XXX
//if exists ... {
// send signal
// continue
// change index? i don't think so. be thorough and check
//}
}
obj.SetState(typeWatching) // reset
select {
case event := <-watcher.Events:
// the deeper you go, the bigger the delta_depth is...
if DEBUG {
log.Printf("File[%v]: Watch(%v), Event(%v): %v", obj.GetName(), current, event.Name, event.Op)
}
obj.SetConvergedState(typeConvergedNil) // XXX: technically i can detect if the event is erroneous or not first
// the deeper you go, the bigger the deltaDepth is...
// this is the difference between what we're watching,
// and the event... doesn't mean we can't watch deeper
if current == event.Name {
delta_depth = 0 // i was watching what i was looking for
deltaDepth = 0 // i was watching what i was looking for
} else if HasPathPrefix(event.Name, current) {
delta_depth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
deltaDepth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
} else if HasPathPrefix(current, event.Name) {
delta_depth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
deltaDepth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
} else {
// XXX multiple watchers receive each others events
// TODO different watchers get each others events!
// https://github.com/go-fsnotify/fsnotify/issues/95
// this happened with two values such as:
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
// are the different watchers getting each others events??
//log.Printf("The delta depth is NaN...\n")
//log.Printf("Value of event.Name is: %v\n", event.Name)
//log.Printf("........ current is: %v\n", current)
//log.Fatal("The delta depth is NaN!")
continue
}
//log.Printf("The delta depth is: %v\n", delta_depth)
//log.Printf("The delta depth is: %v", deltaDepth)
// if we have what we wanted, awesome, send an event...
if event.Name == safename {
//log.Println("Event!")
// FIXME: should all these below cases trigger?
send = true
dirty = true
// file removed, move the watch upwards
if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
//log.Println("Removal!")
watcher.Remove(current)
index--
}
// we must be a parent watcher, so descend in
if delta_depth < 0 {
if deltaDepth < 0 {
watcher.Remove(current)
index++
}
@@ -169,13 +204,18 @@ func (obj FileType) Watch(v *Vertex) {
} else if HasPathPrefix(safename, event.Name) {
//log.Println("Above!")
if delta_depth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
log.Println("Removal!")
watcher.Remove(current)
index--
}
if delta_depth < 0 {
if deltaDepth < 0 {
log.Println("Parent!")
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
send = true
dirty = true
}
watcher.Remove(current)
index++
}
@@ -184,37 +224,42 @@ func (obj FileType) Watch(v *Vertex) {
} else if HasPathPrefix(event.Name, safename) {
//log.Println("Event2!")
send = true
dirty = true
}
case err := <-watcher.Errors:
obj.SetConvergedState(typeConvergedNil) // XXX ?
log.Println("error:", err)
log.Fatal(err)
v.Events <- fmt.Sprintf("file: %v", "error")
//obj.events <- fmt.Sprintf("file: %v", "error") // XXX: how should we handle errors?
case exit := <-obj.Events:
if exit == "exit" {
return
} else {
log.Fatal("Unknown event: %v\n", exit)
case event := <-obj.events:
obj.SetConvergedState(typeConvergedNil)
if exit, send = obj.ReadEvent(&event); exit {
return // exit
}
//dirty = false // these events don't invalidate state
case _ = <-TimeAfterOrBlock(obj.ctimeout):
obj.SetConvergedState(typeConvergedTimeout)
obj.converged <- true
continue
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
//log.Println("Sending event!")
//v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, event.Op)
v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, "event!") // FIXME: use struct
// only invalid state on certain types of events
if dirty {
dirty = false
obj.isStateOK = false // something made state dirty
}
Process(obj) // XXX: rename this function
}
}
}
func (obj FileType) Exit() bool {
obj.Events <- "exit"
return true
}
func (obj FileType) HashSHA256fromContent() string {
func (obj *FileType) HashSHA256fromContent() string {
if obj.sha256sum != "" { // return if already computed
return obj.sha256sum
}
@@ -225,11 +270,16 @@ func (obj FileType) HashSHA256fromContent() string {
return obj.sha256sum
}
func (obj FileType) StateOK() bool {
if _, err := os.Stat(obj.Path); os.IsNotExist(err) {
// FIXME: add the obj.CleanState() calls all over the true returns!
func (obj *FileType) StateOK() bool {
if obj.isStateOK { // cache the state
return true
}
if _, err := os.Stat(obj.GetPath()); os.IsNotExist(err) {
// no such file or directory
if obj.State == "absent" {
return true // missing file should be missing, phew :)
return obj.CleanState() // missing file should be missing, phew :)
} else {
// state invalid, skip expensive checksums
return false
@@ -238,15 +288,15 @@ func (obj FileType) StateOK() bool {
// TODO: add file mode check here...
if PathIsDir(obj.Path) {
if PathIsDir(obj.GetPath()) {
return obj.StateOKDir()
} else {
return obj.StateOKFile()
}
}
func (obj FileType) StateOKFile() bool {
if PathIsDir(obj.Path) {
func (obj *FileType) StateOKFile() bool {
if PathIsDir(obj.GetPath()) {
log.Fatal("This should only be called on a File type.")
}
@@ -254,7 +304,7 @@ func (obj FileType) StateOKFile() bool {
hash := sha256.New()
f, err := os.Open(obj.Path)
f, err := os.Open(obj.GetPath())
if err != nil {
//log.Fatal(err)
return false
@@ -267,7 +317,7 @@ func (obj FileType) StateOKFile() bool {
}
sha256sum := hex.EncodeToString(hash.Sum(nil))
//fmt.Printf("sha256sum: %v\n", sha256sum)
//log.Printf("sha256sum: %v", sha256sum)
if obj.HashSHA256fromContent() == sha256sum {
return true
@@ -276,8 +326,8 @@ func (obj FileType) StateOKFile() bool {
return false
}
func (obj FileType) StateOKDir() bool {
if !PathIsDir(obj.Path) {
func (obj *FileType) StateOKDir() bool {
if !PathIsDir(obj.GetPath()) {
log.Fatal("This should only be called on a Dir type.")
}
@@ -286,33 +336,33 @@ func (obj FileType) StateOKDir() bool {
return false
}
func (obj FileType) Apply() bool {
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
func (obj *FileType) Apply() bool {
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
if PathIsDir(obj.Path) {
if PathIsDir(obj.GetPath()) {
return obj.ApplyDir()
} else {
return obj.ApplyFile()
}
}
func (obj FileType) ApplyFile() bool {
func (obj *FileType) ApplyFile() bool {
if PathIsDir(obj.Path) {
if PathIsDir(obj.GetPath()) {
log.Fatal("This should only be called on a File type.")
}
if obj.State == "absent" {
log.Printf("About to remove: %v\n", obj.Path)
err := os.Remove(obj.Path)
log.Printf("About to remove: %v", obj.GetPath())
err := os.Remove(obj.GetPath())
if err != nil {
return false
}
return true
}
//fmt.Println("writing: " + filename)
f, err := os.Create(obj.Path)
//log.Println("writing: " + filename)
f, err := os.Create(obj.GetPath())
if err != nil {
log.Println("error:", err)
return false
@@ -328,8 +378,8 @@ func (obj FileType) ApplyFile() bool {
return true
}
func (obj FileType) ApplyDir() bool {
if !PathIsDir(obj.Path) {
func (obj *FileType) ApplyDir() bool {
if !PathIsDir(obj.GetPath()) {
log.Fatal("This should only be called on a Dir type.")
}
@@ -337,3 +387,25 @@ func (obj FileType) ApplyDir() bool {
log.Fatal("Not implemented!")
return true
}
func (obj *FileType) Compare(typ Type) bool {
switch typ.(type) {
case *FileType:
typ := typ.(*FileType)
if obj.Name != typ.Name {
return false
}
if obj.GetPath() != typ.Path {
return false
}
if obj.Content != typ.Content {
return false
}
if obj.State != typ.State {
return false
}
default:
return false
}
return true
}

207
main.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -18,7 +18,6 @@
package main
import (
"fmt"
"github.com/codegangsta/cli"
"log"
"os"
@@ -26,12 +25,13 @@ import (
"sync"
"syscall"
"time"
//etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
)
// set at compile time
var (
version string
program string
version string
)
const (
@@ -48,7 +48,6 @@ func waitForSignal(exit chan bool) {
select {
case e := <-signals: // any signal will do
if e == os.Interrupt {
fmt.Println() // put ^C char from terminal on its own line
log.Println("Interrupted by ^C")
} else {
log.Println("Interrupted by signal")
@@ -59,54 +58,149 @@ func waitForSignal(exit chan bool) {
}
func run(c *cli.Context) {
var start int64 = time.Now().UnixNano()
var start = time.Now().UnixNano()
var wg sync.WaitGroup
exit := make(chan bool) // exit signal
log.Printf("This is: %v, version: %v\n", program, version)
exit := make(chan bool) // exit signal
converged := make(chan bool) // converged signal
log.Printf("This is: %v, version: %v", program, version)
log.Printf("Main: Start: %v", start)
G := NewGraph("Graph") // give graph a default name
// exit after `exittime` seconds for no reason at all...
if i := c.Int("exittime"); i > 0 {
// exit after `max-runtime` seconds for no reason at all...
if i := c.Int("max-runtime"); i > 0 {
go func() {
time.Sleep(time.Duration(i) * time.Second)
exit <- true
}()
}
// build the graph from a config file
G := GraphFromConfig(c.String("file"))
log.Printf("Graph: %v\n", G) // show graph
// initial etcd peer endpoint
seed := c.String("seed")
if seed == "" {
// XXX: start up etcd server, others will join me!
seed = "http://127.0.0.1:2379" // thus we use the local server!
}
// then, connect to `seed` as a client
log.Printf("Start: %v\n", start)
// FIXME: validate seed, or wait for it to fail in etcd init?
for x := range G.GetVerticesChan() { // XXX ?
log.Printf("Main->Starting[%v]\n", x.Name)
wg.Add(1)
// must pass in value to avoid races...
// see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/
go func(v *Vertex) {
defer wg.Done()
v.Start()
log.Printf("Main->Finish[%v]\n", v.Name)
}(x)
// generate a startup "poke" so that an initial check happens
go func(v *Vertex) {
v.Events <- fmt.Sprintf("Startup(%v)", v.Name)
}(x)
// etcd
etcdO := &EtcdWObject{
seed: seed,
ctimeout: c.Int("converged-timeout"),
converged: converged,
}
log.Println("Running...")
hostname := c.String("hostname")
if hostname == "" {
hostname, _ = os.Hostname() // etcd watch key // XXX: this is not the correct key name this is the set key name... WOOPS
}
go func() {
startchan := make(chan struct{}) // start signal
go func() { startchan <- struct{}{} }()
file := c.String("file")
configchan := make(chan bool)
if !c.Bool("no-watch") {
configchan = ConfigWatch(file)
}
log.Printf("Etcd: Starting...")
etcdchan := etcdO.EtcdWatch()
first := true // first loop or not
for {
select {
case _ = <-startchan: // kick the loop once at start
// pass
case msg := <-etcdchan:
switch msg {
// some types of messages we ignore...
case etcdFoo, etcdBar:
continue
// while others passthrough and cause a compile!
case etcdStart, etcdEvent:
// pass
default:
log.Fatal("Etcd: Unhandled message: ", msg)
}
case msg := <-configchan:
if c.Bool("no-watch") || !msg {
continue // not ready to read config
}
//case compile_event: XXX
}
config := ParseConfigFromFile(file)
if config == nil {
log.Printf("Config parse failure")
continue
}
// run graph vertex LOCK...
if !first { // XXX: we can flatten this check out I think
log.Printf("State: %v -> %v", G.SetState(graphPausing), G.GetState())
G.Pause() // sync
log.Printf("State: %v -> %v", G.SetState(graphPaused), G.GetState())
}
// build the graph from a config file
// build the graph on events (eg: from etcd)
if !UpdateGraphFromConfig(config, hostname, G, etcdO) {
log.Fatal("Config: We borked the graph.") // XXX
}
log.Printf("Graph: %v", G) // show graph
err := G.ExecGraphviz(c.String("graphviz-filter"), c.String("graphviz"))
if err != nil {
log.Printf("Graphviz: %v", err)
} else {
log.Printf("Graphviz: Successfully generated graph!")
}
G.SetVertex()
G.SetConvergedCallback(c.Int("converged-timeout"), converged)
// G.Start(...) needs to be synchronous or wait,
// because if half of the nodes are started and
// some are not ready yet and the EtcdWatch
// loops, we'll cause G.Pause(...) before we
// even got going, thus causing nil pointer errors
log.Printf("State: %v -> %v", G.SetState(graphStarting), G.GetState())
G.Start(&wg, first) // sync
log.Printf("State: %v -> %v", G.SetState(graphStarted), G.GetState())
first = false
}
}()
if i := c.Int("converged-timeout"); i >= 0 {
go func() {
ConvergedLoop:
for {
<-converged // when anyone says they have converged
if etcdO.GetConvergedState() != etcdConvergedTimeout {
continue
}
for v := range G.GetVerticesChan() {
if v.Type.GetConvergedState() != typeConvergedTimeout {
continue ConvergedLoop
}
}
// if all have converged, exit
log.Printf("Converged for %d seconds, exiting!", i)
exit <- true
for {
<-converged
} // unblock/drain
//return
}
}()
}
log.Println("Main: Running...")
waitForSignal(exit) // pass in exit channel to watch
G.Exit() // tell all the children to exit
if DEBUG {
for i := range G.GetVerticesChan() {
fmt.Printf("Vertex: %v\n", i)
}
fmt.Printf("Graph: %v\n", G)
log.Printf("Graph: %v", G)
}
wg.Wait() // wait for primary go routines to exit
@@ -116,6 +210,13 @@ func run(c *cli.Context) {
}
func main() {
//if DEBUG {
log.SetFlags(log.LstdFlags | log.Lshortfile)
//}
log.SetFlags(log.Flags() - log.Ldate) // remove the date for now
if program == "" || version == "" {
log.Fatal("Program was not compiled correctly. Please see Makefile.")
}
app := cli.NewApp()
app.Name = program
app.Usage = "next generation config management"
@@ -134,8 +235,44 @@ func main() {
Value: "",
Usage: "graph definition to run",
},
cli.BoolFlag{
Name: "no-watch",
Usage: "do not update graph on watched graph definition file changes",
},
cli.StringFlag{
Name: "code, c",
Value: "",
Usage: "code definition to run",
},
cli.StringFlag{
Name: "graphviz, g",
Value: "",
Usage: "output file for graphviz data",
},
cli.StringFlag{
Name: "graphviz-filter, gf",
Value: "dot", // directed graph default
Usage: "graphviz filter to use",
},
// useful for testing multiple instances on same machine
cli.StringFlag{
Name: "hostname",
Value: "",
Usage: "hostname to use",
},
// if empty, it will startup a new server
cli.StringFlag{
Name: "seed, s",
Value: "",
Usage: "default etc peer endpoint",
},
cli.IntFlag{
Name: "exittime",
Name: "converged-timeout, t",
Value: -1,
Usage: "exit after approximately this many seconds in a converged state",
},
cli.IntFlag{
Name: "max-runtime",
Value: 0,
Usage: "exit after a maximum of approximately this many seconds",
},

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify

63
mgmt.spec.in Normal file
View File

@@ -0,0 +1,63 @@
%global project_version __VERSION__
%define debug_package %{nil}
Name: mgmt
Version: __VERSION__
Release: __RELEASE__
Summary: A next generation config management prototype!
License: AGPLv3+
URL: https://github.com/purpleidea/mgmt
Source0: https://dl.fedoraproject.org/pub/alt/purpleidea/mgmt/SOURCES/mgmt-%{project_version}.tar.bz2
# graphviz should really be a "suggests", since technically it's optional
Requires: graphviz
BuildRequires: golang
BuildRequires: golang-googlecode-tools-stringer
BuildRequires: git-core
BuildRequires: mercurial
%description
A next generation config management prototype!
%prep
%setup
%build
# FIXME: in the future, these could be vendor-ed in
mkdir -p vendor/
export GOPATH=`pwd`/vendor/
go get github.com/coreos/etcd/client
go get gopkg.in/yaml.v2
go get gopkg.in/fsnotify.v1
go get github.com/codegangsta/cli
go get github.com/coreos/go-systemd/dbus
go get github.com/coreos/go-systemd/util
make build
%install
rm -rf %{buildroot}
# _datadir is typically /usr/share/
install -d -m 0755 %{buildroot}/%{_datadir}/mgmt/
cp -a AUTHORS COPYING COPYRIGHT DOCUMENTATION.md README.md THANKS examples/ %{buildroot}/%{_datadir}/mgmt/
# install the binary
mkdir -p %{buildroot}/%{_bindir}
install -m 0755 mgmt %{buildroot}/%{_bindir}/mgmt
# profile.d bash completion
mkdir -p %{buildroot}%{_sysconfdir}/profile.d
install misc/mgmt.bashrc -m 0755 %{buildroot}%{_sysconfdir}/profile.d/mgmt.sh
# etc dir
mkdir -p %{buildroot}%{_sysconfdir}/mgmt/
install -m 0644 misc/mgmt.conf.example %{buildroot}%{_sysconfdir}/mgmt/mgmt.conf
%files
%attr(0755, root, root) %{_sysconfdir}/profile.d/mgmt.sh
%{_datadir}/mgmt/*
%{_bindir}/mgmt
%{_sysconfdir}/mgmt/*
# this changelog is auto-generated by git log
%changelog

67
misc.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -18,16 +18,31 @@
package main
import (
"bytes"
"encoding/base64"
"encoding/gob"
"path"
"strings"
"time"
)
// Similar to the GNU dirname command
func Dirname(p string) string {
if p == "/" {
return ""
}
d, _ := path.Split(path.Clean(p))
return d
}
func Basename(p string) string {
_, b := path.Split(path.Clean(p))
if p[len(p)-1:] == "/" { // don't loose the tail slash
b += "/"
}
return b
}
// Split a path into an array of tokens excluding any trailing empty tokens
func PathSplit(p string) []string {
return strings.Split(path.Clean(p), "/")
@@ -52,6 +67,56 @@ func HasPathPrefix(p, prefix string) bool {
return true
}
// Delta of path prefix, tells you how many path tokens different the prefix is
func PathPrefixDelta(p, prefix string) int {
if !HasPathPrefix(p, prefix) {
return -1
}
patharray := PathSplit(p)
prefixarray := PathSplit(prefix)
return len(patharray) - len(prefixarray)
}
func PathIsDir(p string) bool {
return p[len(p)-1:] == "/" // a dir has a trailing slash in this context
}
// encode an object as base 64, serialize and then base64 encode
func ObjToB64(obj interface{}) (string, bool) {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
err := e.Encode(obj)
if err != nil {
//log.Println("Gob failed to Encode: ", err)
return "", false
}
return base64.StdEncoding.EncodeToString(b.Bytes()), true
}
// TODO: is it possible to somehow generically just return the obj?
// decode an object into the waiting obj which you pass a reference to
func B64ToObj(str string, obj interface{}) bool {
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
//log.Println("Base64 failed to Decode: ", err)
return false
}
b := bytes.NewBuffer(bb)
d := gob.NewDecoder(b)
err = d.Decode(obj)
if err != nil {
//log.Println("Gob failed to Decode: ", err)
return false
}
return true
}
// special version of time.After that blocks when given a negative integer
// when used in a case statement, the timer restarts on each select call to it
func TimeAfterOrBlock(t int) <-chan time.Time {
if t < 0 {
return make(chan time.Time) // blocks forever
}
return time.After(time.Duration(t) * time.Second)
}

72
misc/centos-ci.py Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/python
# modified from:
# https://github.com/kbsingh/centos-ci-scripts/blob/master/build_python_script.py
# usage: centos-ci.py giturl [branch [commands]]
import os
import sys
import json
import urllib
import subprocess
# static argv to be used if running script inline
argv = [
#'https://github.com/purpleidea/mgmt', # giturl
#'master',
#'make test',
]
argv.insert(0, '') # add a fake argv[0]
url_base = 'http://admin.ci.centos.org:8080'
apikey = '' # put api key here if running inline
if apikey == '':
apikey = os.environ.get('DUFFY_API_KEY')
if apikey is None or apikey == '':
apikey = open('duffy.key', 'r').read().strip()
ver = '7'
arch = 'x86_64'
count = 1
if len(argv) <= 1: argv = sys.argv # use system argv because ours is empty
if len(argv) <= 1:
print 'Not enough arguments supplied!'
sys.exit(1)
git_url = argv[1]
branch = 'master'
if len(argv) > 2: branch = argv[2]
folder = os.path.basename(git_url) # should be project name
run = 'make vtest' # the omv vtest cmd is a good option to run from this target
if len(argv) > 3: run = ' '.join(argv[3:])
get_nodes_url = "%s/Node/get?key=%s&ver=%s&arch=%s&i_count=%s" % (url_base, apikey, ver, arch, count)
data = json.loads(urllib.urlopen(get_nodes_url).read()) # request host(s)
hosts = data['hosts']
ssid = data['ssid']
done_nodes_url = "%s/Node/done?key=%s&ssid=%s" % (url_base, apikey, ssid)
host = hosts[0]
ssh = "ssh -tt -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o SendEnv=JENKINS_URL root@%s" % host
yum = 'yum -y install git wget tree psmisc'
omv = 'wget https://github.com/purpleidea/oh-my-vagrant/raw/master/extras/install-omv.sh && chmod u+x install-omv.sh && ./install-omv.sh && wget https://github.com/purpleidea/mgmt/raw/master/misc/make-path.sh && chmod u+x make-path.sh && ./make-path.sh'
cmd = "%s '%s && %s'" % (ssh, yum, omv) # setup
print cmd
r = subprocess.call(cmd, shell=True)
if r != 0:
# NOTE: we don't clean up the host here, so that it can be inspected!
print "Error configuring omv on: %s" % host
sys.exit(r)
# the second ssh call will run with the omv /etc/profile.d/ script loaded
git = "git clone --recursive %s %s && cd %s && git checkout %s" % (git_url, folder, folder, branch)
cmd = "%s 'export JENKINS_URL=%s && %s && %s'" % (ssh, os.getenv('JENKINS_URL', ''), git, run) # run
print cmd
r = subprocess.call(cmd, shell=True)
if r != 0:
print "Error running job on: %s" % host
output = urllib.urlopen(done_nodes_url).read() # free host(s)
if output != 'Done':
print "Error freeing host: %s" % host
sys.exit(r)

66
misc/copr-build.py Executable file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/python
# README:
# for initial setup, browse to: https://copr.fedoraproject.org/api/
# and it will have a ~/.config/copr config that you can download.
# happy hacking!
import os
import sys
import copr
import time
COPR = 'mgmt'
if len(sys.argv) != 2:
print("Usage: %s <srpm url>" % sys.argv[0])
sys.exit(1)
url = sys.argv[1]
client = copr.CoprClient.create_from_file_config(os.path.expanduser("~/.config/copr"))
result = client.create_new_build(COPR, [url])
if result.output != 'ok':
print(result.error)
sys.exit(1)
print(result.message)
# modified from: https://python-copr.readthedocs.org/en/latest/Examples.html#work-with-builds
for bw in result.builds_list:
print("Build #{}: {}".format(bw.build_id, bw.handle.get_build_details().status))
# cancel all created build
#for bw in result.builds_list:
# bw.handle.cancel_build()
# get build status for each chroot
#for bw in result.builds_list:
# print("build: {}".format(bw.build_id))
# for ch, status in bw.handle.get_build_details().data["chroots"].items():
# print("\t chroot {}:\t {}".format(ch, status))
# simple build progress:
watched = set(result.builds_list)
done = set()
state = {}
for bw in watched: # store initial states
state[bw.build_id] = bw.handle.get_build_details().status
while watched != done:
for bw in watched:
if bw in done:
continue
status = bw.handle.get_build_details().status
if status != state.get(bw.build_id):
print("Build #{}: {}".format(bw.build_id, status))
state[bw.build_id] = status # update status
if status in ['skipped', 'failed', 'succeeded']:
done.add(bw)
if watched == done: break # avoid long while sleep
else: time.sleep(10)
print 'Done!'

39
misc/make-deps.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# setup a simple go environment
travis=0
if env | grep -q '^TRAVIS=true$'; then
travis=1
fi
if [ $travis -eq 0 ]; then
YUM=`which yum 2>/dev/null`
APT=`which apt-get 2>/dev/null`
if [ -z "$YUM" -a -z "$APT" ]; then
echo "The package managers can't be found."
exit 1
fi
if [ ! -z "$YUM" ]; then
# some go dependencies are stored in mercurial
sudo $YUM install -y golang golang-googlecode-tools-stringer hg
fi
if [ ! -z "$APT" ]; then
sudo $APT install -y golang mercurial
fi
fi
# build etcd
git clone --recursive https://github.com/coreos/etcd/ && cd etcd
git checkout v2.2.4 # TODO: update to newer versions as needed
[ -x build ] && ./build
mkdir -p ~/bin/
cp bin/etcd ~/bin/
cd -
rm -rf etcd # clean up to avoid failing on upstream gofmt errors
go get ./... # get all the go dependencies
[ -e "$GOBIN/mgmt" ] && rm -f "$GOBIN/mgmt" # the `go get` version has no -X
go get golang.org/x/tools/cmd/vet # add in `go vet` for travis
go get golang.org/x/tools/cmd/stringer # for automatic stringer-ing

48
misc/make-path.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/bin/bash
# setup a few environment path values
if ! env | grep -q '^GOPATH='; then
export GOPATH="$HOME/gopath/"
mkdir "$GOPATH"
if ! grep -q '^export GOPATH=' ~/.bashrc; then
echo "export GOPATH=~/gopath/" >> ~/.bashrc
fi
echo "setting go path to: $GOPATH"
fi
echo "gopath is: $GOPATH"
# some versions of golang apparently require this to run go get :(
if ! env | grep -q '^GOBIN='; then
export GOBIN="${GOPATH}bin/"
mkdir "$GOBIN"
if ! grep -q '^export GOBIN=' ~/.bashrc; then
echo 'export GOBIN="${GOPATH}bin/"' >> ~/.bashrc
fi
echo "setting go bin to: $GOBIN"
fi
echo "gobin is: $GOBIN"
# add gobin to $PATH
if ! env | grep '^PATH=' | grep -q "$GOBIN"; then
if ! grep -q '^export PATH="'"${GOBIN}"':${PATH}"' ~/.bashrc; then
echo 'export PATH="'"${GOBIN}"':${PATH}"' >> ~/.bashrc
fi
export PATH="${GOBIN}:${PATH}"
echo "setting path to: $PATH"
fi
echo "path is: $PATH"
# add ~/bin/ to $PATH
if ! env | grep '^PATH=' | grep -q "$HOME/bin"; then
mkdir -p "${HOME}/bin"
if ! grep -q '^export PATH="'"${HOME}/bin"':${PATH}"' ~/.bashrc; then
echo 'export PATH="'"${HOME}/bin"':${PATH}"' >> ~/.bashrc
fi
export PATH="${HOME}/bin:${PATH}"
echo "setting path to: $PATH"
fi
echo "path is: $PATH"

13
misc/mgmt.bashrc Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
_cli_bash_autocomplete_mgmt() {
local cur prev opts base
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion )
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0
}
complete -F _cli_bash_autocomplete_mgmt mgmt

1
misc/mgmt.conf.example Normal file
View File

@@ -0,0 +1 @@
# example mgmt configuration file, currently has not options at the moment!

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -18,6 +18,7 @@
package main
import (
"fmt"
"testing"
)
@@ -31,9 +32,22 @@ func TestMiscT1(t *testing.T) {
t.Errorf("Result is incorrect.")
}
if Dirname("/") != "/" {
if Dirname("/") != "" { // TODO: should this equal "/" or "" ?
t.Errorf("Result is incorrect.")
}
if Basename("/foo/bar/baz") != "baz" {
t.Errorf("Result is incorrect.")
}
if Basename("/foo/bar/baz/") != "baz/" {
t.Errorf("Result is incorrect.")
}
if Basename("/") != "/" { // TODO: should this equal "" or "/" ?
t.Errorf("Result is incorrect.")
}
}
func TestMiscT2(t *testing.T) {
@@ -82,6 +96,41 @@ func TestMiscT3(t *testing.T) {
func TestMiscT4(t *testing.T) {
if PathPrefixDelta("/foo/bar/baz", "/foo/ba") != -1 {
t.Errorf("Result should be -1.")
}
if PathPrefixDelta("/foo/bar/baz", "/foo/bar") != 1 {
t.Errorf("Result should be 1.")
}
if PathPrefixDelta("/foo/bar/baz", "/foo/bar/") != 1 {
t.Errorf("Result should be 1.")
}
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar") != 1 {
t.Errorf("Result should be 1.")
}
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/") != 1 {
t.Errorf("Result should be 1.")
}
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/baz/dude") != -1 {
t.Errorf("Result should be -1.")
}
if PathPrefixDelta("/foo/bar/baz/a/b/c/", "/foo/bar/baz") != 3 {
t.Errorf("Result should be 3.")
}
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/baz") != 0 {
t.Errorf("Result should be 0.")
}
}
func TestMiscT5(t *testing.T) {
if PathIsDir("/foo/bar/baz/") != true {
t.Errorf("Result should be false.")
}
@@ -97,5 +146,55 @@ func TestMiscT4(t *testing.T) {
if PathIsDir("/") != true {
t.Errorf("Result should be true.")
}
}
func TestMiscT6(t *testing.T) {
type foo struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Value int `yaml:"value"`
}
obj := foo{"dude", "sweet", 42}
output, ok := ObjToB64(obj)
if ok != true {
t.Errorf("First result should be true.")
}
var data foo
if B64ToObj(output, &data) != true {
t.Errorf("Second result should be true.")
}
// TODO: there is probably a better way to compare these two...
if fmt.Sprintf("%+v\n", obj) != fmt.Sprintf("%+v\n", data) {
t.Errorf("Strings should match.")
}
}
func TestMiscT7(t *testing.T) {
type Foo struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
Value int `yaml:"value"`
}
type bar struct {
Foo `yaml:",inline"` // anonymous struct must be public!
Comment string `yaml:"comment"`
}
obj := bar{Foo{"dude", "sweet", 42}, "hello world"}
output, ok := ObjToB64(obj)
if ok != true {
t.Errorf("First result should be true.")
}
var data bar
if B64ToObj(output, &data) != true {
t.Errorf("Second result should be true.")
}
// TODO: there is probably a better way to compare these two...
if fmt.Sprintf("%+v\n", obj) != fmt.Sprintf("%+v\n", data) {
t.Errorf("Strings should match.")
}
}

39
omv.yaml Normal file
View File

@@ -0,0 +1,39 @@
---
:domain: example.com
:network: 192.168.123.0/24
:image: fedora-23
:cpus: ''
:memory: ''
:disks: 0
:disksize: 40G
:boxurlprefix: ''
:sync: rsync
:syncdir: mgmt/
:syncsrc: "../"
:folder: ".omv"
:extern: []
:cd: "-"
:puppet: false
:classes: []
:shell:
- cd /vagrant/mgmt/ && make deps
:docker: false
:kubernetes: false
:ansible: []
:playbook: []
:ansible_extras: {}
:cachier: false
:vms: []
:namespace: omv
:count: 1
:username: ''
:password: ''
:poolid: true
:repos: []
:update: false
:reboot: false
:unsafe: false
:nested: false
:tests: []
:comment: ''
:reallyrm: false

512
pgraph.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -19,69 +19,106 @@
package main
import (
"code.google.com/p/go-uuid/uuid"
//"container/list" // doubly linked list
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"strconv"
"sync"
"syscall"
"time"
)
//go:generate stringer -type=graphState -output=graphstate_stringer.go
type graphState int
const (
graphNil graphState = iota
graphStarting
graphStarted
graphPausing
graphPaused
)
// The graph abstract data type (ADT) is defined as follows:
// NOTE: the directed graph arrows point from left to right ( --> )
// NOTE: the arrows point towards their dependencies (eg: arrows mean requires)
// * the directed graph arrows point from left to right ( -> )
// * the arrows point away from their dependencies (eg: arrows mean "before")
// * IOW, you might see package -> file -> service (where package runs first)
// * This is also the direction that the notify should happen in...
type Graph struct {
uuid string
Name string
Adjacency map[*Vertex]map[*Vertex]*Edge
Adjacency map[*Vertex]map[*Vertex]*Edge // *Vertex -> *Vertex (edge)
state graphState
mutex sync.Mutex // used when modifying graph State variable
//Directed bool
startcount int
}
type Vertex struct {
uuid string
graph *Graph // store a pointer to the graph it's on
Name string
Type string
Timestamp int64 // last updated timestamp ?
Events chan string // FIXME: eventually a struct for the event?
Typedata Type
data map[string]string
graph *Graph // store a pointer to the graph it's on
Type // anonymous field
data map[string]string // XXX: currently unused i think, remove?
}
type Edge struct {
uuid string
Name string
}
func NewGraph(name string) *Graph {
return &Graph{
uuid: uuid.New(),
Name: name,
Adjacency: make(map[*Vertex]map[*Vertex]*Edge),
state: graphNil,
}
}
func NewVertex(name, t string) *Vertex {
func NewVertex(t Type) *Vertex {
return &Vertex{
uuid: uuid.New(),
Name: name,
Type: t,
Timestamp: -1,
Events: make(chan string, 1), // XXX: chan size?
data: make(map[string]string),
Type: t,
data: make(map[string]string),
}
}
func NewEdge(name string) *Edge {
return &Edge{
uuid: uuid.New(),
Name: name,
}
}
// Graph() creates a new, empty graph.
// addVertex(vert) adds an instance of Vertex to the graph.
// returns the name of the graph
func (g *Graph) GetName() string {
return g.Name
}
// set name of the graph
func (g *Graph) SetName(name string) {
g.Name = name
}
func (g *Graph) GetState() graphState {
//g.mutex.Lock()
//defer g.mutex.Unlock()
return g.state
}
// set graph state and return previous state
func (g *Graph) SetState(state graphState) graphState {
g.mutex.Lock()
defer g.mutex.Unlock()
prev := g.GetState()
g.state = state
return prev
}
// store a pointer in the type to it's parent vertex
func (g *Graph) SetVertex() {
for v := range g.GetVerticesChan() {
v.Type.SetVertex(v)
}
}
// add a new vertex to the graph
func (g *Graph) AddVertex(v *Vertex) {
if _, exists := g.Adjacency[v]; !exists {
g.Adjacency[v] = make(map[*Vertex]*Edge)
@@ -91,7 +128,14 @@ func (g *Graph) AddVertex(v *Vertex) {
}
}
// addEdge(fromVert, toVert) Adds a new, directed edge to the graph that connects two vertices.
func (g *Graph) DeleteVertex(v *Vertex) {
delete(g.Adjacency, v)
for k := range g.Adjacency {
delete(g.Adjacency[k], v)
}
}
// adds a directed edge to the graph from v1 to v2
func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
// NOTE: this doesn't allow more than one edge between two vertexes...
// TODO: is this a problem?
@@ -100,35 +144,60 @@ func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
g.Adjacency[v1][v2] = e
}
// addEdge(fromVert, toVert, weight) Adds a new, weighted, directed edge to the graph that connects two vertices.
// getVertex(vertKey) finds the vertex in the graph named vertKey.
func (g *Graph) GetVertex(uuid string) chan *Vertex {
// XXX: does it make sense to return a channel here?
// GetVertex finds the vertex in the graph with a particular search name
func (g *Graph) GetVertex(name string) chan *Vertex {
ch := make(chan *Vertex, 1)
go func(uuid string) {
go func(name string) {
for k := range g.Adjacency {
v := *k
if v.uuid == uuid {
if k.GetName() == name {
ch <- k
break
}
}
close(ch)
}(uuid)
}(name)
return ch
}
func (g *Graph) GetVertexMatch(obj Type) *Vertex {
for k := range g.Adjacency {
if k.Compare(obj) { // XXX test
return k
}
}
return nil
}
func (g *Graph) HasVertex(v *Vertex) bool {
if _, exists := g.Adjacency[v]; exists {
return true
}
//for k := range g.Adjacency {
// if k == v {
// return true
// }
//}
return false
}
// number of vertices in the graph
func (g *Graph) NumVertices() int {
return len(g.Adjacency)
}
// number of edges in the graph
func (g *Graph) NumEdges() int {
// XXX: not implemented
return -1
count := 0
for k := range g.Adjacency {
count += len(g.Adjacency[k])
}
return count
}
// get an array (slice) of all vertices in the graph
func (g *Graph) GetVertices() []*Vertex {
vertices := make([]*Vertex, 0)
var vertices []*Vertex
for k := range g.Adjacency {
vertices = append(vertices, k)
}
@@ -138,7 +207,6 @@ func (g *Graph) GetVertices() []*Vertex {
// returns a channel of all vertices in the graph
func (g *Graph) GetVerticesChan() chan *Vertex {
ch := make(chan *Vertex)
// TODO: do you need to pass this through into the go routine?
go func(ch chan *Vertex) {
for k := range g.Adjacency {
ch <- k
@@ -153,7 +221,88 @@ func (g *Graph) String() string {
return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges())
}
//func (s []*Vertex) contains(element *Vertex) bool {
// output the graph in graphviz format
// https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29
func (g *Graph) Graphviz() (out string) {
//digraph g {
// label="hello world";
// node [shape=box];
// A [label="A"];
// B [label="B"];
// C [label="C"];
// D [label="D"];
// E [label="E"];
// A -> B [label=f];
// B -> C [label=g];
// D -> E [label=h];
//}
out += fmt.Sprintf("digraph %v {\n", g.GetName())
out += fmt.Sprintf("\tlabel=\"%v\";\n", g.GetName())
//out += "\tnode [shape=box];\n"
str := ""
for i := range g.Adjacency { // reverse paths
out += fmt.Sprintf("\t%v [label=\"%v[%v]\"];\n", i.GetName(), i.GetType(), i.GetName())
for j := range g.Adjacency[i] {
k := g.Adjacency[i][j]
// use str for clearer output ordering
str += fmt.Sprintf("\t%v -> %v [label=%v];\n", i.GetName(), j.GetName(), k.Name)
}
}
out += str
out += "}\n"
return
}
// write out the graphviz data and run the correct graphviz filter command
func (g *Graph) ExecGraphviz(program, filename string) error {
switch program {
case "dot", "neato", "twopi", "circo", "fdp":
default:
return errors.New("Invalid graphviz program selected!")
}
if filename == "" {
return errors.New("No filename given!")
}
// run as a normal user if possible when run with sudo
uid, err1 := strconv.Atoi(os.Getenv("SUDO_UID"))
gid, err2 := strconv.Atoi(os.Getenv("SUDO_GID"))
err := ioutil.WriteFile(filename, []byte(g.Graphviz()), 0644)
if err != nil {
return errors.New("Error writing to filename!")
}
if err1 == nil && err2 == nil {
if err := os.Chown(filename, uid, gid); err != nil {
return errors.New("Error changing file owner!")
}
}
path, err := exec.LookPath(program)
if err != nil {
return errors.New("Graphviz is missing!")
}
out := fmt.Sprintf("%v.png", filename)
cmd := exec.Command(path, "-Tpng", fmt.Sprintf("-o%v", out), filename)
if err1 == nil && err2 == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
}
}
_, err = cmd.Output()
if err != nil {
return errors.New("Error writing to image!")
}
return nil
}
// google/golang hackers apparently do not think contains should be a built-in!
func Contains(s []*Vertex, element *Vertex) bool {
for _, v := range s {
@@ -164,20 +313,15 @@ func Contains(s []*Vertex, element *Vertex) bool {
return false
}
// return an array (slice) of all vertices that connect to vertex v
func (g *Graph) GraphEdges(vertex *Vertex) []*Vertex {
// return an array (slice) of all directed vertices to vertex v (??? -> v)
// ostimestamp should use this
func (g *Graph) IncomingGraphEdges(v *Vertex) []*Vertex {
// TODO: we might be able to implement this differently by reversing
// the Adjacency graph and then looping through it again...
s := make([]*Vertex, 0) // stack
for w, _ := range g.Adjacency[vertex] { // forward paths
//fmt.Printf("forward: %v -> %v\n", v.Name, w.Name)
s = append(s, w)
}
for k, x := range g.Adjacency { // reverse paths
for w, _ := range x {
if w == vertex {
//fmt.Printf("reverse: %v -> %v\n", v.Name, k.Name)
var s []*Vertex
for k := range g.Adjacency { // reverse paths
for w := range g.Adjacency[k] {
if w == v {
s = append(s, k)
}
}
@@ -185,32 +329,27 @@ func (g *Graph) GraphEdges(vertex *Vertex) []*Vertex {
return s
}
// return an array (slice) of all directed vertices to vertex v
func (g *Graph) DirectedGraphEdges(vertex *Vertex) []*Vertex {
// TODO: we might be able to implement this differently by reversing
// the Adjacency graph and then looping through it again...
s := make([]*Vertex, 0) // stack
for w, _ := range g.Adjacency[vertex] { // forward paths
//fmt.Printf("forward: %v -> %v\n", v.Name, w.Name)
s = append(s, w)
// return an array (slice) of all vertices that vertex v points to (v -> ???)
// poke should use this
func (g *Graph) OutgoingGraphEdges(v *Vertex) []*Vertex {
var s []*Vertex
for k := range g.Adjacency[v] { // forward paths
s = append(s, k)
}
return s
}
// get timestamp of a vertex
func (v *Vertex) GetTimestamp() int64 {
return v.Timestamp
}
// update timestamp of a vertex
func (v *Vertex) UpdateTimestamp() int64 {
v.Timestamp = time.Now().UnixNano() // update
return v.Timestamp
// return an array (slice) of all vertices that connect to vertex v
func (g *Graph) GraphEdges(v *Vertex) []*Vertex {
var s []*Vertex
s = append(s, g.IncomingGraphEdges(v)...)
s = append(s, g.OutgoingGraphEdges(v)...)
return s
}
func (g *Graph) DFS(start *Vertex) []*Vertex {
d := make([]*Vertex, 0) // discovered
s := make([]*Vertex, 0) // stack
var d []*Vertex // discovered
var s []*Vertex // stack
if _, exists := g.Adjacency[start]; !exists {
return nil // TODO: error
}
@@ -238,7 +377,7 @@ func (g *Graph) FilterGraph(name string, vertices []*Vertex) *Graph {
for k1, x := range g.Adjacency {
for k2, e := range x {
//fmt.Printf("Filter: %v -> %v # %v\n", k1.Name, k2.Name, e.Name)
//log.Printf("Filter: %v -> %v # %v", k1.Name, k2.Name, e.Name)
if Contains(vertices, k1) || Contains(vertices, k2) {
newgraph.AddEdge(k1, k2, e)
}
@@ -254,7 +393,7 @@ func (g *Graph) GetDisconnectedGraphs() chan *Graph {
ch := make(chan *Graph)
go func() {
var start *Vertex
d := make([]*Vertex, 0) // discovered
var d []*Vertex // discovered
c := g.NumVertices()
for len(d) < c {
@@ -279,12 +418,91 @@ func (g *Graph) GetDisconnectedGraphs() chan *Graph {
// if we've found all the elements, then we're done
// otherwise loop through to continue...
}
close(ch)
}()
return ch
}
// return the indegree for the graph, IOW the count of vertices that point to me
// NOTE: this returns the values for all vertices in one big lookup table
func (g *Graph) InDegree() map[*Vertex]int {
result := make(map[*Vertex]int)
for k := range g.Adjacency {
result[k] = 0 // initialize
}
for k := range g.Adjacency {
for z := range g.Adjacency[k] {
result[z]++
}
}
return result
}
// return the outdegree for the graph, IOW the count of vertices that point away
// NOTE: this returns the values for all vertices in one big lookup table
func (g *Graph) OutDegree() map[*Vertex]int {
result := make(map[*Vertex]int)
for k := range g.Adjacency {
result[k] = 0 // initialize
for _ = range g.Adjacency[k] {
result[k]++
}
}
return result
}
// returns a topological sort for the graph
// based on descriptions and code from wikipedia and rosetta code
// TODO: add memoization, and cache invalidation to speed this up :)
func (g *Graph) TopologicalSort() (result []*Vertex, ok bool) { // kahn's algorithm
var L []*Vertex // empty list that will contain the sorted elements
var S []*Vertex // set of all nodes with no incoming edges
remaining := make(map[*Vertex]int) // amount of edges remaining
for v, d := range g.InDegree() {
if d == 0 {
// accumulate set of all nodes with no incoming edges
S = append(S, v)
} else {
// initialize remaining edge count from indegree
remaining[v] = d
}
}
for len(S) > 0 {
last := len(S) - 1 // remove a node v from S
v := S[last]
S = S[:last]
L = append(L, v) // add v to tail of L
for n := range g.Adjacency[v] {
// for each node n remaining in the graph, consume from
// remaining, so for remaining[n] > 0
if remaining[n] > 0 {
remaining[n]-- // remove edge from the graph
if remaining[n] == 0 { // if n has no other incoming edges
S = append(S, n) // insert n into S
}
}
}
}
// if graph has edges, eg if any value in rem is > 0
for c, in := range remaining {
if in > 0 {
for n := range g.Adjacency[c] {
if remaining[n] > 0 {
return nil, false // not a dag!
}
}
}
}
return L, true
}
func (v *Vertex) Value(key string) (string, bool) {
if value, exists := v.data[key]; exists {
return value, true
@@ -324,91 +542,93 @@ func HeisenbergCount(ch chan *Vertex) int {
return c
}
func (v *Vertex) Associate(t Type) {
v.Typedata = t
}
// main kick to start the graph
func (g *Graph) Start(wg *sync.WaitGroup, first bool) { // start or continue
t, _ := g.TopologicalSort()
// TODO: only calculate indegree if `first` is true to save resources
indegree := g.InDegree() // compute all of the indegree's
for _, v := range Reverse(t) {
func (v *Vertex) OKTimestamp() bool {
g := v.GetGraph()
for _, n := range g.DirectedGraphEdges(v) {
if v.GetTimestamp() > n.GetTimestamp() {
return false
if !v.Type.IsWatching() { // if Watch() is not running...
wg.Add(1)
// must pass in value to avoid races...
// see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/
go func(vv *Vertex) {
defer wg.Done()
vv.Type.Watch()
log.Printf("%v[%v]: Exited", vv.GetType(), vv.GetName())
}(v)
}
// selective poke: here we reduce the number of initial pokes
// to the minimum required to activate every vertex in the
// graph, either by direct action, or by getting poked by a
// vertex that was previously activated. if we poke each vertex
// that has no incoming edges, then we can be sure to reach the
// whole graph. Please note: this may mask certain optimization
// failures, such as any poke limiting code in Poke() or
// BackPoke(). You might want to disable this selective start
// when experimenting with and testing those elements.
// if we are unpausing (since it's not the first run of this
// function) we need to poke to *unpause* every graph vertex,
// and not just selectively the subset with no indegree.
if (!first) || indegree[v] == 0 {
// ensure state is started before continuing on to next vertex
for !v.Type.SendEvent(eventStart, true, false) {
if DEBUG {
// if SendEvent fails, we aren't up yet
log.Printf("%v[%v]: Retrying SendEvent(Start)", v.GetType(), v.GetName())
// sleep here briefly or otherwise cause
// a different goroutine to be scheduled
time.Sleep(1 * time.Millisecond)
}
}
}
}
return true
}
// poke the XXX children?
func (v *Vertex) Poke() {
g := v.GetGraph()
for _, n := range g.DirectedGraphEdges(v) { // XXX: do we want the reverse order?
// poke!
n.Events <- fmt.Sprintf("poke(%v)", v.Name)
func (g *Graph) Pause() {
t, _ := g.TopologicalSort()
for _, v := range t { // squeeze out the events...
v.Type.SendEvent(eventPause, true, false)
}
}
func (g *Graph) Exit() {
// tell all the vertices to exit...
for v := range g.GetVerticesChan() {
v.Exit()
t, _ := g.TopologicalSort()
for _, v := range t { // squeeze out the events...
// turn off the taps...
// XXX: do this by sending an exit signal, and then returning
// when we hit the 'default' in the select statement!
// XXX: we can do this to quiesce, but it's not necessary now
v.Type.SendEvent(eventExit, true, false)
}
}
func (v *Vertex) Exit() {
v.Events <- "exit"
func (g *Graph) SetConvergedCallback(ctimeout int, converged chan bool) {
for v := range g.GetVerticesChan() {
v.Type.SetConvegedCallback(ctimeout, converged)
}
}
// main loop for each vertex
// warning: this logic might be subtle and tricky.
// be careful as it might not even be correct now!
func (v *Vertex) Start() {
log.Printf("Main->Vertex[%v]->Start()\n", v.Name)
//g := v.GetGraph()
var t = v.Typedata
// this whole wg2 wait group is only necessary if we need to wait for
// the go routine to exit...
var wg2 sync.WaitGroup
wg2.Add(1)
go func(v *Vertex, t Type) {
defer wg2.Done()
//fmt.Printf("About to watch [%v].\n", v.Name)
t.Watch(v)
}(v, t)
var ok bool
//XXX make sure dependencies run and become more current first...
for {
select {
case event := <-v.Events:
log.Printf("Event[%v]: %v\n", v.Name, event)
if event == "exit" {
t.Exit() // type exit
wg2.Wait() // wait for worker to exit
return
}
ok = true
if v.OKTimestamp() {
if !t.StateOK() { // TODO: can we rename this to something better?
// throw an error if apply fails...
// if this fails, don't UpdateTimestamp()
if !t.Apply() { // check for error
ok = false
}
}
if ok {
v.UpdateTimestamp() // this was touched...
v.Poke() // XXX
}
}
// in array function to test *vertices in a slice of *vertices
func HasVertex(v *Vertex, haystack []*Vertex) bool {
for _, r := range haystack {
if v == r {
return true
}
}
return false
}
// reverse a list of vertices
func Reverse(vs []*Vertex) []*Vertex {
//var out []*Vertex // XXX: golint suggests, but it fails testing
out := make([]*Vertex, 0) // empty list
l := len(vs)
for i := range vs {
out = append(out, vs[l-i-1])
}
return out
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -20,6 +20,7 @@
package main
import (
"reflect"
"testing"
)
@@ -31,25 +32,33 @@ func TestPgraphT1(t *testing.T) {
t.Errorf("Should have 0 vertices instead of: %d.", i)
}
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
if i := G.NumEdges(); i != 0 {
t.Errorf("Should have 0 edges instead of: %d.", i)
}
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
e1 := NewEdge("e1")
G.AddEdge(v1, v2, e1)
if i := G.NumVertices(); i != 2 {
t.Errorf("Should have 2 vertices instead of: %d.", i)
}
if i := G.NumEdges(); i != 1 {
t.Errorf("Should have 1 edges instead of: %d.", i)
}
}
func TestPgraphT2(t *testing.T) {
G := NewGraph("g2")
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
v3 := NewVertex("v3", "type")
v4 := NewVertex("v4", "type")
v5 := NewVertex("v5", "type")
v6 := NewVertex("v6", "type")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
@@ -71,12 +80,12 @@ func TestPgraphT2(t *testing.T) {
func TestPgraphT3(t *testing.T) {
G := NewGraph("g3")
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
v3 := NewVertex("v3", "type")
v4 := NewVertex("v4", "type")
v5 := NewVertex("v5", "type")
v6 := NewVertex("v6", "type")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
@@ -95,7 +104,7 @@ func TestPgraphT3(t *testing.T) {
t.Errorf("Should have 3 vertices instead of: %d.", i)
t.Errorf("Found: %v", out1)
for _, v := range out1 {
t.Errorf("Value: %v", v.Name)
t.Errorf("Value: %v", v.GetName())
}
}
@@ -104,7 +113,7 @@ func TestPgraphT3(t *testing.T) {
t.Errorf("Should have 3 vertices instead of: %d.", i)
t.Errorf("Found: %v", out1)
for _, v := range out1 {
t.Errorf("Value: %v", v.Name)
t.Errorf("Value: %v", v.GetName())
}
}
}
@@ -112,9 +121,9 @@ func TestPgraphT3(t *testing.T) {
func TestPgraphT4(t *testing.T) {
G := NewGraph("g4")
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
v3 := NewVertex("v3", "type")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
@@ -127,19 +136,19 @@ func TestPgraphT4(t *testing.T) {
t.Errorf("Should have 3 vertices instead of: %d.", i)
t.Errorf("Found: %v", out)
for _, v := range out {
t.Errorf("Value: %v", v.Name)
t.Errorf("Value: %v", v.GetName())
}
}
}
func TestPgraphT5(t *testing.T) {
G := NewGraph("g5")
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
v3 := NewVertex("v3", "type")
v4 := NewVertex("v4", "type")
v5 := NewVertex("v5", "type")
v6 := NewVertex("v6", "type")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
@@ -159,17 +168,16 @@ func TestPgraphT5(t *testing.T) {
if i := out.NumVertices(); i != 3 {
t.Errorf("Should have 3 vertices instead of: %d.", i)
}
}
func TestPgraphT6(t *testing.T) {
G := NewGraph("g6")
v1 := NewVertex("v1", "type")
v2 := NewVertex("v2", "type")
v3 := NewVertex("v3", "type")
v4 := NewVertex("v4", "type")
v5 := NewVertex("v5", "type")
v6 := NewVertex("v6", "type")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
@@ -197,5 +205,204 @@ func TestPgraphT6(t *testing.T) {
if i := HeisenbergGraphCount(graphs); i != 2 {
t.Errorf("Should have 2 graphs instead of: %d.", i)
}
}
func TestPgraphT7(t *testing.T) {
G := NewGraph("g7")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
G.AddEdge(v1, v2, e1)
G.AddEdge(v2, v3, e2)
G.AddEdge(v3, v1, e3)
if i := G.NumVertices(); i != 3 {
t.Errorf("Should have 3 vertices instead of: %d.", i)
}
G.DeleteVertex(v2)
if i := G.NumVertices(); i != 2 {
t.Errorf("Should have 2 vertices instead of: %d.", i)
}
G.DeleteVertex(v1)
if i := G.NumVertices(); i != 1 {
t.Errorf("Should have 1 vertices instead of: %d.", i)
}
G.DeleteVertex(v3)
if i := G.NumVertices(); i != 0 {
t.Errorf("Should have 0 vertices instead of: %d.", i)
}
G.DeleteVertex(v2) // duplicate deletes don't error...
if i := G.NumVertices(); i != 0 {
t.Errorf("Should have 0 vertices instead of: %d.", i)
}
}
func TestPgraphT8(t *testing.T) {
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
if HasVertex(v1, []*Vertex{v1, v2, v3}) != true {
t.Errorf("Should be true instead of false.")
}
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
if HasVertex(v4, []*Vertex{v5, v6}) != false {
t.Errorf("Should be false instead of true.")
}
v7 := NewVertex(NewNoopType("v7"))
v8 := NewVertex(NewNoopType("v8"))
v9 := NewVertex(NewNoopType("v9"))
if HasVertex(v8, []*Vertex{v7, v8, v9}) != true {
t.Errorf("Should be true instead of false.")
}
v1b := NewVertex(NewNoopType("v1")) // same value, different objects
if HasVertex(v1b, []*Vertex{v1, v2, v3}) != false {
t.Errorf("Should be false instead of true.")
}
}
func TestPgraphT9(t *testing.T) {
G := NewGraph("g9")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
e4 := NewEdge("e4")
e5 := NewEdge("e5")
e6 := NewEdge("e6")
G.AddEdge(v1, v2, e1)
G.AddEdge(v1, v3, e2)
G.AddEdge(v2, v4, e3)
G.AddEdge(v3, v4, e4)
G.AddEdge(v4, v5, e5)
G.AddEdge(v5, v6, e6)
indegree := G.InDegree() // map[*Vertex]int
if i := indegree[v1]; i != 0 {
t.Errorf("Indegree of v1 should be 0 instead of: %d.", i)
}
if i := indegree[v2]; i != 1 {
t.Errorf("Indegree of v2 should be 1 instead of: %d.", i)
}
if i := indegree[v3]; i != 1 {
t.Errorf("Indegree of v3 should be 1 instead of: %d.", i)
}
if i := indegree[v4]; i != 2 {
t.Errorf("Indegree of v4 should be 2 instead of: %d.", i)
}
if i := indegree[v5]; i != 1 {
t.Errorf("Indegree of v5 should be 1 instead of: %d.", i)
}
if i := indegree[v6]; i != 1 {
t.Errorf("Indegree of v6 should be 1 instead of: %d.", i)
}
outdegree := G.OutDegree() // map[*Vertex]int
if i := outdegree[v1]; i != 2 {
t.Errorf("Outdegree of v1 should be 2 instead of: %d.", i)
}
if i := outdegree[v2]; i != 1 {
t.Errorf("Outdegree of v2 should be 1 instead of: %d.", i)
}
if i := outdegree[v3]; i != 1 {
t.Errorf("Outdegree of v3 should be 1 instead of: %d.", i)
}
if i := outdegree[v4]; i != 1 {
t.Errorf("Outdegree of v4 should be 1 instead of: %d.", i)
}
if i := outdegree[v5]; i != 1 {
t.Errorf("Outdegree of v5 should be 1 instead of: %d.", i)
}
if i := outdegree[v6]; i != 0 {
t.Errorf("Outdegree of v6 should be 0 instead of: %d.", i)
}
s, ok := G.TopologicalSort()
// either possibility is a valid toposort
match := reflect.DeepEqual(s, []*Vertex{v1, v2, v3, v4, v5, v6}) || reflect.DeepEqual(s, []*Vertex{v1, v3, v2, v4, v5, v6})
if !ok || !match {
t.Errorf("Topological sort failed, status: %v.", ok)
str := "Found:"
for _, v := range s {
str += " " + v.Type.GetName()
}
t.Errorf(str)
}
}
func TestPgraphT10(t *testing.T) {
G := NewGraph("g10")
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
e1 := NewEdge("e1")
e2 := NewEdge("e2")
e3 := NewEdge("e3")
e4 := NewEdge("e4")
e5 := NewEdge("e5")
e6 := NewEdge("e6")
G.AddEdge(v1, v2, e1)
G.AddEdge(v2, v3, e2)
G.AddEdge(v3, v4, e3)
G.AddEdge(v4, v5, e4)
G.AddEdge(v5, v6, e5)
G.AddEdge(v4, v2, e6) // cycle
if _, ok := G.TopologicalSort(); ok {
t.Errorf("Topological sort passed, but graph is cyclic.")
}
}
func TestPgraphT11(t *testing.T) {
v1 := NewVertex(NewNoopType("v1"))
v2 := NewVertex(NewNoopType("v2"))
v3 := NewVertex(NewNoopType("v3"))
v4 := NewVertex(NewNoopType("v4"))
v5 := NewVertex(NewNoopType("v5"))
v6 := NewVertex(NewNoopType("v6"))
if rev := Reverse([]*Vertex{}); !reflect.DeepEqual(rev, []*Vertex{}) {
t.Errorf("Reverse of vertex slice failed.")
}
if rev := Reverse([]*Vertex{v1}); !reflect.DeepEqual(rev, []*Vertex{v1}) {
t.Errorf("Reverse of vertex slice failed.")
}
if rev := Reverse([]*Vertex{v1, v2, v3, v4, v5, v6}); !reflect.DeepEqual(rev, []*Vertex{v6, v5, v4, v3, v2, v1}) {
t.Errorf("Reverse of vertex slice failed.")
}
if rev := Reverse([]*Vertex{v6, v5, v4, v3, v2, v1}); !reflect.DeepEqual(rev, []*Vertex{v1, v2, v3, v4, v5, v6}) {
t.Errorf("Reverse of vertex slice failed.")
}
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -20,7 +20,6 @@
package main
import (
"code.google.com/p/go-uuid/uuid"
"fmt"
systemd "github.com/coreos/go-systemd/dbus" // change namespace
"github.com/coreos/go-systemd/util"
@@ -29,29 +28,37 @@ import (
)
type ServiceType struct {
uuid string
Type string // always "service"
Name string // name variable
Events chan string // FIXME: eventually a struct for the event?
State string // state: running, stopped
Startup string // enabled, disabled, undefined
BaseType `yaml:",inline"`
State string `yaml:"state"` // state: running, stopped
Startup string `yaml:"startup"` // enabled, disabled, undefined
}
func NewServiceType(name, state, startup string) *ServiceType {
return &ServiceType{
uuid: uuid.New(),
Type: "service",
Name: name,
Events: make(chan string, 1), // XXX: chan size?
BaseType: BaseType{
Name: name,
events: make(chan Event),
vertex: nil,
},
State: state,
Startup: startup,
}
}
// Service watcher
func (obj ServiceType) Watch(v *Vertex) {
// obj.Name: service name
func (obj *ServiceType) GetType() string {
return "Service"
}
// Service watcher
func (obj *ServiceType) Watch() {
if obj.IsWatching() {
return
}
obj.SetWatching(true)
defer obj.SetWatching(false)
// obj.Name: service name
//vertex := obj.GetVertex() // stored with SetVertex
if !util.IsRunningSystemd() {
log.Fatal("Systemd is not running.")
}
@@ -64,7 +71,7 @@ func (obj ServiceType) Watch(v *Vertex) {
bus, err := dbus.SystemBus()
if err != nil {
log.Fatal("Failed to connect to bus: %v\n", err)
log.Fatal("Failed to connect to bus: ", err)
}
// XXX: will this detect new units?
@@ -75,9 +82,11 @@ func (obj ServiceType) Watch(v *Vertex) {
var service = fmt.Sprintf("%v.service", obj.Name) // systemd name
var send = false // send event?
var invalid = false // does the service exist or not?
var previous bool // previous invalid value
set := conn.NewSubscriptionSet() // no error should be returned
var exit = false
var dirty = false
var invalid = false // does the service exist or not?
var previous bool // previous invalid value
set := conn.NewSubscriptionSet() // no error should be returned
subChannel, subErrors := set.Subscribe()
var activeSet = false
@@ -91,40 +100,50 @@ func (obj ServiceType) Watch(v *Vertex) {
// firstly, does service even exist or not?
loadstate, err := conn.GetUnitProperty(service, "LoadState")
if err != nil {
log.Printf("Failed to get property: %v\n", err)
log.Printf("Failed to get property: %v", err)
invalid = true
}
if !invalid {
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
if notFound { // XXX: in the loop we'll handle changes better...
log.Printf("Failed to find service: %v\n", service)
log.Printf("Failed to find service: %v", service)
invalid = true // XXX ?
}
}
if previous != invalid { // if invalid changed, send signal
send = true
dirty = true
}
if invalid {
log.Printf("Waiting for: %v\n", service) // waiting for service to appear...
log.Printf("Waiting for: %v", service) // waiting for service to appear...
if activeSet {
activeSet = false
set.Remove(service) // no return value should ever occur
}
obj.SetState(typeWatching) // reset
select {
case _ = <-buschan: // XXX wait for new units event to unstick
obj.SetConvergedState(typeConvergedNil)
// loop so that we can see the changed invalid signal
log.Printf("Service[%v]->DaemonReload()\n", service)
log.Printf("Service[%v]->DaemonReload()", service)
case exit := <-obj.Events:
if exit == "exit" {
return
} else {
log.Fatal("Unknown event: %v\n", exit)
case event := <-obj.events:
obj.SetConvergedState(typeConvergedNil)
if exit, send = obj.ReadEvent(&event); exit {
return // exit
}
if event.GetActivity() {
dirty = true
}
case _ = <-TimeAfterOrBlock(obj.ctimeout):
obj.SetConvergedState(typeConvergedTimeout)
obj.converged <- true
continue
}
} else {
if !activeSet {
@@ -132,55 +151,62 @@ func (obj ServiceType) Watch(v *Vertex) {
set.Add(service) // no return value should ever occur
}
log.Printf("Watching: %v\n", service) // attempting to watch...
log.Printf("Watching: %v", service) // attempting to watch...
obj.SetState(typeWatching) // reset
select {
case event := <-subChannel:
log.Printf("Service event: %+v\n", event)
log.Printf("Service event: %+v", event)
// NOTE: the value returned is a map for some reason...
if event[service] != nil {
// event[service].ActiveState is not nil
if event[service].ActiveState == "active" {
log.Printf("Service[%v]->Started()\n", service)
log.Printf("Service[%v]->Started()", service)
} else if event[service].ActiveState == "inactive" {
log.Printf("Service[%v]->Stopped!()\n", service)
log.Printf("Service[%v]->Stopped!()", service)
} else {
log.Fatal("Unknown service state: ", event[service].ActiveState)
}
} else {
// service stopped (and ActiveState is nil...)
log.Printf("Service[%v]->Stopped\n", service)
log.Printf("Service[%v]->Stopped", service)
}
send = true
dirty = true
case err := <-subErrors:
obj.SetConvergedState(typeConvergedNil) // XXX ?
log.Println("error:", err)
log.Fatal(err)
v.Events <- fmt.Sprintf("service: %v", "error")
//vertex.events <- fmt.Sprintf("service: %v", "error") // XXX: how should we handle errors?
case exit := <-obj.Events:
if exit == "exit" {
return
} else {
log.Fatal("Unknown event: %v\n", exit)
case event := <-obj.events:
obj.SetConvergedState(typeConvergedNil)
if exit, send = obj.ReadEvent(&event); exit {
return // exit
}
if event.GetActivity() {
dirty = true
}
}
}
if send {
send = false
//log.Println("Sending event!")
v.Events <- fmt.Sprintf("service(%v): %v", obj.Name, "event!") // FIXME: use struct
if dirty {
dirty = false
obj.isStateOK = false // something made state dirty
}
Process(obj) // XXX: rename this function
}
}
}
func (obj ServiceType) Exit() bool {
obj.Events <- "exit"
return true
}
func (obj ServiceType) StateOK() bool {
func (obj *ServiceType) StateOK() bool {
if obj.isStateOK { // cache the state
return true
}
if !util.IsRunningSystemd() {
log.Fatal("Systemd is not running.")
@@ -196,14 +222,14 @@ func (obj ServiceType) StateOK() bool {
loadstate, err := conn.GetUnitProperty(service, "LoadState")
if err != nil {
log.Printf("Failed to get load state: %v\n", err)
log.Printf("Failed to get load state: %v", err)
return false
}
// NOTE: we have to compare variants with other variants, they are really strings...
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
if notFound {
log.Printf("Failed to find service: %v\n", service)
log.Printf("Failed to find service: %v", service)
return false
}
@@ -232,8 +258,8 @@ func (obj ServiceType) StateOK() bool {
return true // all is good, no state change needed
}
func (obj ServiceType) Apply() bool {
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
func (obj *ServiceType) Apply() bool {
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
if !util.IsRunningSystemd() {
log.Fatal("Systemd is not running.")
@@ -256,7 +282,7 @@ func (obj ServiceType) Apply() bool {
err = nil
}
if err != nil {
log.Printf("Unable to change startup status: %v\n", err)
log.Printf("Unable to change startup status: %v", err)
return false
}
@@ -292,3 +318,22 @@ func (obj ServiceType) Apply() bool {
return true
}
func (obj *ServiceType) Compare(typ Type) bool {
switch typ.(type) {
case *ServiceType:
typ := typ.(*ServiceType)
if obj.Name != typ.Name {
return false
}
if obj.State != typ.State {
return false
}
if obj.Startup != typ.Startup {
return false
}
default:
return false
}
return true
}

11
tag.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# TODO: don't run if current HEAD is already tagged (ensure this is idempotent)
# take current HEAD with new version
v=`git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0`
t=`echo "${v%.*}.$((${v##*.}+1))"` # increment version
echo "Version $t is now tagged!"
echo "Pushing $t to origin..."
echo "Press ^C within 3s to abort."
sleep 3s
git tag $t
git push origin $t

23
test.sh
View File

@@ -2,6 +2,8 @@
# test suite...
echo running test.sh
echo "ENV:"
env
# ensure there is no trailing whitespace or other whitespace errors
git diff-tree --check $(git hash-object -t tree /dev/null) HEAD
@@ -9,3 +11,24 @@ git diff-tree --check $(git hash-object -t tree /dev/null) HEAD
# ensure entries to authors file are sorted
start=$(($(grep -n '^[[:space:]]*$' AUTHORS | awk -F ':' '{print $1}' | head -1) + 1))
diff <(tail -n +$start AUTHORS | sort) <(tail -n +$start AUTHORS)
./test/test-gofmt.sh
./test/test-yamlfmt.sh
./test/test-bashfmt.sh
go test
echo running go vet # since it doesn't output an ok message on pass
go vet && echo PASS
# do these longer tests only when running on ci
if env | grep -q -e '^TRAVIS=true$' -e '^JENKINS_URL=' -e '^BUILD_TAG=jenkins'; then
go test -race
./test/test-shell.sh
else
# FIXME: this fails on travis for some reason
./test/test-reproducible.sh
fi
# run omv tests on jenkins physical hosts only
if env | grep -q -e '^JENKINS_URL=' -e '^BUILD_TAG=jenkins'; then
./test/test-omv.sh
fi

52
test/omv/helloworld.yaml Normal file
View File

@@ -0,0 +1,52 @@
---
:domain: example.com
:network: 192.168.123.0/24
:image: fedora-23
:cpus: ''
:memory: ''
:disks: 0
:disksize: 40G
:boxurlprefix: ''
:sync: rsync
:syncdir: ''
:syncsrc: ''
:folder: ".omv"
:extern:
- type: git
repository: https://github.com/purpleidea/mgmt
directory: mgmt
:cd: ''
:puppet: false
:classes: []
:shell:
- mkdir /tmp/mgmt/
:docker: false
:kubernetes: false
:ansible: []
:playbook: []
:ansible_extras: {}
:cachier: false
:vms:
- :name: etcd
:shell:
- iptables -F
- cd /vagrant/mgmt/ && make path
- cd /vagrant/mgmt/ && make deps && make build && cp mgmt ~/bin/
- etcd -bind-addr "`hostname --ip-address`:2379" &
- cd && mgmt --help
:namespace: omv
:count: 0
:username: ''
:password: ''
:poolid: true
:repos: []
:update: false
:reboot: false
:unsafe: false
:nested: false
:tests:
- omv up etcd
- vssh root@etcd -c pidof etcd
- omv destroy
:comment: simple hello world test case for mgmt
:reallyrm: false

1
test/shell/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
mgmt

12
test/shell/etcd.sh Normal file
View File

@@ -0,0 +1,12 @@
# NOTE: boiler plate to run etcd; source with: . etcd.sh; should NOT be +x
cleanup ()
{
killall etcd || killall -9 etcd || true # kill etcd
rm -rf /tmp/etcd/
}
trap cleanup INT QUIT TERM EXIT ERR
mkdir -p /tmp/etcd/
cd /tmp/etcd/ >/dev/null # shush the cd operation
etcd & # start etcd as job # 1
sleep 1s # let etcd startup
cd - >/dev/null

14
test/shell/t1.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
# NOTES:
# * this is a simple shell based `mgmt` test case
# * it is recommended that you run mgmt wrapped in the timeout command
# * it is recommended that you run mgmt with --no-watch
# * it is recommended that you run mgmt --converged-timeout=<seconds>
# * you can run mgmt with --max-runtime=<seconds> in special scenarios
# * you can get a separate etcd going by sourcing etcd.sh: . etcd.sh
set -o errexit
set -o nounset
set -o pipefail
timeout --kill-after=3s 1s ./mgmt --help # hello world!

14
test/shell/t2.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/bash
. etcd.sh # start etcd as job # 1
# run till completion
timeout --kill-after=15s 10s ./mgmt run --file t2.yaml --converged-timeout=5 --no-watch &
#jobs # etcd is 1
wait -n 2 # wait for mgmt to exit
test -e /tmp/mgmt/f1
test -e /tmp/mgmt/f2
test -e /tmp/mgmt/f3
test ! -e /tmp/mgmt/f4

41
test/shell/t2.yaml Normal file
View File

@@ -0,0 +1,41 @@
---
graph: mygraph
types:
noop:
- name: noop1
file:
- name: file1
path: "/tmp/mgmt/f1"
content: |
i am f1
state: exists
- name: file2
path: "/tmp/mgmt/f2"
content: |
i am f2
state: exists
- name: file3
path: "/tmp/mgmt/f3"
content: |
i am f3
state: exists
- name: file4
path: "/tmp/mgmt/f4"
content: |
i am f4 and i should not be here
state: absent
edges:
- name: e1
from:
type: file
name: file1
to:
type: file
name: file2
- name: e2
from:
type: file
name: file2
to:
type: file
name: file3

28
test/shell/t3-a.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1a
path: "/tmp/mgmt/mgmtA/f1a"
content: |
i am f1
state: exists
- name: file2a
path: "/tmp/mgmt/mgmtA/f2a"
content: |
i am f2
state: exists
- name: "@@file3a"
path: "/tmp/mgmt/mgmtA/f3a"
content: |
i am f3, exported from host A
state: exists
- name: "@@file4a"
path: "/tmp/mgmt/mgmtA/f4a"
content: |
i am f4, exported from host A
state: exists
collect:
- type: file
pattern: "/tmp/mgmt/mgmtA/"
edges: []

28
test/shell/t3-b.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1b
path: "/tmp/mgmt/mgmtB/f1b"
content: |
i am f1
state: exists
- name: file2b
path: "/tmp/mgmt/mgmtB/f2b"
content: |
i am f2
state: exists
- name: "@@file3b"
path: "/tmp/mgmt/mgmtB/f3b"
content: |
i am f3, exported from host B
state: exists
- name: "@@file4b"
path: "/tmp/mgmt/mgmtB/f4b"
content: |
i am f4, exported from host B
state: exists
collect:
- type: file
pattern: "/tmp/mgmt/mgmtB/"
edges: []

28
test/shell/t3-c.yaml Normal file
View File

@@ -0,0 +1,28 @@
---
graph: mygraph
types:
file:
- name: file1c
path: "/tmp/mgmt/mgmtC/f1c"
content: |
i am f1
state: exists
- name: file2c
path: "/tmp/mgmt/mgmtC/f2c"
content: |
i am f2
state: exists
- name: "@@file3c"
path: "/tmp/mgmt/mgmtC/f3c"
content: |
i am f3, exported from host C
state: exists
- name: "@@file4c"
path: "/tmp/mgmt/mgmtC/f4c"
content: |
i am f4, exported from host C
state: exists
collect:
- type: file
pattern: "/tmp/mgmt/mgmtC/"
edges: []

67
test/shell/t3.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
. etcd.sh # start etcd as job # 1
# setup
mkdir -p "${MGMT_TMPDIR}"mgmt{A..C}
# run till completion
timeout --kill-after=15s 10s ./mgmt run --file t3-a.yaml --converged-timeout=5 --no-watch &
timeout --kill-after=15s 10s ./mgmt run --file t3-b.yaml --converged-timeout=5 --no-watch &
timeout --kill-after=15s 10s ./mgmt run --file t3-c.yaml --converged-timeout=5 --no-watch &
. wait.sh # wait for everything except etcd
# A: collected
test -e "${MGMT_TMPDIR}"mgmtA/f3b
test -e "${MGMT_TMPDIR}"mgmtA/f3c
test -e "${MGMT_TMPDIR}"mgmtA/f4b
test -e "${MGMT_TMPDIR}"mgmtA/f4c
# A: local
test -e "${MGMT_TMPDIR}"mgmtA/f1a
test -e "${MGMT_TMPDIR}"mgmtA/f2a
test -e "${MGMT_TMPDIR}"mgmtA/f3a
test -e "${MGMT_TMPDIR}"mgmtA/f4a
# A: nope!
test ! -e "${MGMT_TMPDIR}"mgmtA/f1b
test ! -e "${MGMT_TMPDIR}"mgmtA/f2b
test ! -e "${MGMT_TMPDIR}"mgmtA/f1c
test ! -e "${MGMT_TMPDIR}"mgmtA/f2c
# B: collected
test -e "${MGMT_TMPDIR}"mgmtB/f3a
test -e "${MGMT_TMPDIR}"mgmtB/f3c
test -e "${MGMT_TMPDIR}"mgmtB/f4a
test -e "${MGMT_TMPDIR}"mgmtB/f4c
# B: local
test -e "${MGMT_TMPDIR}"mgmtB/f1b
test -e "${MGMT_TMPDIR}"mgmtB/f2b
test -e "${MGMT_TMPDIR}"mgmtB/f3b
test -e "${MGMT_TMPDIR}"mgmtB/f4b
# B: nope!
test ! -e "${MGMT_TMPDIR}"mgmtB/f1a
test ! -e "${MGMT_TMPDIR}"mgmtB/f2a
test ! -e "${MGMT_TMPDIR}"mgmtB/f1c
test ! -e "${MGMT_TMPDIR}"mgmtB/f2c
# C: collected
test -e "${MGMT_TMPDIR}"mgmtC/f3a
test -e "${MGMT_TMPDIR}"mgmtC/f3b
test -e "${MGMT_TMPDIR}"mgmtC/f4a
test -e "${MGMT_TMPDIR}"mgmtC/f4b
# C: local
test -e "${MGMT_TMPDIR}"mgmtC/f1c
test -e "${MGMT_TMPDIR}"mgmtC/f2c
test -e "${MGMT_TMPDIR}"mgmtC/f3c
test -e "${MGMT_TMPDIR}"mgmtC/f4c
# C: nope!
test ! -e "${MGMT_TMPDIR}"mgmtC/f1a
test ! -e "${MGMT_TMPDIR}"mgmtC/f2a
test ! -e "${MGMT_TMPDIR}"mgmtC/f1b
test ! -e "${MGMT_TMPDIR}"mgmtC/f2b

10
test/shell/t4.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
. etcd.sh # start etcd as job # 1
# should take slightly more than 25s, but fail if we take 35s)
timeout --kill-after=35s 30s ./mgmt run --file t4.yaml --converged-timeout=5 --no-watch &
#jobs # etcd is 1
#wait -n 2 # wait for mgmt to exit
. wait.sh # wait for everything except etcd

77
test/shell/t4.yaml Normal file
View File

@@ -0,0 +1,77 @@
---
graph: mygraph
comment: simple exec fan in example to demonstrate optimization)
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec3
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec4
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec5
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec5
- name: e2
from:
type: exec
name: exec2
to:
type: exec
name: exec5
- name: e3
from:
type: exec
name: exec3
to:
type: exec
name: exec5

10
test/shell/t5.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
. etcd.sh # start etcd as job # 1
# should take slightly more than 35s, but fail if we take 45s)
timeout --kill-after=45s 40s ./mgmt run --file t5.yaml --converged-timeout=5 --no-watch &
#jobs # etcd is 1
#wait -n 2 # wait for mgmt to exit
. wait.sh # wait for everything except etcd

128
test/shell/t5.yaml Normal file
View File

@@ -0,0 +1,128 @@
---
graph: mygraph
comment: simple exec fan in to fan out example to demonstrate optimization
types:
exec:
- name: exec1
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec2
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec3
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec4
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec5
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec6
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec7
cmd: sleep 10s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
- name: exec8
cmd: sleep 15s
shell: ''
timeout: 0
watchcmd: ''
watchshell: ''
ifcmd: ''
ifshell: ''
pollint: 0
state: present
edges:
- name: e1
from:
type: exec
name: exec1
to:
type: exec
name: exec4
- name: e2
from:
type: exec
name: exec2
to:
type: exec
name: exec4
- name: e3
from:
type: exec
name: exec3
to:
type: exec
name: exec4
- name: e4
from:
type: exec
name: exec4
to:
type: exec
name: exec5
- name: e5
from:
type: exec
name: exec4
to:
type: exec
name: exec6
- name: e6
from:
type: exec
name: exec4
to:
type: exec
name: exec7

6
test/shell/wait.sh Normal file
View File

@@ -0,0 +1,6 @@
# NOTE: boiler plate to wait on mgmt; source with: . wait.sh; should NOT be +x
for j in `jobs -p`
do
[ $j -eq `pidof etcd` ] && continue # don't wait for etcd
wait $j # wait for mgmt job $j
done

31
test/test-bashfmt.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# check for any bash files that aren't properly formatted
# TODO: this is hardly exhaustive
set -o errexit
set -o nounset
set -o pipefail
ROOT=$(dirname "${BASH_SOURCE}")/..
cd "${ROOT}"
find_files() {
git ls-files | grep -e '\.sh$' -e '\.bash$'
}
bad_files=$(
for i in $(find_files); do
# search for more than one leading space, to ensure we use tabs
if grep -q '^ ' "$i"; then
echo "$i"
fi
done
)
if [[ -n "${bad_files}" ]]; then
echo 'FAIL'
echo 'The following bash files are not properly formatted:'
echo "${bad_files}"
exit 1
fi

View File

@@ -9,7 +9,7 @@ ROOT=$(dirname "${BASH_SOURCE}")/..
GO_VERSION=($(go version))
if [[ -z $(echo "${GO_VERSION[2]}" | grep -E 'go1.2|go1.3|go1.4') ]]; then
if [[ -z $(echo "${GO_VERSION[2]}" | grep -E 'go1.2|go1.3|go1.4|go1.5') ]]; then
echo "Unknown go version '${GO_VERSION}', skipping gofmt."
exit 0
fi
@@ -17,19 +17,14 @@ fi
cd "${ROOT}"
find_files() {
find . -not \( \
\( \
-wholename './old' \
-o -wholename './tmp' \
\) -prune \
\) -name '*.go'
git ls-files | grep '\.go$'
}
GOFMT="gofmt" # we prefer to not use the -s flag, which is pretty annoying...
bad_files=$(find_files | xargs $GOFMT -l)
if [[ -n "${bad_files}" ]]; then
echo 'FAIL'
echo 'The following files are not properly formatted:'
echo 'The following golang files are not properly formatted:'
echo "${bad_files}"
exit 1
fi

11
test/test-omv.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash -ie
# simple test harness for testing mgmt via omv
CWD=`pwd`
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" # dir!
cd "$DIR" >/dev/null # work from test directory
# vtest+ tests
vtest+ omv/helloworld.yaml
# return to original dir
cd "$CWD" >/dev/null

39
test/test-reproducible.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/bin/bash
# simple test for reproducibility, probably needs major improvements
set -o errexit
set -o pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir!
cd "$DIR" >/dev/null # work from main mgmt directory
make build
T=`mktemp --tmpdir -d tmp.XXX`
cp -a ./mgmt "$T"/mgmt.1
make clean
make build
cp -a ./mgmt "$T"/mgmt.2
# size comparison test
[ `stat -c '%s' "$T"/mgmt.1` -eq `stat -c '%s' "$T"/mgmt.2` ] || failures="Size of binary was not reproducible"
# sha1sum test
sha1sum "$T"/mgmt.1 > "$T"/mgmt.SHA1SUMS.1
sha1sum "$T"/mgmt.2 > "$T"/mgmt.SHA1SUMS.2
cat "$T"/mgmt.SHA1SUMS.1 | sed 's/mgmt\.1/mgmt\.X/' > "$T"/mgmt.SHA1SUMS.1X
cat "$T"/mgmt.SHA1SUMS.2 | sed 's/mgmt\.2/mgmt\.X/' > "$T"/mgmt.SHA1SUMS.2X
diff -q "$T"/mgmt.SHA1SUMS.1X "$T"/mgmt.SHA1SUMS.2X || failures=$( [ -n "${failures}" ] && echo "$failures" ; echo "SHA1SUM of binary was not reproducible" )
# clean up
if [ "$T" != '' ]; then
rm -rf "$T"
fi
make clean
# display errors
if [[ -n "${failures}" ]]; then
echo 'FAIL'
echo 'The following tests failed:'
echo "${failures}"
exit 1
fi
echo PASS

73
test/test-shell.sh Executable file
View File

@@ -0,0 +1,73 @@
#!/bin/bash
# simple test harness for testing mgmt
# NOTE: this will rm -rf /tmp/mgmt/
set -o errexit
set -o pipefail
LINE=$(printf '=%.0s' `seq -s ' ' $(tput cols)`) # a terminal width string
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir!
cd "$DIR" >/dev/null # work from main mgmt directory
make build
MGMT="$DIR/test/shell/mgmt"
cp -a "$DIR/mgmt" "$MGMT" # put a copy there
failures=""
count=0
# loop through tests
for i in $DIR/test/shell/*.sh; do
[ -x "$i" ] || continue # file must be executable
ii=`basename "$i"` # short name
# if ARGV has test names, only execute those!
if [ "$1" != '' ]; then
[ "$ii" != "$1" ] && continue
fi
cd $DIR/test/shell/ >/dev/null # shush the cd operation
mkdir -p '/tmp/mgmt/' # directory for mgmt to put files in
#echo "Running: $ii"
export MGMT_TMPDIR='/tmp/mgmt/' # we can add to env like this
count=`expr $count + 1`
set +o errexit # don't kill script on test failure
out=$($i 2>&1) # run and capture stdout & stderr
e=$? # save exit code
set -o errexit # re-enable killing on script failure
cd - >/dev/null
rm -rf '/tmp/mgmt/' # clean up after test
if [ $e -ne 0 ]; then
# store failures...
failures=$(
# prepend previous failures if any
[ -n "${failures}" ] && echo "$failures" && echo "$LINE"
echo "Script: $ii"
# if we see 124, it might be the exit value of timeout!
[ $e -eq 124 ] && echo "Exited: $e (timeout?)" || echo "Exited: $e"
if [ "$out" = "" ]; then
echo "Output: (empty!)"
else
echo "Output:"
echo "$out"
fi
)
else
echo -e "ok\t$ii" # pass
fi
done
# clean up
rm -f "$MGMT"
make clean
if [ "$count" = '0' ]; then
echo 'FAIL'
echo 'No tests were run!'
exit 1
fi
# display errors
if [[ -n "${failures}" ]]; then
echo 'FAIL'
echo 'The following tests failed:'
echo "${failures}"
exit 1
fi
echo PASS

View File

@@ -5,21 +5,33 @@ set -o errexit
set -o nounset
set -o pipefail
if env | grep -q -e '^TRAVIS=true$' -e '^JENKINS_URL=' -e '^BUILD_TAG=jenkins'; then
echo "Travis and Jenkins give wonky results here, skipping test!"
exit 0
fi
ROOT=$(dirname "${BASH_SOURCE}")/..
RUBY=`which ruby 2>/dev/null`
if [ -z $RUBY ]; then
echo "The 'ruby' utility can't be found."
exit 1
fi
$RUBY -e "require 'yaml'" 2>/dev/null || (
echo "The ruby 'yaml' library can't be found."
exit 1
)
cd "${ROOT}"
find_files() {
find . -not \( \
\( \
-wholename './old' \
-o -wholename './tmp' \
\) -prune \
\) -name '*.yaml'
git ls-files | grep '\.yaml$'
}
bad_files=$(
for i in $(find_files); do
if ! diff -q <( ruby -e "require 'yaml'; puts YAML.load_file('$i').to_yaml" 2>/dev/null ) <( cat "$i" ) &>/dev/null; then
if ! diff -q <( ruby -e "require 'yaml'; puts YAML.load_file('$i').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr" 2>/dev/null ) <( cat "$i" ) &>/dev/null; then
echo "$i"
fi
done
@@ -27,7 +39,7 @@ bad_files=$(
if [[ -n "${bad_files}" ]]; then
echo 'FAIL'
echo 'The following files are not properly formatted:'
echo 'The following yaml files are not properly formatted:'
echo "${bad_files}"
exit 1
fi

384
types.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2015+ James Shubin and the project contributors
// Copyright (C) 2013-2016+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
@@ -18,56 +18,390 @@
package main
import (
"code.google.com/p/go-uuid/uuid"
"fmt"
"log"
"time"
)
//go:generate stringer -type=typeState -output=typestate_stringer.go
type typeState int
const (
typeNil typeState = iota
typeWatching
typeEvent // an event has happened, but we haven't poked yet
typeApplying
typePoking
)
//go:generate stringer -type=typeConvergedState -output=typeconvergedstate_stringer.go
type typeConvergedState int
const (
typeConvergedNil typeConvergedState = iota
//typeConverged
typeConvergedTimeout
)
type Type interface {
//Name() string
Watch(*Vertex)
Init()
GetName() string // can't be named "Name()" because of struct field
GetType() string
Watch()
StateOK() bool // TODO: can we rename this to something better?
Apply() bool
Exit() bool
SetVertex(*Vertex)
SetConvegedCallback(ctimeout int, converged chan bool)
Compare(Type) bool
SendEvent(eventName, bool, bool) bool
IsWatching() bool
SetWatching(bool)
GetConvergedState() typeConvergedState
SetConvergedState(typeConvergedState)
GetState() typeState
SetState(typeState)
GetTimestamp() int64
UpdateTimestamp() int64
OKTimestamp() bool
Poke(bool)
BackPoke()
}
type BaseType struct {
Name string `yaml:"name"`
timestamp int64 // last updated timestamp ?
events chan Event
vertex *Vertex
state typeState
convergedState typeConvergedState
watching bool // is Watch() loop running ?
ctimeout int // converged timeout
converged chan bool
isStateOK bool // whether the state is okay based on events or not
}
type NoopType struct {
uuid string
Type string // always "noop"
Name string // name variable
Events chan string // FIXME: eventually a struct for the event?
BaseType `yaml:",inline"`
Comment string `yaml:"comment"` // extra field for example purposes
}
func NewNoopType(name string) *NoopType {
// FIXME: we could get rid of this New constructor and use raw object creation with a required Init()
return &NoopType{
uuid: uuid.New(),
Type: "noop",
Name: name,
Events: make(chan string, 1), // XXX: chan size?
BaseType: BaseType{
Name: name,
events: make(chan Event), // unbuffered chan size to avoid stale events
vertex: nil,
},
Comment: "",
}
}
func (obj NoopType) Watch(v *Vertex) {
select {
case exit := <-obj.Events:
if exit == "exit" {
return
// initialize structures like channels if created without New constructor
func (obj *BaseType) Init() {
obj.events = make(chan Event)
}
// this method gets used by all the types, if we have one of (obj NoopType) it would get overridden in that case!
func (obj *BaseType) GetName() string {
return obj.Name
}
func (obj *BaseType) GetType() string {
return "Base"
}
func (obj *BaseType) GetVertex() *Vertex {
return obj.vertex
}
func (obj *BaseType) SetVertex(v *Vertex) {
obj.vertex = v
}
func (obj *BaseType) SetConvegedCallback(ctimeout int, converged chan bool) {
obj.ctimeout = ctimeout
obj.converged = converged
}
// is the Watch() function running?
func (obj *BaseType) IsWatching() bool {
return obj.watching
}
// store status of if the Watch() function is running
func (obj *BaseType) SetWatching(b bool) {
obj.watching = b
}
func (obj *BaseType) GetConvergedState() typeConvergedState {
return obj.convergedState
}
func (obj *BaseType) SetConvergedState(state typeConvergedState) {
obj.convergedState = state
}
func (obj *BaseType) GetState() typeState {
return obj.state
}
func (obj *BaseType) SetState(state typeState) {
if DEBUG {
log.Printf("%v[%v]: State: %v -> %v", obj.GetType(), obj.GetName(), obj.GetState(), state)
}
obj.state = state
}
// GetTimestamp returns the timestamp of a vertex
func (obj *BaseType) GetTimestamp() int64 {
return obj.timestamp
}
// UpdateTimestamp updates the timestamp on a vertex and returns the new value
func (obj *BaseType) UpdateTimestamp() int64 {
obj.timestamp = time.Now().UnixNano() // update
return obj.timestamp
}
// can this element run right now?
func (obj *BaseType) OKTimestamp() bool {
v := obj.GetVertex()
g := v.GetGraph()
// these are all the vertices pointing TO v, eg: ??? -> v
for _, n := range g.IncomingGraphEdges(v) {
// if the vertex has a greater timestamp than any pre-req (n)
// then we can't run right now...
// if they're equal (eg: on init of 0) then we also can't run
// b/c we should let our pre-req's go first...
x, y := obj.GetTimestamp(), n.Type.GetTimestamp()
if DEBUG {
log.Printf("%v[%v]: OKTimestamp: (%v) >= %v[%v](%v): !%v", obj.GetType(), obj.GetName(), x, n.GetType(), n.GetName(), y, x >= y)
}
if x >= y {
return false
}
}
return true
}
// notify nodes after me in the dependency graph that they need refreshing...
// NOTE: this assumes that this can never fail or need to be rescheduled
func (obj *BaseType) Poke(activity bool) {
v := obj.GetVertex()
g := v.GetGraph()
// these are all the vertices pointing AWAY FROM v, eg: v -> ???
for _, n := range g.OutgoingGraphEdges(v) {
// XXX: if we're in state event and haven't been cancelled by
// apply, then we can cancel a poke to a child, right? XXX
// XXX: if n.Type.GetState() != typeEvent { // is this correct?
if true { // XXX
if DEBUG {
log.Printf("%v[%v]: Poke: %v[%v]", v.GetType(), v.GetName(), n.GetType(), n.GetName())
}
n.SendEvent(eventPoke, false, activity) // XXX: can this be switched to sync?
} else {
log.Fatal("Unknown event: %v\n", exit)
if DEBUG {
log.Printf("%v[%v]: Poke: %v[%v]: Skipped!", v.GetType(), v.GetName(), n.GetType(), n.GetName())
}
}
}
}
func (obj NoopType) Exit() bool {
obj.Events <- "exit"
// poke the pre-requisites that are stale and need to run before I can run...
func (obj *BaseType) BackPoke() {
v := obj.GetVertex()
g := v.GetGraph()
// these are all the vertices pointing TO v, eg: ??? -> v
for _, n := range g.IncomingGraphEdges(v) {
x, y, s := obj.GetTimestamp(), n.Type.GetTimestamp(), n.Type.GetState()
// if the parent timestamp needs poking AND it's not in state
// typeEvent, then poke it. If the parent is in typeEvent it
// means that an event is pending, so we'll be expecting a poke
// back soon, so we can safely discard the extra parent poke...
// TODO: implement a stateLT (less than) to tell if something
// happens earlier in the state cycle and that doesn't wrap nil
if x >= y && (s != typeEvent && s != typeApplying) {
if DEBUG {
log.Printf("%v[%v]: BackPoke: %v[%v]", v.GetType(), v.GetName(), n.GetType(), n.GetName())
}
n.SendEvent(eventBackPoke, false, false) // XXX: can this be switched to sync?
} else {
if DEBUG {
log.Printf("%v[%v]: BackPoke: %v[%v]: Skipped!", v.GetType(), v.GetName(), n.GetType(), n.GetName())
}
}
}
}
// push an event into the message queue for a particular type vertex
func (obj *BaseType) SendEvent(event eventName, sync bool, activity bool) bool {
// TODO: isn't this race-y ?
if !obj.IsWatching() { // element has already exited
return false // if we don't return, we'll block on the send
}
if !sync {
obj.events <- Event{event, nil, "", activity}
return true
}
resp := make(chan bool)
obj.events <- Event{event, resp, "", activity}
for {
value := <-resp
// wait until true value
if value {
return true
}
}
}
// process events when a select gets one, this handles the pause code too!
// the return values specify if we should exit and poke respectively
func (obj *BaseType) ReadEvent(event *Event) (exit, poke bool) {
event.ACK()
switch event.Name {
case eventStart:
return false, true
case eventPoke:
return false, true
case eventBackPoke:
return false, true // forward poking in response to a back poke!
case eventExit:
return true, false
case eventPause:
// wait for next event to continue
select {
case e := <-obj.events:
e.ACK()
if e.Name == eventExit {
return true, false
} else if e.Name == eventStart { // eventContinue
return false, false // don't poke on unpause!
} else {
// if we get a poke event here, it's a bug!
log.Fatalf("%v[%v]: Unknown event: %v, while paused!", obj.GetType(), obj.GetName(), e)
}
}
default:
log.Fatal("Unknown event: ", event)
}
return true, false // required to keep the stupid go compiler happy
}
// useful for using as: return CleanState() in the StateOK functions when there
// are multiple `true` return exits
func (obj *BaseType) CleanState() bool {
obj.isStateOK = true
return true
}
func (obj NoopType) StateOK() bool {
// XXX: rename this function
func Process(obj Type) {
if DEBUG {
log.Printf("%v[%v]: Process()", obj.GetType(), obj.GetName())
}
obj.SetState(typeEvent)
var ok = true
var apply = false // did we run an apply?
// is it okay to run dependency wise right now?
// if not, that's okay because when the dependency runs, it will poke
// us back and we will run if needed then!
if obj.OKTimestamp() {
if DEBUG {
log.Printf("%v[%v]: OKTimestamp(%v)", obj.GetType(), obj.GetName(), obj.GetTimestamp())
}
if !obj.StateOK() { // TODO: can we rename this to something better?
if DEBUG {
log.Printf("%v[%v]: !StateOK()", obj.GetType(), obj.GetName())
}
// throw an error if apply fails...
// if this fails, don't UpdateTimestamp()
obj.SetState(typeApplying)
if !obj.Apply() { // check for error
ok = false
} else {
apply = true
}
}
if ok {
// update this timestamp *before* we poke or the poked
// nodes might fail due to having a too old timestamp!
obj.UpdateTimestamp() // this was touched...
obj.SetState(typePoking) // can't cancel parent poke
obj.Poke(apply)
}
// poke at our pre-req's instead since they need to refresh/run...
} else {
// only poke at the pre-req's that need to run
go obj.BackPoke()
}
}
func (obj *NoopType) GetType() string {
return "Noop"
}
func (obj *NoopType) Watch() {
if obj.IsWatching() {
return
}
obj.SetWatching(true)
defer obj.SetWatching(false)
//vertex := obj.vertex // stored with SetVertex
var send = false // send event?
var exit = false
for {
obj.SetState(typeWatching) // reset
select {
case event := <-obj.events:
obj.SetConvergedState(typeConvergedNil)
// we avoid sending events on unpause
if exit, send = obj.ReadEvent(&event); exit {
return // exit
}
case _ = <-TimeAfterOrBlock(obj.ctimeout):
obj.SetConvergedState(typeConvergedTimeout)
obj.converged <- true
continue
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
// only do this on certain types of events
//obj.isStateOK = false // something made state dirty
Process(obj) // XXX: rename this function
}
}
}
func (obj *NoopType) StateOK() bool {
return true // never needs updating
}
func (obj NoopType) Apply() bool {
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
func (obj *NoopType) Apply() bool {
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
return true
}
func (obj *NoopType) Compare(typ Type) bool {
switch typ.(type) {
// we can only compare NoopType to others of the same type
case *NoopType:
typ := typ.(*NoopType)
if obj.Name != typ.Name {
return false
}
default:
return false
}
return true
}