Compare commits
95 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e9ab3ca4d | ||
|
|
99b058e5e8 | ||
|
|
fc14e5c70e | ||
|
|
e921dfa498 | ||
|
|
40476a66c2 | ||
|
|
89182521de | ||
|
|
ead025cbe7 | ||
|
|
83caea1bdc | ||
|
|
f4da8756bd | ||
|
|
acff20c54e | ||
|
|
bbb35fa12c | ||
|
|
2896775f77 | ||
|
|
625ae31f63 | ||
|
|
72681349e5 | ||
|
|
e342c5a06a | ||
|
|
028fb1c258 | ||
|
|
6e6614808b | ||
|
|
c47418b02d | ||
|
|
97fda59999 | ||
|
|
b3e5f77d5d | ||
|
|
85f9db12f5 | ||
|
|
655d527d5f | ||
|
|
925811984e | ||
|
|
4cb76d3347 | ||
|
|
ff838700d0 | ||
|
|
3cf8c4a6e8 | ||
|
|
9e08de0bcf | ||
|
|
8f0d3e3abe | ||
|
|
3870a2c781 | ||
|
|
cc9bc6ac75 | ||
|
|
cd663d2384 | ||
|
|
dee8cd97c5 | ||
|
|
a64b9f8e1a | ||
|
|
b3b78b9405 | ||
|
|
f4a86b2364 | ||
|
|
8b0a078dac | ||
|
|
fb8513094b | ||
|
|
08d5a3baae | ||
|
|
358604def2 | ||
|
|
0795cadad1 | ||
|
|
0d8b4aa2bd | ||
|
|
2930985238 | ||
|
|
d5367b7a1c | ||
|
|
820294cd9a | ||
|
|
9ab746fbf3 | ||
|
|
d7903d8736 | ||
|
|
0ca9351665 | ||
|
|
491e9fd9bc | ||
|
|
4599b393e9 | ||
|
|
30385c85f3 | ||
|
|
8308680a50 | ||
|
|
9c18972af4 | ||
|
|
79a5e0972f | ||
|
|
304b48265f | ||
|
|
c0d3678b79 | ||
|
|
74baa032b5 | ||
|
|
61c668edd3 | ||
|
|
8db5d630d5 | ||
|
|
6e9439f4e3 | ||
|
|
f7858b8e9b | ||
|
|
935805aeda | ||
|
|
4c6647d807 | ||
|
|
c57946e29b | ||
|
|
48eddc3721 | ||
|
|
8ea8ef8d0e | ||
|
|
1c49bbc487 | ||
|
|
ebc1c60063 | ||
|
|
590394b2be | ||
|
|
97664c3b13 | ||
|
|
ea7fd76f93 | ||
|
|
45ff3b6aa4 | ||
|
|
d769309cc0 | ||
|
|
d2bcfdc7aa | ||
|
|
72525d30b1 | ||
|
|
95489b9c07 | ||
|
|
0bbfd1d071 | ||
|
|
904ace8027 | ||
|
|
d8cbeb56f9 | ||
|
|
72a8027b7f | ||
|
|
39f7c305f1 | ||
|
|
1ba6be2957 | ||
|
|
6b4fa21074 | ||
|
|
0ea6f30ef2 | ||
|
|
4f6605b3d1 | ||
|
|
e44da9578e | ||
|
|
dd3759ae38 | ||
|
|
3e4709d9da | ||
|
|
66e030a175 | ||
|
|
451fb35f93 | ||
|
|
8dbca80853 | ||
|
|
6150c2ccb9 | ||
|
|
2708223ab5 | ||
|
|
327c5fb6fb | ||
|
|
f789cf1403 | ||
|
|
19a909001b |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -1,3 +1,9 @@
|
|||||||
|
.omv/
|
||||||
|
.ssh/
|
||||||
|
.vagrant/
|
||||||
mgmt-documentation.pdf
|
mgmt-documentation.pdf
|
||||||
old/
|
old/
|
||||||
tmp/
|
tmp/
|
||||||
|
*_stringer.go
|
||||||
|
mgmt
|
||||||
|
rpmbuild/
|
||||||
|
|||||||
25
.travis.yml
Normal file
25
.travis.yml
Normal 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
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
Mgmt
|
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
|
Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Mgmt
|
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
|
Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
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
|
the design flaws or limitations that the current generation of tools do, and no
|
||||||
tool existed!
|
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!
|
###You didn't answer my question, or I have a question!
|
||||||
|
|
||||||
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
|
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>`
|
####`--file <graph.yaml>`
|
||||||
Point to a graph file to run.
|
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
|
##Examples
|
||||||
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git
|
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:
|
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
|
##Authors
|
||||||
|
|
||||||
Copyright (C) 2013-2015+ James Shubin and the project contributors
|
Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||||
|
|
||||||
Please see the
|
Please see the
|
||||||
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
|
||||||
|
|||||||
206
Makefile
206
Makefile
@@ -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
|
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
|
.SILENT: clean
|
||||||
|
|
||||||
VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty)
|
SVERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)
|
||||||
PROGRAM := $(notdir $(CURDIR))
|
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
|
all: docs
|
||||||
|
|
||||||
@@ -11,35 +43,179 @@ all: docs
|
|||||||
version:
|
version:
|
||||||
@echo $(VERSION)
|
@echo $(VERSION)
|
||||||
|
|
||||||
run:
|
program:
|
||||||
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.version $(VERSION) -X main.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:
|
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
|
build: mgmt
|
||||||
|
|
||||||
mgmt: main.go
|
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:
|
clean:
|
||||||
[ ! -e mgmt ] || rm mgmt
|
[ ! -e mgmt ] || rm mgmt
|
||||||
|
rm -f *_stringer.go # generated by `go generate`
|
||||||
|
|
||||||
test:
|
test:
|
||||||
./test.sh
|
./test.sh
|
||||||
./test/test-gofmt.sh
|
|
||||||
./test/test-yamlfmt.sh
|
|
||||||
go test
|
|
||||||
#go test ./pgraph
|
|
||||||
go test -race
|
|
||||||
#go test -race ./pgraph
|
|
||||||
|
|
||||||
format:
|
format:
|
||||||
find -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
|
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
|
docs: mgmt-documentation.pdf
|
||||||
|
|
||||||
mgmt-documentation.pdf: DOCUMENTATION.md
|
mgmt-documentation.pdf: DOCUMENTATION.md
|
||||||
pandoc DOCUMENTATION.md -o 'mgmt-documentation.pdf'
|
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
|
||||||
|
|||||||
72
README.md
72
README.md
@@ -1,35 +1,65 @@
|
|||||||
# *mgmt*: This is: mgmt!
|
# *mgmt*: This is: mgmt!
|
||||||
|
|
||||||
[](http://travis-ci.org/purpleidea/mgmt)
|
[](http://travis-ci.org/purpleidea/mgmt)
|
||||||
|
[](DOCUMENTATION.md)
|
||||||
|
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||||
|
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||||
|
[](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:
|
## Documentation:
|
||||||
Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md).
|
Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md).
|
||||||
|
|
||||||
## Questions:
|
## Roadmap:
|
||||||
Come join us in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode!
|
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:
|
## Bugs:
|
||||||
Please look in the [examples/](examples/) folder for usage. If none exist, please contribute one!
|
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.
|
||||||
## 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
|
|
||||||
|
|
||||||
## Dependencies:
|
## Dependencies:
|
||||||
* golang (available in most distros)
|
* golang 1.4 or higher (required, available in most distros)
|
||||||
* pandoc (for building a pdf of the documentation)
|
* 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:
|
## 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
2
THANKS
@@ -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.
|
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!
|
Jeff Darcy - For some algorithm recommendations, and NACKing my TopoSort idea!
|
||||||
|
|
||||||
Red Hat, inc. - For paying my salary, thus financially supporting my hacking.
|
Red Hat, inc. - For paying my salary, thus financially supporting my hacking.
|
||||||
|
|||||||
43
TODO.md
Normal file
43
TODO.md
Normal 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!
|
||||||
203
config.go
203
config.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -22,23 +22,12 @@ import (
|
|||||||
"gopkg.in/yaml.v2"
|
"gopkg.in/yaml.v2"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type noopTypeConfig struct {
|
type collectorTypeConfig struct {
|
||||||
Name string `yaml:"name"`
|
Type string `yaml:"type"`
|
||||||
}
|
Pattern string `yaml:"pattern"` // XXX: Not Implemented
|
||||||
|
|
||||||
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 vertexConfig struct {
|
type vertexConfig struct {
|
||||||
@@ -52,18 +41,20 @@ type edgeConfig struct {
|
|||||||
To vertexConfig `yaml:"to"`
|
To vertexConfig `yaml:"to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type graphConfig struct {
|
type GraphConfig struct {
|
||||||
Graph string `yaml:"graph"`
|
Graph string `yaml:"graph"`
|
||||||
Types struct {
|
Types struct {
|
||||||
Noop []noopTypeConfig `yaml:"noop"`
|
Noop []NoopType `yaml:"noop"`
|
||||||
File []fileTypeConfig `yaml:"file"`
|
File []FileType `yaml:"file"`
|
||||||
Service []serviceTypeConfig `yaml:"service"`
|
Service []ServiceType `yaml:"service"`
|
||||||
|
Exec []ExecType `yaml:"exec"`
|
||||||
} `yaml:"types"`
|
} `yaml:"types"`
|
||||||
|
Collector []collectorTypeConfig `yaml:"collect"`
|
||||||
Edges []edgeConfig `yaml:"edges"`
|
Edges []edgeConfig `yaml:"edges"`
|
||||||
Comment string `yaml:"comment"`
|
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 {
|
if err := yaml.Unmarshal(data, c); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -73,54 +64,166 @@ func (c *graphConfig) Parse(data []byte) error {
|
|||||||
return nil
|
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 config GraphConfig
|
||||||
var FileMap map[string]*Vertex = make(map[string]*Vertex)
|
if err := config.Parse(data); err != nil {
|
||||||
var ServiceMap map[string]*Vertex = make(map[string]*Vertex)
|
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["noop"] = NoopMap
|
||||||
lookup["file"] = FileMap
|
lookup["file"] = FileMap
|
||||||
lookup["service"] = ServiceMap
|
lookup["service"] = ServiceMap
|
||||||
|
lookup["exec"] = ExecMap
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(filename)
|
//log.Printf("%+v", config) // debug
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var config graphConfig
|
g.SetName(config.Graph) // set graph name
|
||||||
if err := config.Parse(data); err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
//fmt.Printf("%+v\n", config) // debug
|
|
||||||
|
|
||||||
g := NewGraph(config.Graph)
|
var keep []*Vertex // list of vertex which are the same in new graph
|
||||||
|
|
||||||
for _, t := range config.Types.Noop {
|
for _, t := range config.Types.Noop {
|
||||||
NoopMap[t.Name] = NewVertex(t.Name, "noop")
|
obj := NewNoopType(t.Name)
|
||||||
// FIXME: duplicate of name stored twice... where should it go?
|
v := g.GetVertexMatch(obj)
|
||||||
NoopMap[t.Name].Associate(NewNoopType(t.Name))
|
if v == nil { // no match found
|
||||||
g.AddVertex(NoopMap[t.Name]) // call standalone in case not part of an edge
|
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 {
|
for _, t := range config.Types.File {
|
||||||
FileMap[t.Name] = NewVertex(t.Name, "file")
|
// XXX: should we export based on a @@ prefix, or a metaparam
|
||||||
// FIXME: duplicate of name stored twice... where should it go?
|
// like exported => true || exported => (host pattern)||(other pattern?)
|
||||||
FileMap[t.Name].Associate(NewFileType(t.Name, t.Path, t.Content, t.State))
|
if strings.HasPrefix(t.Name, "@@") { // exported resource
|
||||||
g.AddVertex(FileMap[t.Name]) // call standalone in case not part of an edge
|
// 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 {
|
for _, t := range config.Types.Service {
|
||||||
ServiceMap[t.Name] = NewVertex(t.Name, "service")
|
obj := NewServiceType(t.Name, t.State, t.Startup)
|
||||||
// FIXME: duplicate of name stored twice... where should it go?
|
v := g.GetVertexMatch(obj)
|
||||||
ServiceMap[t.Name].Associate(NewServiceType(t.Name, t.State, t.Startup))
|
if v == nil { // no match found
|
||||||
g.AddVertex(ServiceMap[t.Name]) // call standalone in case not part of an edge
|
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 {
|
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))
|
g.AddEdge(lookup[e.From.Type][e.From.Name], lookup[e.To.Type][e.To.Name], NewEdge(e.Name))
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
return g
|
|
||||||
}
|
}
|
||||||
|
|||||||
155
configwatch.go
Normal file
155
configwatch.go
Normal 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
300
etcd.go
Normal 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
|
||||||
|
}
|
||||||
41
event.go
41
event.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -17,20 +17,39 @@
|
|||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
//go:generate stringer -type=eventName -output=eventname_stringer.go
|
||||||
"code.google.com/p/go-uuid/uuid"
|
type eventName int
|
||||||
|
|
||||||
|
const (
|
||||||
|
eventExit eventName = iota
|
||||||
|
eventStart
|
||||||
|
eventPause
|
||||||
|
eventPoke
|
||||||
|
eventBackPoke
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
uuid string
|
Name eventName
|
||||||
Name string
|
Resp chan bool // channel to send an ack response on, nil to skip
|
||||||
Type string
|
//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 {
|
// send a single acknowledgement on the channel if one was requested
|
||||||
return &Event{
|
func (event *Event) ACK() {
|
||||||
uuid: uuid.New(),
|
if event.Resp != nil { // if they've requested an ACK
|
||||||
Name: name,
|
event.Resp <- true // send ACK
|
||||||
Type: t,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
20
examples/graph0.yaml
Normal 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
|
||||||
@@ -5,22 +5,22 @@ types:
|
|||||||
- name: noop1
|
- name: noop1
|
||||||
file:
|
file:
|
||||||
- name: file1
|
- name: file1
|
||||||
path: /tmp/mgmt/f1
|
path: "/tmp/mgmt/f1"
|
||||||
content: |
|
content: |
|
||||||
i am f1
|
i am f1
|
||||||
state: exists
|
state: exists
|
||||||
- name: file2
|
- name: file2
|
||||||
path: /tmp/mgmt/f2
|
path: "/tmp/mgmt/f2"
|
||||||
content: |
|
content: |
|
||||||
i am f2
|
i am f2
|
||||||
state: exists
|
state: exists
|
||||||
- name: file3
|
- name: file3
|
||||||
path: /tmp/mgmt/f3
|
path: "/tmp/mgmt/f3"
|
||||||
content: |
|
content: |
|
||||||
i am f3
|
i am f3
|
||||||
state: exists
|
state: exists
|
||||||
- name: file4
|
- name: file4
|
||||||
path: /tmp/mgmt/f4
|
path: "/tmp/mgmt/f4"
|
||||||
content: |
|
content: |
|
||||||
i am f4 and i should not be here
|
i am f4 and i should not be here
|
||||||
state: absent
|
state: absent
|
||||||
|
|||||||
128
examples/graph10.yaml
Normal file
128
examples/graph10.yaml
Normal 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
22
examples/graph1a.yaml
Normal 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
22
examples/graph1b.yaml
Normal 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
|
||||||
@@ -5,7 +5,7 @@ types:
|
|||||||
- name: noop1
|
- name: noop1
|
||||||
file:
|
file:
|
||||||
- name: file1
|
- name: file1
|
||||||
path: /tmp/mgmt/f1
|
path: "/tmp/mgmt/f1"
|
||||||
content: |
|
content: |
|
||||||
i am f1
|
i am f1
|
||||||
state: exists
|
state: exists
|
||||||
|
|||||||
28
examples/graph3a.yaml
Normal file
28
examples/graph3a.yaml
Normal 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
28
examples/graph3b.yaml
Normal 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
28
examples/graph3c.yaml
Normal 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
18
examples/graph4.yaml
Normal 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
13
examples/graph5.yaml
Normal 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
6
examples/graph6.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
graph: mygraph
|
||||||
|
types:
|
||||||
|
noop:
|
||||||
|
- name: noop1
|
||||||
|
edges:
|
||||||
17
examples/graph7.yaml
Normal file
17
examples/graph7.yaml
Normal 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
59
examples/graph8.yaml
Normal 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
32
examples/graph8a.yaml
Normal 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
32
examples/graph8b.yaml
Normal 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
32
examples/graph8c.yaml
Normal 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
15
examples/graph8d.yaml
Normal 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
77
examples/graph9.yaml
Normal 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
|
||||||
8
examples/purpleidea.service
Normal file
8
examples/purpleidea.service
Normal 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
339
exec.go
Normal 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
|
||||||
|
}
|
||||||
256
file.go
256
file.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,10 +18,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/go-uuid/uuid"
|
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
|
||||||
"gopkg.in/fsnotify.v1"
|
"gopkg.in/fsnotify.v1"
|
||||||
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
||||||
"io"
|
"io"
|
||||||
@@ -34,39 +32,82 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type FileType struct {
|
type FileType struct {
|
||||||
uuid string
|
BaseType `yaml:",inline"`
|
||||||
Type string // always "file"
|
Path string `yaml:"path"` // path variable (should default to name)
|
||||||
Name string // name variable
|
Dirname string `yaml:"dirname"`
|
||||||
Events chan string // FIXME: eventually a struct for the event?
|
Basename string `yaml:"basename"`
|
||||||
Path string // path variable (should default to name)
|
Content string `yaml:"content"`
|
||||||
Content string
|
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
|
||||||
State string // state: exists/present?, absent, (undefined?)
|
|
||||||
sha256sum string
|
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 ...
|
// FIXME if path = nil, path = name ...
|
||||||
return &FileType{
|
return &FileType{
|
||||||
uuid: uuid.New(),
|
BaseType: BaseType{
|
||||||
Type: "file",
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Events: make(chan string, 1), // XXX: chan size?
|
events: make(chan Event),
|
||||||
|
vertex: nil,
|
||||||
|
},
|
||||||
Path: path,
|
Path: path,
|
||||||
|
Dirname: dirname,
|
||||||
|
Basename: basename,
|
||||||
Content: content,
|
Content: content,
|
||||||
State: state,
|
State: state,
|
||||||
sha256sum: "",
|
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
|
// File watcher for files and directories
|
||||||
// Modify with caution, probably important to write some test cases first!
|
// Modify with caution, probably important to write some test cases first!
|
||||||
func (obj FileType) Watch(v *Vertex) {
|
// obj.GetPath(): file or directory
|
||||||
// obj.Path: file or directory
|
func (obj *FileType) Watch() {
|
||||||
//var recursive bool = false
|
if obj.IsWatching() {
|
||||||
//var isdir = (obj.Path[len(obj.Path)-1:] == "/") // dirs have trailing slashes
|
return
|
||||||
//fmt.Printf("IsDirectory: %v\n", isdir)
|
}
|
||||||
|
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()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -77,90 +118,84 @@ func (obj FileType) Watch(v *Vertex) {
|
|||||||
patharray := PathSplit(safename) // tokenize the path
|
patharray := PathSplit(safename) // tokenize the path
|
||||||
var index = len(patharray) // starting index
|
var index = len(patharray) // starting index
|
||||||
var current string // current "watcher" location
|
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 send = false // send event?
|
||||||
var extraCheck = false
|
var exit = false
|
||||||
|
var dirty = false
|
||||||
|
|
||||||
for {
|
for {
|
||||||
current = strings.Join(patharray[0:index], "/")
|
current = strings.Join(patharray[0:index], "/")
|
||||||
if current == "" { // the empty string top is the root dir ("/")
|
if current == "" { // the empty string top is the root dir ("/")
|
||||||
current = "/"
|
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
|
// initialize in the loop so that we can reset on rm-ed handles
|
||||||
err = watcher.Add(current)
|
err = watcher.Add(current)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if DEBUG {
|
||||||
|
log.Printf("File[%v]: watcher.Add(%v): Error: %v", obj.GetName(), current, err)
|
||||||
|
}
|
||||||
if err == syscall.ENOENT {
|
if err == syscall.ENOENT {
|
||||||
index-- // usually not found, move up one dir
|
index-- // usually not found, move up one dir
|
||||||
} else if err == syscall.ENOSPC {
|
} else if err == syscall.ENOSPC {
|
||||||
// XXX: i sometimes see: no space left on device
|
// XXX: occasionally: no space left on device,
|
||||||
// XXX: why causes this to happen ?
|
// XXX: probably due to lack of inotify watches
|
||||||
log.Printf("Strange file[%v] error: %+v\n", obj.Name, err.Error) // 0x408da0
|
log.Printf("Lack of watches for file[%v] error: %+v", obj.Name, err.Error) // 0x408da0
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Unknown file[%v] error:\n", obj.Name)
|
log.Printf("Unknown file[%v] error:", obj.Name)
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
index = int(math.Max(1, float64(index)))
|
index = int(math.Max(1, float64(index)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: check state after inotify started
|
obj.SetState(typeWatching) // reset
|
||||||
// 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
|
|
||||||
//}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case event := <-watcher.Events:
|
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,
|
// this is the difference between what we're watching,
|
||||||
// and the event... doesn't mean we can't watch deeper
|
// and the event... doesn't mean we can't watch deeper
|
||||||
if current == event.Name {
|
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) {
|
} 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) {
|
} 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 {
|
} else {
|
||||||
// XXX multiple watchers receive each others events
|
// TODO different watchers get each others events!
|
||||||
// https://github.com/go-fsnotify/fsnotify/issues/95
|
// https://github.com/go-fsnotify/fsnotify/issues/95
|
||||||
// this happened with two values such as:
|
// this happened with two values such as:
|
||||||
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
// 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
|
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 we have what we wanted, awesome, send an event...
|
||||||
if event.Name == safename {
|
if event.Name == safename {
|
||||||
//log.Println("Event!")
|
//log.Println("Event!")
|
||||||
|
// FIXME: should all these below cases trigger?
|
||||||
send = true
|
send = true
|
||||||
|
dirty = true
|
||||||
|
|
||||||
// file removed, move the watch upwards
|
// 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!")
|
//log.Println("Removal!")
|
||||||
watcher.Remove(current)
|
watcher.Remove(current)
|
||||||
index--
|
index--
|
||||||
}
|
}
|
||||||
|
|
||||||
// we must be a parent watcher, so descend in
|
// we must be a parent watcher, so descend in
|
||||||
if delta_depth < 0 {
|
if deltaDepth < 0 {
|
||||||
watcher.Remove(current)
|
watcher.Remove(current)
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
@@ -169,13 +204,18 @@ func (obj FileType) Watch(v *Vertex) {
|
|||||||
} else if HasPathPrefix(safename, event.Name) {
|
} else if HasPathPrefix(safename, event.Name) {
|
||||||
//log.Println("Above!")
|
//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!")
|
log.Println("Removal!")
|
||||||
watcher.Remove(current)
|
watcher.Remove(current)
|
||||||
index--
|
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)
|
watcher.Remove(current)
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
@@ -184,37 +224,42 @@ func (obj FileType) Watch(v *Vertex) {
|
|||||||
} else if HasPathPrefix(event.Name, safename) {
|
} else if HasPathPrefix(event.Name, safename) {
|
||||||
//log.Println("Event2!")
|
//log.Println("Event2!")
|
||||||
send = true
|
send = true
|
||||||
|
dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
case err := <-watcher.Errors:
|
case err := <-watcher.Errors:
|
||||||
|
obj.SetConvergedState(typeConvergedNil) // XXX ?
|
||||||
log.Println("error:", err)
|
log.Println("error:", err)
|
||||||
log.Fatal(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:
|
case event := <-obj.events:
|
||||||
if exit == "exit" {
|
obj.SetConvergedState(typeConvergedNil)
|
||||||
return
|
if exit, send = obj.ReadEvent(&event); exit {
|
||||||
} else {
|
return // exit
|
||||||
log.Fatal("Unknown event: %v\n", 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
|
// do all our event sending all together to avoid duplicate msgs
|
||||||
if send {
|
if send {
|
||||||
send = false
|
send = false
|
||||||
//log.Println("Sending event!")
|
// only invalid state on certain types of events
|
||||||
//v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, event.Op)
|
if dirty {
|
||||||
v.Events <- fmt.Sprintf("file(%v): %v", obj.Path, "event!") // FIXME: use struct
|
dirty = false
|
||||||
|
obj.isStateOK = false // something made state dirty
|
||||||
|
}
|
||||||
|
Process(obj) // XXX: rename this function
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) Exit() bool {
|
func (obj *FileType) HashSHA256fromContent() string {
|
||||||
obj.Events <- "exit"
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (obj FileType) HashSHA256fromContent() string {
|
|
||||||
if obj.sha256sum != "" { // return if already computed
|
if obj.sha256sum != "" { // return if already computed
|
||||||
return obj.sha256sum
|
return obj.sha256sum
|
||||||
}
|
}
|
||||||
@@ -225,11 +270,16 @@ func (obj FileType) HashSHA256fromContent() string {
|
|||||||
return obj.sha256sum
|
return obj.sha256sum
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) StateOK() bool {
|
// FIXME: add the obj.CleanState() calls all over the true returns!
|
||||||
if _, err := os.Stat(obj.Path); os.IsNotExist(err) {
|
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
|
// no such file or directory
|
||||||
if obj.State == "absent" {
|
if obj.State == "absent" {
|
||||||
return true // missing file should be missing, phew :)
|
return obj.CleanState() // missing file should be missing, phew :)
|
||||||
} else {
|
} else {
|
||||||
// state invalid, skip expensive checksums
|
// state invalid, skip expensive checksums
|
||||||
return false
|
return false
|
||||||
@@ -238,15 +288,15 @@ func (obj FileType) StateOK() bool {
|
|||||||
|
|
||||||
// TODO: add file mode check here...
|
// TODO: add file mode check here...
|
||||||
|
|
||||||
if PathIsDir(obj.Path) {
|
if PathIsDir(obj.GetPath()) {
|
||||||
return obj.StateOKDir()
|
return obj.StateOKDir()
|
||||||
} else {
|
} else {
|
||||||
return obj.StateOKFile()
|
return obj.StateOKFile()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) StateOKFile() bool {
|
func (obj *FileType) StateOKFile() bool {
|
||||||
if PathIsDir(obj.Path) {
|
if PathIsDir(obj.GetPath()) {
|
||||||
log.Fatal("This should only be called on a File type.")
|
log.Fatal("This should only be called on a File type.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +304,7 @@ func (obj FileType) StateOKFile() bool {
|
|||||||
|
|
||||||
hash := sha256.New()
|
hash := sha256.New()
|
||||||
|
|
||||||
f, err := os.Open(obj.Path)
|
f, err := os.Open(obj.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
//log.Fatal(err)
|
//log.Fatal(err)
|
||||||
return false
|
return false
|
||||||
@@ -267,7 +317,7 @@ func (obj FileType) StateOKFile() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sha256sum := hex.EncodeToString(hash.Sum(nil))
|
sha256sum := hex.EncodeToString(hash.Sum(nil))
|
||||||
//fmt.Printf("sha256sum: %v\n", sha256sum)
|
//log.Printf("sha256sum: %v", sha256sum)
|
||||||
|
|
||||||
if obj.HashSHA256fromContent() == sha256sum {
|
if obj.HashSHA256fromContent() == sha256sum {
|
||||||
return true
|
return true
|
||||||
@@ -276,8 +326,8 @@ func (obj FileType) StateOKFile() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) StateOKDir() bool {
|
func (obj *FileType) StateOKDir() bool {
|
||||||
if !PathIsDir(obj.Path) {
|
if !PathIsDir(obj.GetPath()) {
|
||||||
log.Fatal("This should only be called on a Dir type.")
|
log.Fatal("This should only be called on a Dir type.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,33 +336,33 @@ func (obj FileType) StateOKDir() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) Apply() bool {
|
func (obj *FileType) Apply() bool {
|
||||||
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
|
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
|
||||||
|
|
||||||
if PathIsDir(obj.Path) {
|
if PathIsDir(obj.GetPath()) {
|
||||||
return obj.ApplyDir()
|
return obj.ApplyDir()
|
||||||
} else {
|
} else {
|
||||||
return obj.ApplyFile()
|
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.")
|
log.Fatal("This should only be called on a File type.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if obj.State == "absent" {
|
if obj.State == "absent" {
|
||||||
log.Printf("About to remove: %v\n", obj.Path)
|
log.Printf("About to remove: %v", obj.GetPath())
|
||||||
err := os.Remove(obj.Path)
|
err := os.Remove(obj.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
//fmt.Println("writing: " + filename)
|
//log.Println("writing: " + filename)
|
||||||
f, err := os.Create(obj.Path)
|
f, err := os.Create(obj.GetPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("error:", err)
|
log.Println("error:", err)
|
||||||
return false
|
return false
|
||||||
@@ -328,8 +378,8 @@ func (obj FileType) ApplyFile() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj FileType) ApplyDir() bool {
|
func (obj *FileType) ApplyDir() bool {
|
||||||
if !PathIsDir(obj.Path) {
|
if !PathIsDir(obj.GetPath()) {
|
||||||
log.Fatal("This should only be called on a Dir type.")
|
log.Fatal("This should only be called on a Dir type.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,3 +387,25 @@ func (obj FileType) ApplyDir() bool {
|
|||||||
log.Fatal("Not implemented!")
|
log.Fatal("Not implemented!")
|
||||||
return true
|
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
|
||||||
|
}
|
||||||
|
|||||||
205
main.go
205
main.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"github.com/codegangsta/cli"
|
"github.com/codegangsta/cli"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
@@ -26,12 +25,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
//etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// set at compile time
|
// set at compile time
|
||||||
var (
|
var (
|
||||||
version string
|
|
||||||
program string
|
program string
|
||||||
|
version string
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -48,7 +48,6 @@ func waitForSignal(exit chan bool) {
|
|||||||
select {
|
select {
|
||||||
case e := <-signals: // any signal will do
|
case e := <-signals: // any signal will do
|
||||||
if e == os.Interrupt {
|
if e == os.Interrupt {
|
||||||
fmt.Println() // put ^C char from terminal on its own line
|
|
||||||
log.Println("Interrupted by ^C")
|
log.Println("Interrupted by ^C")
|
||||||
} else {
|
} else {
|
||||||
log.Println("Interrupted by signal")
|
log.Println("Interrupted by signal")
|
||||||
@@ -59,54 +58,149 @@ func waitForSignal(exit chan bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run(c *cli.Context) {
|
func run(c *cli.Context) {
|
||||||
var start int64 = time.Now().UnixNano()
|
var start = time.Now().UnixNano()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
exit := make(chan bool) // exit signal
|
exit := make(chan bool) // exit signal
|
||||||
log.Printf("This is: %v, version: %v\n", program, version)
|
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...
|
// exit after `max-runtime` seconds for no reason at all...
|
||||||
if i := c.Int("exittime"); i > 0 {
|
if i := c.Int("max-runtime"); i > 0 {
|
||||||
go func() {
|
go func() {
|
||||||
time.Sleep(time.Duration(i) * time.Second)
|
time.Sleep(time.Duration(i) * time.Second)
|
||||||
exit <- true
|
exit <- true
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the graph from a config file
|
// initial etcd peer endpoint
|
||||||
G := GraphFromConfig(c.String("file"))
|
seed := c.String("seed")
|
||||||
log.Printf("Graph: %v\n", G) // show graph
|
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 ?
|
// etcd
|
||||||
log.Printf("Main->Starting[%v]\n", x.Name)
|
etcdO := &EtcdWObject{
|
||||||
|
seed: seed,
|
||||||
wg.Add(1)
|
ctimeout: c.Int("converged-timeout"),
|
||||||
// must pass in value to avoid races...
|
converged: converged,
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
waitForSignal(exit) // pass in exit channel to watch
|
||||||
|
|
||||||
G.Exit() // tell all the children to exit
|
G.Exit() // tell all the children to exit
|
||||||
|
|
||||||
if DEBUG {
|
if DEBUG {
|
||||||
for i := range G.GetVerticesChan() {
|
log.Printf("Graph: %v", G)
|
||||||
fmt.Printf("Vertex: %v\n", i)
|
|
||||||
}
|
|
||||||
fmt.Printf("Graph: %v\n", G)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait() // wait for primary go routines to exit
|
wg.Wait() // wait for primary go routines to exit
|
||||||
@@ -116,6 +210,13 @@ func run(c *cli.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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 := cli.NewApp()
|
||||||
app.Name = program
|
app.Name = program
|
||||||
app.Usage = "next generation config management"
|
app.Usage = "next generation config management"
|
||||||
@@ -134,8 +235,44 @@ func main() {
|
|||||||
Value: "",
|
Value: "",
|
||||||
Usage: "graph definition to run",
|
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{
|
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,
|
Value: 0,
|
||||||
Usage: "exit after a maximum of approximately this many seconds",
|
Usage: "exit after a maximum of approximately this many seconds",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|||||||
63
mgmt.spec.in
Normal file
63
mgmt.spec.in
Normal 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
67
misc.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,16 +18,31 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/gob"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Similar to the GNU dirname command
|
// Similar to the GNU dirname command
|
||||||
func Dirname(p string) string {
|
func Dirname(p string) string {
|
||||||
|
if p == "/" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
d, _ := path.Split(path.Clean(p))
|
d, _ := path.Split(path.Clean(p))
|
||||||
return d
|
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
|
// Split a path into an array of tokens excluding any trailing empty tokens
|
||||||
func PathSplit(p string) []string {
|
func PathSplit(p string) []string {
|
||||||
return strings.Split(path.Clean(p), "/")
|
return strings.Split(path.Clean(p), "/")
|
||||||
@@ -52,6 +67,56 @@ func HasPathPrefix(p, prefix string) bool {
|
|||||||
return true
|
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 {
|
func PathIsDir(p string) bool {
|
||||||
return p[len(p)-1:] == "/" // a dir has a trailing slash in this context
|
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
72
misc/centos-ci.py
Executable 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
66
misc/copr-build.py
Executable 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
39
misc/make-deps.sh
Executable 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
48
misc/make-path.sh
Executable 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
13
misc/mgmt.bashrc
Executable 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
1
misc/mgmt.conf.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# example mgmt configuration file, currently has not options at the moment!
|
||||||
105
misc_test.go
105
misc_test.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,9 +32,22 @@ func TestMiscT1(t *testing.T) {
|
|||||||
t.Errorf("Result is incorrect.")
|
t.Errorf("Result is incorrect.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if Dirname("/") != "/" {
|
if Dirname("/") != "" { // TODO: should this equal "/" or "" ?
|
||||||
t.Errorf("Result is incorrect.")
|
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) {
|
func TestMiscT2(t *testing.T) {
|
||||||
@@ -82,6 +96,41 @@ func TestMiscT3(t *testing.T) {
|
|||||||
|
|
||||||
func TestMiscT4(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 {
|
if PathIsDir("/foo/bar/baz/") != true {
|
||||||
t.Errorf("Result should be false.")
|
t.Errorf("Result should be false.")
|
||||||
}
|
}
|
||||||
@@ -97,5 +146,55 @@ func TestMiscT4(t *testing.T) {
|
|||||||
if PathIsDir("/") != true {
|
if PathIsDir("/") != true {
|
||||||
t.Errorf("Result should be 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
39
omv.yaml
Normal 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
|
||||||
506
pgraph.go
506
pgraph.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -19,69 +19,106 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/go-uuid/uuid"
|
"errors"
|
||||||
//"container/list" // doubly linked list
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"syscall"
|
||||||
"time"
|
"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:
|
// The graph abstract data type (ADT) is defined as follows:
|
||||||
// NOTE: the directed graph arrows point from left to right ( --> )
|
// * the directed graph arrows point from left to right ( -> )
|
||||||
// NOTE: the arrows point towards their dependencies (eg: arrows mean requires)
|
// * 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 {
|
type Graph struct {
|
||||||
uuid string
|
|
||||||
Name 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
|
//Directed bool
|
||||||
startcount int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Vertex struct {
|
type Vertex struct {
|
||||||
uuid string
|
|
||||||
graph *Graph // store a pointer to the graph it's on
|
graph *Graph // store a pointer to the graph it's on
|
||||||
Name string
|
Type // anonymous field
|
||||||
Type string
|
data map[string]string // XXX: currently unused i think, remove?
|
||||||
Timestamp int64 // last updated timestamp ?
|
|
||||||
Events chan string // FIXME: eventually a struct for the event?
|
|
||||||
Typedata Type
|
|
||||||
data map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Edge struct {
|
type Edge struct {
|
||||||
uuid string
|
|
||||||
Name string
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewGraph(name string) *Graph {
|
func NewGraph(name string) *Graph {
|
||||||
return &Graph{
|
return &Graph{
|
||||||
uuid: uuid.New(),
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Adjacency: make(map[*Vertex]map[*Vertex]*Edge),
|
Adjacency: make(map[*Vertex]map[*Vertex]*Edge),
|
||||||
|
state: graphNil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVertex(name, t string) *Vertex {
|
func NewVertex(t Type) *Vertex {
|
||||||
return &Vertex{
|
return &Vertex{
|
||||||
uuid: uuid.New(),
|
|
||||||
Name: name,
|
|
||||||
Type: t,
|
Type: t,
|
||||||
Timestamp: -1,
|
|
||||||
Events: make(chan string, 1), // XXX: chan size?
|
|
||||||
data: make(map[string]string),
|
data: make(map[string]string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEdge(name string) *Edge {
|
func NewEdge(name string) *Edge {
|
||||||
return &Edge{
|
return &Edge{
|
||||||
uuid: uuid.New(),
|
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Graph() creates a new, empty graph.
|
// returns the name of the graph
|
||||||
// addVertex(vert) adds an instance of Vertex to 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) {
|
func (g *Graph) AddVertex(v *Vertex) {
|
||||||
if _, exists := g.Adjacency[v]; !exists {
|
if _, exists := g.Adjacency[v]; !exists {
|
||||||
g.Adjacency[v] = make(map[*Vertex]*Edge)
|
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) {
|
func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
|
||||||
// NOTE: this doesn't allow more than one edge between two vertexes...
|
// NOTE: this doesn't allow more than one edge between two vertexes...
|
||||||
// TODO: is this a problem?
|
// TODO: is this a problem?
|
||||||
@@ -100,35 +144,60 @@ func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
|
|||||||
g.Adjacency[v1][v2] = e
|
g.Adjacency[v1][v2] = e
|
||||||
}
|
}
|
||||||
|
|
||||||
// addEdge(fromVert, toVert, weight) Adds a new, weighted, directed edge to the graph that connects two vertices.
|
// XXX: does it make sense to return a channel here?
|
||||||
// getVertex(vertKey) finds the vertex in the graph named vertKey.
|
// GetVertex finds the vertex in the graph with a particular search name
|
||||||
func (g *Graph) GetVertex(uuid string) chan *Vertex {
|
func (g *Graph) GetVertex(name string) chan *Vertex {
|
||||||
ch := make(chan *Vertex, 1)
|
ch := make(chan *Vertex, 1)
|
||||||
go func(uuid string) {
|
go func(name string) {
|
||||||
for k := range g.Adjacency {
|
for k := range g.Adjacency {
|
||||||
v := *k
|
if k.GetName() == name {
|
||||||
if v.uuid == uuid {
|
|
||||||
ch <- k
|
ch <- k
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
close(ch)
|
close(ch)
|
||||||
}(uuid)
|
}(name)
|
||||||
return ch
|
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 {
|
func (g *Graph) NumVertices() int {
|
||||||
return len(g.Adjacency)
|
return len(g.Adjacency)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// number of edges in the graph
|
||||||
func (g *Graph) NumEdges() int {
|
func (g *Graph) NumEdges() int {
|
||||||
// XXX: not implemented
|
count := 0
|
||||||
return -1
|
for k := range g.Adjacency {
|
||||||
|
count += len(g.Adjacency[k])
|
||||||
|
}
|
||||||
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
// get an array (slice) of all vertices in the graph
|
// get an array (slice) of all vertices in the graph
|
||||||
func (g *Graph) GetVertices() []*Vertex {
|
func (g *Graph) GetVertices() []*Vertex {
|
||||||
vertices := make([]*Vertex, 0)
|
var vertices []*Vertex
|
||||||
for k := range g.Adjacency {
|
for k := range g.Adjacency {
|
||||||
vertices = append(vertices, k)
|
vertices = append(vertices, k)
|
||||||
}
|
}
|
||||||
@@ -138,7 +207,6 @@ func (g *Graph) GetVertices() []*Vertex {
|
|||||||
// returns a channel of all vertices in the graph
|
// returns a channel of all vertices in the graph
|
||||||
func (g *Graph) GetVerticesChan() chan *Vertex {
|
func (g *Graph) GetVerticesChan() chan *Vertex {
|
||||||
ch := make(chan *Vertex)
|
ch := make(chan *Vertex)
|
||||||
// TODO: do you need to pass this through into the go routine?
|
|
||||||
go func(ch chan *Vertex) {
|
go func(ch chan *Vertex) {
|
||||||
for k := range g.Adjacency {
|
for k := range g.Adjacency {
|
||||||
ch <- k
|
ch <- k
|
||||||
@@ -153,7 +221,88 @@ func (g *Graph) String() string {
|
|||||||
return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges())
|
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!
|
// google/golang hackers apparently do not think contains should be a built-in!
|
||||||
func Contains(s []*Vertex, element *Vertex) bool {
|
func Contains(s []*Vertex, element *Vertex) bool {
|
||||||
for _, v := range s {
|
for _, v := range s {
|
||||||
@@ -164,20 +313,15 @@ func Contains(s []*Vertex, element *Vertex) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// return an array (slice) of all vertices that connect to vertex v
|
// return an array (slice) of all directed vertices to vertex v (??? -> v)
|
||||||
func (g *Graph) GraphEdges(vertex *Vertex) []*Vertex {
|
// ostimestamp should use this
|
||||||
|
func (g *Graph) IncomingGraphEdges(v *Vertex) []*Vertex {
|
||||||
// TODO: we might be able to implement this differently by reversing
|
// TODO: we might be able to implement this differently by reversing
|
||||||
// the Adjacency graph and then looping through it again...
|
// the Adjacency graph and then looping through it again...
|
||||||
s := make([]*Vertex, 0) // stack
|
var s []*Vertex
|
||||||
for w, _ := range g.Adjacency[vertex] { // forward paths
|
for k := range g.Adjacency { // reverse paths
|
||||||
//fmt.Printf("forward: %v -> %v\n", v.Name, w.Name)
|
for w := range g.Adjacency[k] {
|
||||||
s = append(s, w)
|
if w == v {
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
s = append(s, k)
|
s = append(s, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,32 +329,27 @@ func (g *Graph) GraphEdges(vertex *Vertex) []*Vertex {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// return an array (slice) of all directed vertices to vertex v
|
// return an array (slice) of all vertices that vertex v points to (v -> ???)
|
||||||
func (g *Graph) DirectedGraphEdges(vertex *Vertex) []*Vertex {
|
// poke should use this
|
||||||
// TODO: we might be able to implement this differently by reversing
|
func (g *Graph) OutgoingGraphEdges(v *Vertex) []*Vertex {
|
||||||
// the Adjacency graph and then looping through it again...
|
var s []*Vertex
|
||||||
s := make([]*Vertex, 0) // stack
|
for k := range g.Adjacency[v] { // forward paths
|
||||||
for w, _ := range g.Adjacency[vertex] { // forward paths
|
s = append(s, k)
|
||||||
//fmt.Printf("forward: %v -> %v\n", v.Name, w.Name)
|
|
||||||
s = append(s, w)
|
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// get timestamp of a vertex
|
// return an array (slice) of all vertices that connect to vertex v
|
||||||
func (v *Vertex) GetTimestamp() int64 {
|
func (g *Graph) GraphEdges(v *Vertex) []*Vertex {
|
||||||
return v.Timestamp
|
var s []*Vertex
|
||||||
}
|
s = append(s, g.IncomingGraphEdges(v)...)
|
||||||
|
s = append(s, g.OutgoingGraphEdges(v)...)
|
||||||
// update timestamp of a vertex
|
return s
|
||||||
func (v *Vertex) UpdateTimestamp() int64 {
|
|
||||||
v.Timestamp = time.Now().UnixNano() // update
|
|
||||||
return v.Timestamp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Graph) DFS(start *Vertex) []*Vertex {
|
func (g *Graph) DFS(start *Vertex) []*Vertex {
|
||||||
d := make([]*Vertex, 0) // discovered
|
var d []*Vertex // discovered
|
||||||
s := make([]*Vertex, 0) // stack
|
var s []*Vertex // stack
|
||||||
if _, exists := g.Adjacency[start]; !exists {
|
if _, exists := g.Adjacency[start]; !exists {
|
||||||
return nil // TODO: error
|
return nil // TODO: error
|
||||||
}
|
}
|
||||||
@@ -238,7 +377,7 @@ func (g *Graph) FilterGraph(name string, vertices []*Vertex) *Graph {
|
|||||||
for k1, x := range g.Adjacency {
|
for k1, x := range g.Adjacency {
|
||||||
for k2, e := range x {
|
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) {
|
if Contains(vertices, k1) || Contains(vertices, k2) {
|
||||||
newgraph.AddEdge(k1, k2, e)
|
newgraph.AddEdge(k1, k2, e)
|
||||||
}
|
}
|
||||||
@@ -254,7 +393,7 @@ func (g *Graph) GetDisconnectedGraphs() chan *Graph {
|
|||||||
ch := make(chan *Graph)
|
ch := make(chan *Graph)
|
||||||
go func() {
|
go func() {
|
||||||
var start *Vertex
|
var start *Vertex
|
||||||
d := make([]*Vertex, 0) // discovered
|
var d []*Vertex // discovered
|
||||||
c := g.NumVertices()
|
c := g.NumVertices()
|
||||||
for len(d) < c {
|
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
|
// if we've found all the elements, then we're done
|
||||||
// otherwise loop through to continue...
|
// otherwise loop through to continue...
|
||||||
}
|
}
|
||||||
|
|
||||||
close(ch)
|
close(ch)
|
||||||
}()
|
}()
|
||||||
return 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) {
|
func (v *Vertex) Value(key string) (string, bool) {
|
||||||
if value, exists := v.data[key]; exists {
|
if value, exists := v.data[key]; exists {
|
||||||
return value, true
|
return value, true
|
||||||
@@ -324,91 +542,93 @@ func HeisenbergCount(ch chan *Vertex) int {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vertex) Associate(t Type) {
|
// main kick to start the graph
|
||||||
v.Typedata = t
|
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 {
|
if !v.Type.IsWatching() { // if Watch() is not running...
|
||||||
g := v.GetGraph()
|
wg.Add(1)
|
||||||
for _, n := range g.DirectedGraphEdges(v) {
|
// must pass in value to avoid races...
|
||||||
if v.GetTimestamp() > n.GetTimestamp() {
|
// see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/
|
||||||
return false
|
go func(vv *Vertex) {
|
||||||
}
|
defer wg.Done()
|
||||||
|
vv.Type.Watch()
|
||||||
|
log.Printf("%v[%v]: Exited", vv.GetType(), vv.GetName())
|
||||||
|
}(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// poke the XXX children?
|
func (g *Graph) Pause() {
|
||||||
func (v *Vertex) Poke() {
|
t, _ := g.TopologicalSort()
|
||||||
g := v.GetGraph()
|
for _, v := range t { // squeeze out the events...
|
||||||
|
v.Type.SendEvent(eventPause, true, false)
|
||||||
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) Exit() {
|
func (g *Graph) Exit() {
|
||||||
// tell all the vertices to 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 (g *Graph) SetConvergedCallback(ctimeout int, converged chan bool) {
|
||||||
for v := range g.GetVerticesChan() {
|
for v := range g.GetVerticesChan() {
|
||||||
v.Exit()
|
v.Type.SetConvegedCallback(ctimeout, converged)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *Vertex) Exit() {
|
// in array function to test *vertices in a slice of *vertices
|
||||||
v.Events <- "exit"
|
func HasVertex(v *Vertex, haystack []*Vertex) bool {
|
||||||
|
for _, r := range haystack {
|
||||||
|
if v == r {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// main loop for each vertex
|
// reverse a list of vertices
|
||||||
// warning: this logic might be subtle and tricky.
|
func Reverse(vs []*Vertex) []*Vertex {
|
||||||
// be careful as it might not even be correct now!
|
//var out []*Vertex // XXX: golint suggests, but it fails testing
|
||||||
func (v *Vertex) Start() {
|
out := make([]*Vertex, 0) // empty list
|
||||||
log.Printf("Main->Vertex[%v]->Start()\n", v.Name)
|
l := len(vs)
|
||||||
|
for i := range vs {
|
||||||
//g := v.GetGraph()
|
out = append(out, vs[l-i-1])
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
275
pgraph_test.go
275
pgraph_test.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -31,25 +32,33 @@ func TestPgraphT1(t *testing.T) {
|
|||||||
t.Errorf("Should have 0 vertices instead of: %d.", i)
|
t.Errorf("Should have 0 vertices instead of: %d.", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
v1 := NewVertex("v1", "type")
|
if i := G.NumEdges(); i != 0 {
|
||||||
v2 := NewVertex("v2", "type")
|
t.Errorf("Should have 0 edges instead of: %d.", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
G.AddEdge(v1, v2, e1)
|
G.AddEdge(v1, v2, e1)
|
||||||
|
|
||||||
if i := G.NumVertices(); i != 2 {
|
if i := G.NumVertices(); i != 2 {
|
||||||
t.Errorf("Should have 2 vertices instead of: %d.", i)
|
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) {
|
func TestPgraphT2(t *testing.T) {
|
||||||
|
|
||||||
G := NewGraph("g2")
|
G := NewGraph("g2")
|
||||||
v1 := NewVertex("v1", "type")
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
v2 := NewVertex("v2", "type")
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
v3 := NewVertex("v3", "type")
|
v3 := NewVertex(NewNoopType("v3"))
|
||||||
v4 := NewVertex("v4", "type")
|
v4 := NewVertex(NewNoopType("v4"))
|
||||||
v5 := NewVertex("v5", "type")
|
v5 := NewVertex(NewNoopType("v5"))
|
||||||
v6 := NewVertex("v6", "type")
|
v6 := NewVertex(NewNoopType("v6"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
e2 := NewEdge("e2")
|
e2 := NewEdge("e2")
|
||||||
e3 := NewEdge("e3")
|
e3 := NewEdge("e3")
|
||||||
@@ -71,12 +80,12 @@ func TestPgraphT2(t *testing.T) {
|
|||||||
func TestPgraphT3(t *testing.T) {
|
func TestPgraphT3(t *testing.T) {
|
||||||
|
|
||||||
G := NewGraph("g3")
|
G := NewGraph("g3")
|
||||||
v1 := NewVertex("v1", "type")
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
v2 := NewVertex("v2", "type")
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
v3 := NewVertex("v3", "type")
|
v3 := NewVertex(NewNoopType("v3"))
|
||||||
v4 := NewVertex("v4", "type")
|
v4 := NewVertex(NewNoopType("v4"))
|
||||||
v5 := NewVertex("v5", "type")
|
v5 := NewVertex(NewNoopType("v5"))
|
||||||
v6 := NewVertex("v6", "type")
|
v6 := NewVertex(NewNoopType("v6"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
e2 := NewEdge("e2")
|
e2 := NewEdge("e2")
|
||||||
e3 := NewEdge("e3")
|
e3 := NewEdge("e3")
|
||||||
@@ -95,7 +104,7 @@ func TestPgraphT3(t *testing.T) {
|
|||||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||||
t.Errorf("Found: %v", out1)
|
t.Errorf("Found: %v", out1)
|
||||||
for _, v := range 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("Should have 3 vertices instead of: %d.", i)
|
||||||
t.Errorf("Found: %v", out1)
|
t.Errorf("Found: %v", out1)
|
||||||
for _, v := range 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) {
|
func TestPgraphT4(t *testing.T) {
|
||||||
|
|
||||||
G := NewGraph("g4")
|
G := NewGraph("g4")
|
||||||
v1 := NewVertex("v1", "type")
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
v2 := NewVertex("v2", "type")
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
v3 := NewVertex("v3", "type")
|
v3 := NewVertex(NewNoopType("v3"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
e2 := NewEdge("e2")
|
e2 := NewEdge("e2")
|
||||||
e3 := NewEdge("e3")
|
e3 := NewEdge("e3")
|
||||||
@@ -127,19 +136,19 @@ func TestPgraphT4(t *testing.T) {
|
|||||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||||
t.Errorf("Found: %v", out)
|
t.Errorf("Found: %v", out)
|
||||||
for _, v := range out {
|
for _, v := range out {
|
||||||
t.Errorf("Value: %v", v.Name)
|
t.Errorf("Value: %v", v.GetName())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPgraphT5(t *testing.T) {
|
func TestPgraphT5(t *testing.T) {
|
||||||
G := NewGraph("g5")
|
G := NewGraph("g5")
|
||||||
v1 := NewVertex("v1", "type")
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
v2 := NewVertex("v2", "type")
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
v3 := NewVertex("v3", "type")
|
v3 := NewVertex(NewNoopType("v3"))
|
||||||
v4 := NewVertex("v4", "type")
|
v4 := NewVertex(NewNoopType("v4"))
|
||||||
v5 := NewVertex("v5", "type")
|
v5 := NewVertex(NewNoopType("v5"))
|
||||||
v6 := NewVertex("v6", "type")
|
v6 := NewVertex(NewNoopType("v6"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
e2 := NewEdge("e2")
|
e2 := NewEdge("e2")
|
||||||
e3 := NewEdge("e3")
|
e3 := NewEdge("e3")
|
||||||
@@ -159,17 +168,16 @@ func TestPgraphT5(t *testing.T) {
|
|||||||
if i := out.NumVertices(); i != 3 {
|
if i := out.NumVertices(); i != 3 {
|
||||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPgraphT6(t *testing.T) {
|
func TestPgraphT6(t *testing.T) {
|
||||||
G := NewGraph("g6")
|
G := NewGraph("g6")
|
||||||
v1 := NewVertex("v1", "type")
|
v1 := NewVertex(NewNoopType("v1"))
|
||||||
v2 := NewVertex("v2", "type")
|
v2 := NewVertex(NewNoopType("v2"))
|
||||||
v3 := NewVertex("v3", "type")
|
v3 := NewVertex(NewNoopType("v3"))
|
||||||
v4 := NewVertex("v4", "type")
|
v4 := NewVertex(NewNoopType("v4"))
|
||||||
v5 := NewVertex("v5", "type")
|
v5 := NewVertex(NewNoopType("v5"))
|
||||||
v6 := NewVertex("v6", "type")
|
v6 := NewVertex(NewNoopType("v6"))
|
||||||
e1 := NewEdge("e1")
|
e1 := NewEdge("e1")
|
||||||
e2 := NewEdge("e2")
|
e2 := NewEdge("e2")
|
||||||
e3 := NewEdge("e3")
|
e3 := NewEdge("e3")
|
||||||
@@ -197,5 +205,204 @@ func TestPgraphT6(t *testing.T) {
|
|||||||
if i := HeisenbergGraphCount(graphs); i != 2 {
|
if i := HeisenbergGraphCount(graphs); i != 2 {
|
||||||
t.Errorf("Should have 2 graphs instead of: %d.", i)
|
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.")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
139
service.go
139
service.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/go-uuid/uuid"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
systemd "github.com/coreos/go-systemd/dbus" // change namespace
|
systemd "github.com/coreos/go-systemd/dbus" // change namespace
|
||||||
"github.com/coreos/go-systemd/util"
|
"github.com/coreos/go-systemd/util"
|
||||||
@@ -29,29 +28,37 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ServiceType struct {
|
type ServiceType struct {
|
||||||
uuid string
|
BaseType `yaml:",inline"`
|
||||||
Type string // always "service"
|
State string `yaml:"state"` // state: running, stopped
|
||||||
Name string // name variable
|
Startup string `yaml:"startup"` // enabled, disabled, undefined
|
||||||
Events chan string // FIXME: eventually a struct for the event?
|
|
||||||
State string // state: running, stopped
|
|
||||||
Startup string // enabled, disabled, undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServiceType(name, state, startup string) *ServiceType {
|
func NewServiceType(name, state, startup string) *ServiceType {
|
||||||
return &ServiceType{
|
return &ServiceType{
|
||||||
uuid: uuid.New(),
|
BaseType: BaseType{
|
||||||
Type: "service",
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Events: make(chan string, 1), // XXX: chan size?
|
events: make(chan Event),
|
||||||
|
vertex: nil,
|
||||||
|
},
|
||||||
State: state,
|
State: state,
|
||||||
Startup: startup,
|
Startup: startup,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service watcher
|
func (obj *ServiceType) GetType() string {
|
||||||
func (obj ServiceType) Watch(v *Vertex) {
|
return "Service"
|
||||||
// obj.Name: service name
|
}
|
||||||
|
|
||||||
|
// 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() {
|
if !util.IsRunningSystemd() {
|
||||||
log.Fatal("Systemd is not running.")
|
log.Fatal("Systemd is not running.")
|
||||||
}
|
}
|
||||||
@@ -64,7 +71,7 @@ func (obj ServiceType) Watch(v *Vertex) {
|
|||||||
|
|
||||||
bus, err := dbus.SystemBus()
|
bus, err := dbus.SystemBus()
|
||||||
if err != nil {
|
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?
|
// XXX: will this detect new units?
|
||||||
@@ -75,6 +82,8 @@ func (obj ServiceType) Watch(v *Vertex) {
|
|||||||
|
|
||||||
var service = fmt.Sprintf("%v.service", obj.Name) // systemd name
|
var service = fmt.Sprintf("%v.service", obj.Name) // systemd name
|
||||||
var send = false // send event?
|
var send = false // send event?
|
||||||
|
var exit = false
|
||||||
|
var dirty = false
|
||||||
var invalid = false // does the service exist or not?
|
var invalid = false // does the service exist or not?
|
||||||
var previous bool // previous invalid value
|
var previous bool // previous invalid value
|
||||||
set := conn.NewSubscriptionSet() // no error should be returned
|
set := conn.NewSubscriptionSet() // no error should be returned
|
||||||
@@ -91,40 +100,50 @@ func (obj ServiceType) Watch(v *Vertex) {
|
|||||||
// firstly, does service even exist or not?
|
// firstly, does service even exist or not?
|
||||||
loadstate, err := conn.GetUnitProperty(service, "LoadState")
|
loadstate, err := conn.GetUnitProperty(service, "LoadState")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get property: %v\n", err)
|
log.Printf("Failed to get property: %v", err)
|
||||||
invalid = true
|
invalid = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !invalid {
|
if !invalid {
|
||||||
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
|
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
|
||||||
if notFound { // XXX: in the loop we'll handle changes better...
|
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 ?
|
invalid = true // XXX ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if previous != invalid { // if invalid changed, send signal
|
if previous != invalid { // if invalid changed, send signal
|
||||||
send = true
|
send = true
|
||||||
|
dirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if invalid {
|
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 {
|
if activeSet {
|
||||||
activeSet = false
|
activeSet = false
|
||||||
set.Remove(service) // no return value should ever occur
|
set.Remove(service) // no return value should ever occur
|
||||||
}
|
}
|
||||||
|
|
||||||
|
obj.SetState(typeWatching) // reset
|
||||||
select {
|
select {
|
||||||
case _ = <-buschan: // XXX wait for new units event to unstick
|
case _ = <-buschan: // XXX wait for new units event to unstick
|
||||||
|
obj.SetConvergedState(typeConvergedNil)
|
||||||
// loop so that we can see the changed invalid signal
|
// 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:
|
case event := <-obj.events:
|
||||||
if exit == "exit" {
|
obj.SetConvergedState(typeConvergedNil)
|
||||||
return
|
if exit, send = obj.ReadEvent(&event); exit {
|
||||||
} else {
|
return // exit
|
||||||
log.Fatal("Unknown event: %v\n", exit)
|
|
||||||
}
|
}
|
||||||
|
if event.GetActivity() {
|
||||||
|
dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ = <-TimeAfterOrBlock(obj.ctimeout):
|
||||||
|
obj.SetConvergedState(typeConvergedTimeout)
|
||||||
|
obj.converged <- true
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if !activeSet {
|
if !activeSet {
|
||||||
@@ -132,55 +151,62 @@ func (obj ServiceType) Watch(v *Vertex) {
|
|||||||
set.Add(service) // no return value should ever occur
|
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 {
|
select {
|
||||||
case event := <-subChannel:
|
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...
|
// NOTE: the value returned is a map for some reason...
|
||||||
if event[service] != nil {
|
if event[service] != nil {
|
||||||
// event[service].ActiveState is not nil
|
// event[service].ActiveState is not nil
|
||||||
if event[service].ActiveState == "active" {
|
if event[service].ActiveState == "active" {
|
||||||
log.Printf("Service[%v]->Started()\n", service)
|
log.Printf("Service[%v]->Started()", service)
|
||||||
} else if event[service].ActiveState == "inactive" {
|
} else if event[service].ActiveState == "inactive" {
|
||||||
log.Printf("Service[%v]->Stopped!()\n", service)
|
log.Printf("Service[%v]->Stopped!()", service)
|
||||||
} else {
|
} else {
|
||||||
log.Fatal("Unknown service state: ", event[service].ActiveState)
|
log.Fatal("Unknown service state: ", event[service].ActiveState)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// service stopped (and ActiveState is nil...)
|
// service stopped (and ActiveState is nil...)
|
||||||
log.Printf("Service[%v]->Stopped\n", service)
|
log.Printf("Service[%v]->Stopped", service)
|
||||||
}
|
}
|
||||||
send = true
|
send = true
|
||||||
|
dirty = true
|
||||||
|
|
||||||
case err := <-subErrors:
|
case err := <-subErrors:
|
||||||
|
obj.SetConvergedState(typeConvergedNil) // XXX ?
|
||||||
log.Println("error:", err)
|
log.Println("error:", err)
|
||||||
log.Fatal(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:
|
case event := <-obj.events:
|
||||||
if exit == "exit" {
|
obj.SetConvergedState(typeConvergedNil)
|
||||||
return
|
if exit, send = obj.ReadEvent(&event); exit {
|
||||||
} else {
|
return // exit
|
||||||
log.Fatal("Unknown event: %v\n", exit)
|
}
|
||||||
|
if event.GetActivity() {
|
||||||
|
dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if send {
|
if send {
|
||||||
send = false
|
send = false
|
||||||
//log.Println("Sending event!")
|
if dirty {
|
||||||
v.Events <- fmt.Sprintf("service(%v): %v", obj.Name, "event!") // FIXME: use struct
|
dirty = false
|
||||||
|
obj.isStateOK = false // something made state dirty
|
||||||
}
|
}
|
||||||
|
Process(obj) // XXX: rename this function
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj ServiceType) Exit() bool {
|
func (obj *ServiceType) StateOK() bool {
|
||||||
obj.Events <- "exit"
|
if obj.isStateOK { // cache the state
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj ServiceType) StateOK() bool {
|
|
||||||
|
|
||||||
if !util.IsRunningSystemd() {
|
if !util.IsRunningSystemd() {
|
||||||
log.Fatal("Systemd is not running.")
|
log.Fatal("Systemd is not running.")
|
||||||
@@ -196,14 +222,14 @@ func (obj ServiceType) StateOK() bool {
|
|||||||
|
|
||||||
loadstate, err := conn.GetUnitProperty(service, "LoadState")
|
loadstate, err := conn.GetUnitProperty(service, "LoadState")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get load state: %v\n", err)
|
log.Printf("Failed to get load state: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: we have to compare variants with other variants, they are really strings...
|
// NOTE: we have to compare variants with other variants, they are really strings...
|
||||||
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
|
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
|
||||||
if notFound {
|
if notFound {
|
||||||
log.Printf("Failed to find service: %v\n", service)
|
log.Printf("Failed to find service: %v", service)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,8 +258,8 @@ func (obj ServiceType) StateOK() bool {
|
|||||||
return true // all is good, no state change needed
|
return true // all is good, no state change needed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj ServiceType) Apply() bool {
|
func (obj *ServiceType) Apply() bool {
|
||||||
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
|
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
|
||||||
|
|
||||||
if !util.IsRunningSystemd() {
|
if !util.IsRunningSystemd() {
|
||||||
log.Fatal("Systemd is not running.")
|
log.Fatal("Systemd is not running.")
|
||||||
@@ -256,7 +282,7 @@ func (obj ServiceType) Apply() bool {
|
|||||||
err = nil
|
err = nil
|
||||||
}
|
}
|
||||||
if 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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,3 +318,22 @@ func (obj ServiceType) Apply() bool {
|
|||||||
|
|
||||||
return true
|
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
11
tag.sh
Executable 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
23
test.sh
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
# test suite...
|
# test suite...
|
||||||
echo running test.sh
|
echo running test.sh
|
||||||
|
echo "ENV:"
|
||||||
|
env
|
||||||
|
|
||||||
# ensure there is no trailing whitespace or other whitespace errors
|
# ensure there is no trailing whitespace or other whitespace errors
|
||||||
git diff-tree --check $(git hash-object -t tree /dev/null) HEAD
|
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
|
# ensure entries to authors file are sorted
|
||||||
start=$(($(grep -n '^[[:space:]]*$' AUTHORS | awk -F ':' '{print $1}' | head -1) + 1))
|
start=$(($(grep -n '^[[:space:]]*$' AUTHORS | awk -F ':' '{print $1}' | head -1) + 1))
|
||||||
diff <(tail -n +$start AUTHORS | sort) <(tail -n +$start AUTHORS)
|
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
52
test/omv/helloworld.yaml
Normal 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
1
test/shell/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
mgmt
|
||||||
12
test/shell/etcd.sh
Normal file
12
test/shell/etcd.sh
Normal 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
14
test/shell/t1.sh
Executable 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
14
test/shell/t2.sh
Executable 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
41
test/shell/t2.yaml
Normal 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
28
test/shell/t3-a.yaml
Normal 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
28
test/shell/t3-b.yaml
Normal 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
28
test/shell/t3-c.yaml
Normal 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
67
test/shell/t3.sh
Executable 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
10
test/shell/t4.sh
Executable 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
77
test/shell/t4.yaml
Normal 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
10
test/shell/t5.sh
Executable 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
128
test/shell/t5.yaml
Normal 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
6
test/shell/wait.sh
Normal 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
31
test/test-bashfmt.sh
Executable 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
|
||||||
@@ -9,7 +9,7 @@ ROOT=$(dirname "${BASH_SOURCE}")/..
|
|||||||
|
|
||||||
GO_VERSION=($(go version))
|
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."
|
echo "Unknown go version '${GO_VERSION}', skipping gofmt."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -17,19 +17,14 @@ fi
|
|||||||
cd "${ROOT}"
|
cd "${ROOT}"
|
||||||
|
|
||||||
find_files() {
|
find_files() {
|
||||||
find . -not \( \
|
git ls-files | grep '\.go$'
|
||||||
\( \
|
|
||||||
-wholename './old' \
|
|
||||||
-o -wholename './tmp' \
|
|
||||||
\) -prune \
|
|
||||||
\) -name '*.go'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
GOFMT="gofmt" # we prefer to not use the -s flag, which is pretty annoying...
|
GOFMT="gofmt" # we prefer to not use the -s flag, which is pretty annoying...
|
||||||
bad_files=$(find_files | xargs $GOFMT -l)
|
bad_files=$(find_files | xargs $GOFMT -l)
|
||||||
if [[ -n "${bad_files}" ]]; then
|
if [[ -n "${bad_files}" ]]; then
|
||||||
echo 'FAIL'
|
echo 'FAIL'
|
||||||
echo 'The following files are not properly formatted:'
|
echo 'The following golang files are not properly formatted:'
|
||||||
echo "${bad_files}"
|
echo "${bad_files}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
11
test/test-omv.sh
Executable file
11
test/test-omv.sh
Executable 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
39
test/test-reproducible.sh
Executable 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
73
test/test-shell.sh
Executable 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
|
||||||
@@ -5,21 +5,33 @@ set -o errexit
|
|||||||
set -o nounset
|
set -o nounset
|
||||||
set -o pipefail
|
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}")/..
|
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}"
|
cd "${ROOT}"
|
||||||
|
|
||||||
find_files() {
|
find_files() {
|
||||||
find . -not \( \
|
git ls-files | grep '\.yaml$'
|
||||||
\( \
|
|
||||||
-wholename './old' \
|
|
||||||
-o -wholename './tmp' \
|
|
||||||
\) -prune \
|
|
||||||
\) -name '*.yaml'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bad_files=$(
|
bad_files=$(
|
||||||
for i in $(find_files); do
|
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"
|
echo "$i"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
@@ -27,7 +39,7 @@ bad_files=$(
|
|||||||
|
|
||||||
if [[ -n "${bad_files}" ]]; then
|
if [[ -n "${bad_files}" ]]; then
|
||||||
echo 'FAIL'
|
echo 'FAIL'
|
||||||
echo 'The following files are not properly formatted:'
|
echo 'The following yaml files are not properly formatted:'
|
||||||
echo "${bad_files}"
|
echo "${bad_files}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
388
types.go
388
types.go
@@ -1,5 +1,5 @@
|
|||||||
// Mgmt
|
// 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
|
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
@@ -18,56 +18,390 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"code.google.com/p/go-uuid/uuid"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"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 {
|
type Type interface {
|
||||||
//Name() string
|
Init()
|
||||||
Watch(*Vertex)
|
GetName() string // can't be named "Name()" because of struct field
|
||||||
|
GetType() string
|
||||||
|
Watch()
|
||||||
StateOK() bool // TODO: can we rename this to something better?
|
StateOK() bool // TODO: can we rename this to something better?
|
||||||
Apply() bool
|
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 {
|
type NoopType struct {
|
||||||
uuid string
|
BaseType `yaml:",inline"`
|
||||||
Type string // always "noop"
|
Comment string `yaml:"comment"` // extra field for example purposes
|
||||||
Name string // name variable
|
|
||||||
Events chan string // FIXME: eventually a struct for the event?
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNoopType(name string) *NoopType {
|
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{
|
return &NoopType{
|
||||||
uuid: uuid.New(),
|
BaseType: BaseType{
|
||||||
Type: "noop",
|
|
||||||
Name: name,
|
Name: name,
|
||||||
Events: make(chan string, 1), // XXX: chan size?
|
events: make(chan Event), // unbuffered chan size to avoid stale events
|
||||||
|
vertex: nil,
|
||||||
|
},
|
||||||
|
Comment: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj NoopType) Watch(v *Vertex) {
|
// initialize structures like channels if created without New constructor
|
||||||
select {
|
func (obj *BaseType) Init() {
|
||||||
case exit := <-obj.Events:
|
obj.events = make(chan Event)
|
||||||
if exit == "exit" {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
log.Fatal("Unknown event: %v\n", exit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj NoopType) Exit() bool {
|
// this method gets used by all the types, if we have one of (obj NoopType) it would get overridden in that case!
|
||||||
obj.Events <- "exit"
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj NoopType) StateOK() bool {
|
// 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 {
|
||||||
|
if DEBUG {
|
||||||
|
log.Printf("%v[%v]: Poke: %v[%v]: Skipped!", v.GetType(), v.GetName(), n.GetType(), n.GetName())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
return true // never needs updating
|
||||||
}
|
}
|
||||||
|
|
||||||
func (obj NoopType) Apply() bool {
|
func (obj *NoopType) Apply() bool {
|
||||||
fmt.Printf("Apply->%v[%v]\n", obj.Type, obj.Name)
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user