61 Commits

Author SHA1 Message Date
James Shubin
0ec00fe57f make: Improve release pipeline
Hopefully this makes releases a little better for users.
In particular, this avoids listing old build artifacts in the SHA256SUMS
files when we make new releases, and users can now download them
directly.

Now to make a release you run: `make tag && make release`.
After the first make session ends, you'll have a new tag released
publicly, and then during the second make session, the release target
will notice this new tag, build some assets, and upload them!
2018-11-30 19:08:53 -05:00
Jonathan Gold
80931e1cb4 make: Release pipeline
This commit adds new make targets for rpm, deb, and pacman packages.
It also adds a phony target that uploads tarballs of the packages,
along with their signed (and unsigned) checksums to the github release
page. Once the current commit is tagged as a release, run `make release`
to build the packages and upload them to github.
2018-11-30 04:53:51 -05:00
James Shubin
cc02e96a13 engine: resources: Add nodocker build tag
Make it easy to disable building docker which is enormous.
2018-11-29 08:22:05 -05:00
Jonathan Gold
51ec91dd16 engine: resources: docker: Add a docker container resource 2018-11-29 08:14:07 -05:00
James Shubin
916a92c3d8 vendor: Add vendored docker modules with out of tree fix
The docker project absurdly *copies* all of the dependencies into the
vendor/ directory instead of using git submodules or avoiding
unnecessary vendoring entirely. We manually remove these changes until
they learn to use tools how they're intended.

As an aside, we recommend using a more intelligent, modern tool like
systemd-nspawn instead.
2018-11-29 08:14:07 -05:00
James Shubin
5431bfdc29 test: Improve commit message tests 2018-11-24 04:42:50 -05:00
Jonathan Gold
43b5b4f5a4 build: Add rubygems to make deps target
cffdb06 adds a linter for markdown which requires rubygems.
This commit adds the dependency to the make target.
2018-10-30 17:16:50 -04:00
James Shubin
f342e06ef0 readme: Add Liberapay link to README 2018-06-21 19:21:41 -04:00
James Shubin
81bb87f4cd test: Add a test to ensure the parser doesn't have any conflicts
Our grammar shouldn't be ambiguous, and it makes sense to test this.
2018-06-18 16:06:23 -04:00
James Shubin
c4b97fadcc lang: Update map type definition to include a prefix
It turns out that some planned additions to the parser make it so that
the map type definition can be ambiguous. As a result, this patch
updates the definition so that the map definition is not confused with
an open curly bracket anywhere.

Thanks to pestle and stbenjamin for their help understanding yacc!
2018-06-18 16:06:23 -04:00
James Shubin
05f6ba7297 lang: Add partial recursive support/detection to class
This adds the additional bits onto the class/include statements to
support or detect class recursion. It's not currently supported, but
I figured I'd commit the detection code as a variant of the recursion
implementation, since I think this is correct, and it was a bit tricky
for me to get it right.
2018-06-17 17:35:34 -04:00
James Shubin
c62b8a5d4f lang: Add class and include statements
This adds support for the class definition statement and the include
statement which produces the output from the corresponding class.

The classes in this language support optional input parameters.

In contrast with other tools, the class is *not* a singleton, although
it can be used as one. Using include with equivalent input parameters
will cause the class to act as a singleton, although it can also be used
to produce distinct output.

The output produced by including a class is actually a list of
statements (a prog) which is ultimately a list of resources and edges.
This is different from functions which produces values.
2018-06-17 17:29:44 -04:00
James Shubin
83dab30ecf lang: Simplify bind stmt collection in the prog stmt
This cleans up the code to be more consistent with the other
improvements in this area.
2018-06-12 17:44:42 -04:00
James Shubin
24b08a332d pgraph: Handle empty graphs when merging two
In case we choose to add an empty (nil) graph, handle it safely. This
could allow us to return nil in a lang/structs Graph method without
issue.
2018-06-12 17:44:36 -04:00
James Shubin
70ccb3022a lang: Simplify struct interpolation
Cleaner code, nothing fancy.
2018-06-12 17:40:57 -04:00
James Shubin
8019b90b8a lang: Don't add identical resources to graph
This means that it's legal to produce two compatible (usually identical)
resources without a compile error and without causing two of them to get
run. It's too bad puppet never got this right.

It's probably worth checking if this could be done for edges too, and if
the logic can be contained in the engine and not in the frontend.
2018-06-12 17:40:57 -04:00
James Shubin
5f12ff6178 lang: Add indentation test to parser
This adds a test case to catch some common typos.
2018-06-12 17:40:18 -04:00
James Shubin
6e20e48489 lang: Simplify graph function for edge half in parser 2018-06-12 17:40:18 -04:00
James Shubin
f29a72235c lang: funcs: Registered functions map should be private
Make the map is private so that the public methods must be used to
access it.
2018-06-12 17:40:18 -04:00
James Shubin
e25d499eeb lang: Add edges to StmtProg output
I think I forgot to add these previously, and I think they should be
part of the output now.
2018-06-12 17:40:18 -04:00
James Shubin
9cae339546 lang: Error parser if SetType fails to avoid a panic
Turns out we can actually cause the parser to error instead of needing
to panic. It definitely seems to work, and is better than the panic. The
only awkward thing is how this plumbing works in yacc world. If anyone
knows why this is wrong, please let me know. Reading the generated code
seems to imply that this is correct.
2018-05-22 20:02:50 -04:00
James Shubin
a049af6262 engine: resources: print: Add missing Recvable trait
We we're receiving values, but we forgot to list the trait. This caused
an intentional engine panic, but is easily fixed :)
2018-05-22 19:32:40 -04:00
Jonathan Gold
a402f50f9b docs: Update url for AWS EC2 blog post 2018-05-19 22:05:12 -04:00
Jonathan Gold
9f89ea9be6 docs: Add netlink post to on-the-web.md 2018-05-19 22:05:12 -04:00
phaer
e538aacf9d vagrant: Fix example path in motd 2018-05-19 09:21:14 +02:00
phaer
968c609697 vagrant: Add gem package 2018-05-19 09:21:06 +02:00
phaer
c11cfa0a62 vagrant: Bump to fedora 28 2018-05-19 09:20:51 +02:00
Jonathan Gold
074f4677d5 build: Fix ldflags pattern for 1.10
Prior to go 1.10 ldflags would apply to all packages by default.
As of go 1.10 it is necessary to specify the package for the
flags to apply. This patch checks the go version, and formats
the build command accordingly.
2018-05-11 16:17:24 -04:00
James Shubin
9ea5c03371 travis: Enable apt updates on builds
This used to happen by default, and travis changed the default.
2018-05-09 13:46:04 -04:00
James Shubin
22c0ff3cf5 test: Improve golang tests with root and disabling cache
This allows golang tests to be marked as root or !root using build tags.
The matching tests are then run as expected using our test runner.

This also disables test caching which is unfriendly to repeated test
running and is an absurd golang default to add.

Lastly this hooks up the testing verbose flag to tests that accept a
debug variable.

These tests aren't enabled on travis yet because of how it installs
golang.
2018-05-09 13:44:01 -04:00
James Shubin
3ced981d28 engine: test: Pass in the go test verbose flag
This hooks up our debug variable to the go test verbose flag.
2018-05-09 12:11:35 -04:00
Jonathan Gold
299080f590 engine: DBus cleanup 2018-05-07 15:57:17 -04:00
James Shubin
a407771eaf test: Catch naked returns and check for canonically named imports
This catches scenarios where we forgot to prefix the error with return.
One of our contributors occasionally made this typo, and since core go
vet didn't (surprisingly) catch it, we should add a test!

It also adds a simple check for import naming aliases. Expanding this
test to add other cases and check for differently named values might
make sense.
2018-05-06 15:18:46 -04:00
Jonathan Gold
d26a6de759 engine: resources: mount: Add a mount resource 2018-05-04 15:53:05 -04:00
Jonathan Gold
9baad56197 util: Move dbus AddMatch const to util package 2018-05-04 15:46:14 -04:00
James Shubin
a589e2ecf3 docs, test: Remove old reference to resources package
Forgot to change this previously. Also updated the resources list in the
documentation.
2018-05-02 15:28:15 -04:00
Jonathan Gold
d7029871b1 engine: resources: nspawn: Remove godbus channel buffer
https://github.com/godbus/dbus/issues/94 is fixed with
https://github.com/godbus/dbus/pull/105, so the
buffered channel is no longer necessary.
2018-05-01 12:19:34 -04:00
Alan Jenkins
b80a505be5 engine: resources: packagekit: Add Arch mapping 'any' for Arch Linux compatibility
Arch Linux uses the mapping architecture name 'any'. This mapping was
missing from mgmt resulting in an error stating that arch 'any' did not
exist. Adding this mapping allows successful installation of packages
under Arch Linux.
2018-04-30 07:28:58 +01:00
James Shubin
412a25462e test: Improve commit message test
We can classify better now that we have the new engine.
2018-04-21 19:29:26 -04:00
James Shubin
9a8408a092 engine: Small fixes 2018-04-20 21:11:32 -04:00
James Shubin
86a9181e9b puppet: Clean up the GAPI and remove log package
This uses the proper facilities which makes things a bit more uniform.
2018-04-19 01:56:31 -04:00
James Shubin
9969286224 engine: Resources package rewrite
This giant patch makes some much needed improvements to the code base.

* The engine has been rewritten and lives within engine/graph/
* All of the common interfaces and code now live in engine/
* All of the resources are in one package called engine/resources/
* The Res API can use different "traits" from engine/traits/
* The Res API has been simplified to hide many of the old internals
* The Watch & Process loops were previously inverted, but is now fixed
* The likelihood of package cycles has been reduced drastically
* And much, much more...

Unfortunately, some code had to be temporarily removed. The remote code
had to be taken out, as did the prometheus code. We hope to have these
back in new forms as soon as possible.
2018-04-19 01:10:58 -04:00
James Shubin
ef49aa7e08 lang: Don't race with a ^C to the obj.lang calls
If we trigger a close, we must not run the LangClose before we've exited
from the loop, because that loop could race and run code which depends
on LangClose not having run first. So run the loop shutdown, then let
the wait group expire, before shutting down the lang.
2018-04-16 08:38:22 -04:00
James Shubin
acdb497b80 etcd: Pull in default URLs from upstream
This depends on https://github.com/coreos/etcd/pull/6837
2018-04-16 08:38:22 -04:00
James Shubin
4d8faeb826 lib, yamlgraph: Remove old yamlgraph GAPI frontend
I should have removed this a long time ago, but didn't. Now it's done.
The new v2 frontend is loosing the v2 name and just replacing v1.
2018-04-16 08:38:22 -04:00
James Shubin
6e0dfdb16f lib: Remove hcl GAPI frontend
This is currently unmaintained and the normal mcl language exists which
is preferable to this. As a result, I'm removing this for now to make an
upcoming refactor easier. We can add it back easily if someone has
interest.
2018-04-16 08:38:22 -04:00
James Shubin
754480a9b6 readme: Add patreon link to README file 2018-04-16 08:37:49 -04:00
jesus m. rodriguez
15681ddca9 build: Add help to main Makefile 2018-04-08 23:09:47 -04:00
Jonathan Gold
3c8d424a43 util: Rename SortedStrSliceCompare and move to util package 2018-03-29 00:55:18 -04:00
jonathangold
7d7eb3d1cd resources: net: Add net resource
This patch adds a net resource for managing nework interfaces, based
around netlink.
2018-03-27 17:46:00 -04:00
James Shubin
8500339ba6 lang: Add mutex around Expr String/Value/SetValue calls
The golang race detector complains about some unimportant races, and as
a result, this patch adds some mutexes to prevent these test failures.
We actually lock more than necessary, because a more accurate version
would be more time consuming to implement. Secondarily, it's likely that
in the future we replace this function graph algorithm with something
that is guaranteed to be glitch-free and supports back pressure.
2018-03-27 15:30:59 -04:00
James Shubin
06ee05026b lang: funcs: Don't race when building an initial graph
I noticed a very intermittent test failure where interpret would end up
running, but *fail* because a value wasn't present. This should never
happen, because the function engine is designed to only call interpret
when there has been at least one value produced for every node in the
AST. So what is the bug that would produce:

interpret error: could not interpret: func value does not yet exist

About 20 minutes ago while I was getting to bed, it occurred to me where
to look! Out of bed and to the laptop, and after briefly reminding
myself of the code, I think I've found the issue.

What I think was happening, was that an AST node would produce a value,
and send a message on the aggregate channel. This channel is monitored,
and every time it receives a message, it checks to ensure that all the
values now exist before producing a message for interpret to run.
However, this AST node was not the final one to be produced, but before
the message was read by the aggregate channel, the last remaining AST
node ran and set it's "loaded" state to `true`, but *before* its value
was made available for the aggregate channel to read. That channel then
occasionally won the race and tried to access a value before it existed,
thus causing out intermittent bug.

At least I think that's what was going on. Hopefully this patch fixes
this, if not, then there's another bug hiding too! And of course, this
entire function engine could do with some proper analysis from someone
familiar with glitches, back pressure, and FRP parallelism.

One particular note was that I used my brain, not some fancy debugging
tool to find this. Maybe skilled debuggers can fork lift their tools
onto this type of problem, but I haven't those skills!

¯\_(ツ)_/¯
2018-03-15 23:22:21 -04:00
James Shubin
ddefb4e987 integration: Log the instance output
This adds logging so that you can dig deeper into crashes or issues.
2018-03-13 06:38:21 -04:00
James Shubin
62d1fc7ed3 test, integration: Add cluster primitives to integration framework
This further extends the integration framework to add some simple
primitives for building clusters. More complex primitives and patterns
can be added in the future, but this should serve the general cases.
2018-03-13 06:38:21 -04:00
James Shubin
f3b99b3940 test, integration: Add an integration test framework
This adds an initial implementation of an integration test framework for
writing more complicated tests. In particular this also makes some small
additions to the mgmt core so that testing is easier.
2018-03-13 06:38:21 -04:00
Lauri Ojansivu
97c11c18d0 resources: svc: Add activating state
There seems to be a "activating" state that some services can reach.
Related #369
2018-03-10 15:27:07 +02:00
James Shubin
93a909551f recwatch: Remove the ConfigWatch functionality
This is some now dead code which was buggy and badly written. Time to
get rid of unnecessary technical debt so that we can move forward!
2018-03-09 22:26:10 -05:00
James Shubin
ea52eb78d9 lib: Remove remote execution from core
I have an improved design for remote execution as a resource. Since I
need to get rid of some technical debt to clean up the resource API, and
this main loop, a good first step is to remote it's invocation. It will
be coming back as a resource as soon as possible!
2018-03-09 17:07:58 -05:00
James Shubin
fdd698dade resources: svc: Add deactivating state
There seems to be a "deactivating" state that some services can reach.
Add this case, and switch the panic to an error.
2018-03-09 17:04:30 -05:00
James Shubin
173ccf6861 pgraph: Don't panic on new or nil graphs
This adds a bit of flexibility so that we can still run a topological
sort on a nil graph.
2018-03-05 01:58:43 -05:00
James Shubin
a5c3db6303 lang: Misc fixes for typos and grammar 2018-02-28 00:35:22 -05:00
220 changed files with 15058 additions and 10306 deletions

2
.gitignore vendored
View File

@@ -13,4 +13,4 @@ mgmt.static
build/mgmt-* build/mgmt-*
mgmt.iml mgmt.iml
rpmbuild/ rpmbuild/
*.deb releases/

9
.gitmodules vendored
View File

@@ -22,3 +22,12 @@
[submodule "vendor/github.com/ugorji/go"] [submodule "vendor/github.com/ugorji/go"]
path = vendor/github.com/ugorji/go path = vendor/github.com/ugorji/go
url = https://github.com/ugorji/go url = https://github.com/ugorji/go
[submodule "vendor/github.com/purpleidea/docker"]
path = vendor/github.com/docker/docker
url = https://github.com/purpleidea/docker
[submodule "vendor/github.com/purpleidea/distribution"]
path = vendor/github.com/docker/distribution
url = https://github.com/purpleidea/distribution
[submodule "vendor/github.com/purpleidea/go-connections"]
path = vendor/github.com/docker/go-connections
url = https://github.com/docker/go-connections

View File

@@ -8,6 +8,9 @@ go:
go_import_path: github.com/purpleidea/mgmt go_import_path: github.com/purpleidea/mgmt
sudo: true sudo: true
dist: trusty dist: trusty
# travis requires that you update manually, and provides this key to trigger it
apt:
update: true
before_install: before_install:
# as per a number of comments online, this might mitigate some flaky fails... # as per a number of comments online, this might mitigate some flaky fails...
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6; fi - if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6; fi

124
Makefile
View File

@@ -16,7 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
SHELL = /usr/bin/env bash SHELL = /usr/bin/env bash
.PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr .PHONY: all art cleanart version program lang path deps run race bindata generate build build-debug crossbuild clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr tag release
.SILENT: clean bindata .SILENT: clean bindata
# a large amount of output from this `find`, can cause `make` to be much slower! # a large amount of output from this `find`, can cause `make` to be much slower!
@@ -25,6 +25,7 @@ GO_FILES := $(shell find * -name '*.go' -not -path 'old/*' -not -path 'tmp/*')
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)) SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)) VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-") PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
PKGNAME := $(shell go list .)
ifeq ($(VERSION),$(SVERSION)) ifeq ($(VERSION),$(SVERSION))
RELEASE = 1 RELEASE = 1
else else
@@ -47,12 +48,19 @@ GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
GOHOSTOS = $(shell go env GOHOSTOS) GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH) GOHOSTARCH = $(shell go env GOHOSTARCH)
RPM_PKG = releases/$(VERSION)/rpm/mgmt-$(VERSION)-1.x86_64.rpm
DEB_PKG = releases/$(VERSION)/deb/mgmt_$(VERSION)_amd64.deb
PACMAN_PKG = releases/$(VERSION)/pacman/mgmt-$(VERSION)-1-x86_64.pkg.tar.xz
SHA256SUMS = releases/$(VERSION)/SHA256SUMS
SHA256SUMS_ASC = $(SHA256SUMS).asc
default: build default: build
# #
# art # art
# #
art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png ## generate artwork
cleanart: cleanart:
rm -f art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png rm -f art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
@@ -88,19 +96,19 @@ art/mgmt_logo_white_wide.png: art/mgmt_logo_white_wide.svg
all: docs $(PROGRAM).static all: docs $(PROGRAM).static
# show the current version # show the current version
version: version: ## show the current version
@echo $(VERSION) @echo $(VERSION)
program: program: ## show the program name
@echo $(PROGRAM) @echo $(PROGRAM)
path: path: ## create working paths
./misc/make-path.sh ./misc/make-path.sh
deps: deps: ## install system and golang dependencies
./misc/make-deps.sh ./misc/make-deps.sh
run: run: ## run mgmt
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" 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 # include race flag
@@ -108,28 +116,28 @@ race:
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
# generate go files from non-go source # generate go files from non-go source
bindata: bindata: ## generate go files from non-go sources
@echo "Generating: bindata..." @echo "Generating: bindata..."
$(MAKE) --quiet -C bindata $(MAKE) --quiet -C bindata
generate: generate:
go generate go generate
lang: lang: ## generates the lexer/parser for the language frontend
@# recursively run make in child dir named lang @# recursively run make in child dir named lang
@echo "Generating: lang..." @echo "Generating: lang..."
$(MAKE) --quiet -C lang $(MAKE) --quiet -C lang
# build a `mgmt` binary for current host os/arch # build a `mgmt` binary for current host os/arch
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} $(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
cp $< $@ cp -a $< $@
$(PROGRAM).static: $(GO_FILES) $(PROGRAM).static: $(GO_FILES)
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..." @echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
go generate go generate
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS); go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION) -s -w' -o $(PROGRAM).static $(BUILD_FLAGS);
build: LDFLAGS=-s -w build: LDFLAGS=-s -w ## build a fresh mgmt binary
build: $(PROGRAM) build: $(PROGRAM)
build-debug: LDFLAGS= build-debug: LDFLAGS=
@@ -142,13 +150,18 @@ GOARCH=$(lastword $(subst -, ,$*))
build/mgmt-%: $(GO_FILES) | bindata lang build/mgmt-%: $(GO_FILES) | bindata lang
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..." @echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
@# reassigning GOOS and GOARCH to make build command copy/pastable @# reassigning GOOS and GOARCH to make build command copy/pastable
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); @# go 1.10 requires specifying the package for ldflags
@if go version | grep -qE 'go1.9'; then \
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
else \
time env GOOS=${GOOS} GOARCH=${GOARCH} go build -i -ldflags=$(PKGNAME)="-X main.program=$(PROGRAM) -X main.version=$(SVERSION) ${LDFLAGS}" -o $@ $(BUILD_FLAGS); \
fi
# create a list of binary file names to use as make targets # create a list of binary file names to use as make targets
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES})) crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
crossbuild: ${crossbuild_targets} crossbuild: ${crossbuild_targets}
clean: clean: ## clean things up
$(MAKE) --quiet -C bindata clean $(MAKE) --quiet -C bindata clean
$(MAKE) --quiet -C lang clean $(MAKE) --quiet -C lang clean
[ ! -e $(PROGRAM) ] || rm $(PROGRAM) [ ! -e $(PROGRAM) ] || rm $(PROGRAM)
@@ -157,7 +170,7 @@ clean:
# crossbuild artifacts # crossbuild artifacts
rm -f build/mgmt-* rm -f build/mgmt-*
test: build test: build ## run tests
./test.sh ./test.sh
# create all test targets for make tab completion (eg: make test-gofmt) # create all test targets for make tab completion (eg: make test-gofmt)
@@ -179,9 +192,9 @@ gofmt:
yamlfmt: yamlfmt:
find . -maxdepth 3 -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" \; find . -maxdepth 3 -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" \;
format: gofmt yamlfmt format: gofmt yamlfmt ## format yaml and golang code
docs: $(PROGRAM)-documentation.pdf docs: $(PROGRAM)-documentation.pdf ## generate docs
$(PROGRAM)-documentation.pdf: docs/documentation.md $(PROGRAM)-documentation.pdf: docs/documentation.md
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf' pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
@@ -206,7 +219,7 @@ rpmbuild/SOURCES/: tar
rpmbuild/SRPMS/: srpm rpmbuild/SRPMS/: srpm
rpmbuild/RPMS/: rpm rpmbuild/RPMS/: rpm
upload: upload-sources upload-srpms upload-rpms upload: upload-sources upload-srpms upload-rpms ## upload sources
# do nothing # do nothing
# #
@@ -314,30 +327,83 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
# #
# copr build # copr build
# #
copr: upload-srpms copr: upload-srpms ## build in copr
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE) ./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
# #
# deb build # tag
# #
tag: ## tags a new release
./misc/tag.sh
deb: #
./misc/gen-deb-changelog-from-git.sh # release
dpkg-buildpackage #
# especially when building in Docker container, pull build artifact in project directory. release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
cp ../mgmt_*_amd64.deb ./
# cleanup
rm -rf debian/mgmt/
build_container: releases/$(VERSION)/mgmt-release.url: $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) $(SHA256SUMS_ASC)
@echo "Creating github release..."
hub release create \
-F <( echo -e "$(VERSION)\n";echo "Verify the signatures of all packages before you use them. The signing key can be downloaded from https://purpleidea.com/contact/#pgp-key to verify the release." ) \
-a $(RPM_PKG) \
-a $(DEB_PKG) \
-a $(PACMAN_PKG) \
-a $(SHA256SUMS_ASC) \
$(VERSION) \
> releases/$(VERSION)/mgmt-release.url \
&& cat releases/$(VERSION)/mgmt-release.url \
|| rm -f releases/$(VERSION)/mgmt-release.url
releases/$(VERSION)/.mkdir:
mkdir -p releases/$(VERSION)/{deb,rpm,pacman}/ && touch releases/$(VERSION)/.mkdir
releases/$(VERSION)/rpm/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Generating rpm changelog..."
./misc/make-rpm-changelog.sh $(VERSION)
$(RPM_PKG): releases/$(VERSION)/rpm/changelog
@echo "Building rpm package..."
./misc/fpm-pack.sh rpm $(VERSION) libvirt-devel augeas-devel
releases/$(VERSION)/deb/changelog: $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Generating deb changelog..."
./misc/make-deb-changelog.sh $(VERSION)
$(DEB_PKG): releases/$(VERSION)/deb/changelog
@echo "Building deb package..."
./misc/fpm-pack.sh deb $(VERSION) libvirt-dev libaugeas-dev
$(PACMAN_PKG): $(PROGRAM) releases/$(VERSION)/.mkdir
@echo "Building pacman package..."
./misc/fpm-pack.sh pacman $(VERSION) libvirt augeas
$(SHA256SUMS): $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG)
@# remove the directory separator in the SHA256SUMS file
@echo "Generating sha256 sum..."
sha256sum $(RPM_PKG) $(DEB_PKG) $(PACMAN_PKG) | awk -F '/| ' '{print $$1" "$$6}' > $(SHA256SUMS)
$(SHA256SUMS_ASC): $(SHA256SUMS)
@echo "Signing sha256 sum..."
gpg2 --yes --clearsign $(SHA256SUMS)
build_container: ## builds the container
docker build -t purpleidea/mgmt-build -f docker/Dockerfile.build . docker build -t purpleidea/mgmt-build -f docker/Dockerfile.build .
docker run -td --name mgmt-build purpleidea/mgmt-build docker run -td --name mgmt-build purpleidea/mgmt-build
docker cp mgmt-build:/root/gopath/src/github.com/purpleidea/mgmt/mgmt . docker cp mgmt-build:/root/gopath/src/github.com/purpleidea/mgmt/mgmt .
docker build -t purpleidea/mgmt -f docker/Dockerfile.static . docker build -t purpleidea/mgmt -f docker/Dockerfile.static .
docker rm mgmt-build || true docker rm mgmt-build || true
clean_container: clean_container: ## removes the container
docker rmi purpleidea/mgmt-build docker rmi purpleidea/mgmt-build
docker rmi purpleidea/mgmt docker rmi purpleidea/mgmt
help: ## show this help screen
@echo 'Usage: make <OPTIONS> ... <TARGETS>'
@echo ''
@echo 'Available targets are:'
@echo ''
@grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
@echo ''
# vim: ts=8 # vim: ts=8

View File

@@ -5,8 +5,9 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/purpleidea/mgmt?style=flat-square)](https://goreportcard.com/report/github.com/purpleidea/mgmt) [![Go Report Card](https://goreportcard.com/badge/github.com/purpleidea/mgmt?style=flat-square)](https://goreportcard.com/report/github.com/purpleidea/mgmt)
[![Build Status](https://img.shields.io/travis/purpleidea/mgmt/master.svg?style=flat-square)](http://travis-ci.org/purpleidea/mgmt) [![Build Status](https://img.shields.io/travis/purpleidea/mgmt/master.svg?style=flat-square)](http://travis-ci.org/purpleidea/mgmt)
[![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/purpleidea/mgmt) [![GoDoc](https://img.shields.io/badge/godoc-reference-5272B4.svg?style=flat-square)](https://godoc.org/github.com/purpleidea/mgmt)
[![IRC](https://img.shields.io/badge/irc-%23mgmtconfig-brightgreen.svg?style=flat-square)](https://webchat.freenode.net/?channels=#mgmtconfig) [![IRC](https://img.shields.io/badge/irc-%23mgmtconfig-orange.svg?style=flat-square)](https://webchat.freenode.net/?channels=#mgmtconfig)
[![Jenkins](https://img.shields.io/badge/jenkins-status-brightgreen.svg?style=flat-square)](https://ci.centos.org/job/purpleidea-mgmt/) [![Patreon](https://img.shields.io/badge/patreon-donate-yellow.svg?style=flat-square)](https://www.patreon.com/purpleidea)
[![Liberapay](https://img.shields.io/badge/liberapay-donate-yellow.svg?style=flat-square)](https://liberapay.com/purpleidea/donate)
## Community: ## Community:
@@ -17,6 +18,8 @@ Come join us in the `mgmt` community!
| IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode | | IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
| Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) | | Twitter | [@mgmtconfig](https://twitter.com/mgmtconfig) & [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) |
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) | | Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |
| Liberapay | [purpleidea](https://liberapay.com/purpleidea/donate) on Liberapay |
## Status: ## Status:

4
Vagrantfile vendored
View File

@@ -6,7 +6,7 @@ Vagrant.configure(2) do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.define "mgmt-dev" do |instance| config.vm.define "mgmt-dev" do |instance|
instance.vm.box = "fedora/26-cloud-base" instance.vm.box = "fedora/28-cloud-base"
end end
config.vm.provider "virtualbox" do |v| config.vm.provider "virtualbox" do |v|
@@ -24,7 +24,7 @@ Vagrant.configure(2) do |config|
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig" config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
# copied from make-deps.sh (with added git) # copied from make-deps.sh (with added git)
config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make" config.vm.provision "shell", inline: "dnf install -y libvirt-devel golang golang-googlecode-tools-stringer hg git make gem"
# set up packagekit # set up packagekit
config.vm.provision "shell" do |shell| config.vm.provision "shell" do |shell|

View File

@@ -20,10 +20,13 @@ package converger
import ( import (
"fmt" "fmt"
"sort"
"sync" "sync"
"time" "time"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
multierr "github.com/hashicorp/go-multierror"
) )
// TODO: we could make a new function that masks out the state of certain // TODO: we could make a new function that masks out the state of certain
@@ -41,7 +44,8 @@ type Converger interface { // TODO: need a better name
ConvergedTimer(UID) <-chan time.Time ConvergedTimer(UID) <-chan time.Time
Status() map[uint64]bool Status() map[uint64]bool
Timeout() int // returns the timeout that this was created with Timeout() int // returns the timeout that this was created with
SetStateFn(func(bool) error) // sets the stateFn AddStateFn(string, func(bool) error) error // adds a stateFn with a name
RemoveStateFn(string) error // remove a stateFn with a given name
} }
// UID is the interface resources can use to notify with if converged. You'll // UID is the interface resources can use to notify with if converged. You'll
@@ -64,13 +68,14 @@ type UID interface {
// converger is an implementation of the Converger interface. // converger is an implementation of the Converger interface.
type converger struct { type converger struct {
timeout int // must be zero (instant) or greater seconds to run timeout int // must be zero (instant) or greater seconds to run
stateFn func(bool) error // run on converged state changes with state bool
converged bool // did we converge (state changes of this run Fn) converged bool // did we converge (state changes of this run Fn)
channel chan struct{} // signal here to run an isConverged check channel chan struct{} // signal here to run an isConverged check
control chan bool // control channel for start/pause control chan bool // control channel for start/pause
mutex sync.RWMutex // used for controlling access to status and lastid mutex *sync.RWMutex // used for controlling access to status and lastid
lastid uint64 lastid uint64
status map[uint64]bool status map[uint64]bool
stateFns map[string]func(bool) error // run on converged state changes with state bool
smutex *sync.RWMutex // used for controlling access to stateFns
} }
// cuid is an implementation of the UID interface. // cuid is an implementation of the UID interface.
@@ -78,21 +83,23 @@ type cuid struct {
converger Converger converger Converger
id uint64 id uint64
name string // user defined, friendly name name string // user defined, friendly name
mutex sync.Mutex mutex *sync.Mutex
timer chan struct{} timer chan struct{}
running bool // is the above timer running? running bool // is the above timer running?
wg sync.WaitGroup wg *sync.WaitGroup
} }
// NewConverger builds a new converger struct. // NewConverger builds a new converger struct.
func NewConverger(timeout int, stateFn func(bool) error) Converger { func NewConverger(timeout int) Converger {
return &converger{ return &converger{
timeout: timeout, timeout: timeout,
stateFn: stateFn,
channel: make(chan struct{}), channel: make(chan struct{}),
control: make(chan bool), control: make(chan bool),
mutex: &sync.RWMutex{},
lastid: 0, lastid: 0,
status: make(map[uint64]bool), status: make(map[uint64]bool),
stateFns: make(map[string]func(bool) error),
smutex: &sync.RWMutex{},
} }
} }
@@ -106,8 +113,10 @@ func (obj *converger) Register() UID {
converger: obj, converger: obj,
id: obj.lastid, id: obj.lastid,
name: fmt.Sprintf("%d", obj.lastid), // some default name: fmt.Sprintf("%d", obj.lastid), // some default
mutex: &sync.Mutex{},
timer: nil, timer: nil,
running: false, running: false,
wg: &sync.WaitGroup{},
} }
} }
@@ -216,13 +225,11 @@ func (obj *converger) Loop(startPaused bool) {
case <-obj.channel: case <-obj.channel:
if !obj.isConverged() { if !obj.isConverged() {
if obj.converged { // we're doing a state change if obj.converged { // we're doing a state change
if obj.stateFn != nil { // call the arbitrary functions (takes a read lock!)
// call an arbitrary function if err := obj.runStateFns(false); err != nil {
if err := obj.stateFn(false); err != nil {
// FIXME: what to do on error ? // FIXME: what to do on error ?
} }
} }
}
obj.converged = false obj.converged = false
continue continue
} }
@@ -230,14 +237,12 @@ func (obj *converger) Loop(startPaused bool) {
// we have converged! // we have converged!
if obj.timeout >= 0 { // only run if timeout is valid if obj.timeout >= 0 { // only run if timeout is valid
if !obj.converged { // we're doing a state change if !obj.converged { // we're doing a state change
if obj.stateFn != nil { // call the arbitrary functions (takes a read lock!)
// call an arbitrary function if err := obj.runStateFns(true); err != nil {
if err := obj.stateFn(true); err != nil {
// FIXME: what to do on error ? // FIXME: what to do on error ?
} }
} }
} }
}
obj.converged = true obj.converged = true
// loop and wait again... // loop and wait again...
} }
@@ -275,9 +280,46 @@ func (obj *converger) Timeout() int {
return obj.timeout return obj.timeout
} }
// SetStateFn sets the state function to be run on change of converged state. // AddStateFn adds a state function to be run on change of converged state.
func (obj *converger) SetStateFn(stateFn func(bool) error) { func (obj *converger) AddStateFn(name string, stateFn func(bool) error) error {
obj.stateFn = stateFn obj.smutex.Lock()
defer obj.smutex.Unlock()
if _, exists := obj.stateFns[name]; exists {
return fmt.Errorf("a stateFn with that name already exists")
}
obj.stateFns[name] = stateFn
return nil
}
// RemoveStateFn adds a state function to be run on change of converged state.
func (obj *converger) RemoveStateFn(name string) error {
obj.smutex.Lock()
defer obj.smutex.Unlock()
if _, exists := obj.stateFns[name]; !exists {
return fmt.Errorf("a stateFn with that name doesn't exist")
}
delete(obj.stateFns, name)
return nil
}
// runStateFns runs the listed of stored state functions.
func (obj *converger) runStateFns(converged bool) error {
obj.smutex.RLock()
defer obj.smutex.RUnlock()
var keys []string
for k := range obj.stateFns {
keys = append(keys, k)
}
sort.Strings(keys)
var err error
for _, name := range keys { // run in deterministic order
fn := obj.stateFns[name]
// call an arbitrary function
if e := fn(converged); e != nil {
err = multierr.Append(err, e) // list of errors
}
}
return err
} }
// ID returns the unique id of this UID object. // ID returns the unique id of this UID object.

View File

@@ -351,18 +351,28 @@ GOTAGS=novirt make build
#### Disable augeas support #### Disable augeas support
If you wish to compile mgmt without augeas support, you can use the following command: If you wish to compile mgmt without augeas support, you can use the following
command:
``` ```
GOTAGS=noaugeas make build GOTAGS=noaugeas make build
``` ```
#### Disable docker support
If you wish to compile mgmt without docker support, you can use the following
command:
```
GOTAGS=nodocker make build
```
#### Combining compile-time flags #### Combining compile-time flags
You can combine multiple tags by using a space-separated list: You can combine multiple tags by using a space-separated list:
``` ```
GOTAGS="noaugeas novirt" make build GOTAGS="noaugeas novirt nodocker" make build
``` ```
## Examples ## Examples

View File

@@ -85,8 +85,9 @@ These docs will be expanded on when things are more certain to be stable.
There are a very small number of statements in our language. They include: There are a very small number of statements in our language. They include:
- **bind**: bind's an expression to a variable within that scope - **bind**: bind's an expression to a variable within that scope without output
- eg: `$x = 42` - eg: `$x = 42`
- **if**: produces up to one branch of statements based on a conditional - **if**: produces up to one branch of statements based on a conditional
expression expression
@@ -114,6 +115,31 @@ expression
File["/tmp/hello"] -> Print["alert4"] File["/tmp/hello"] -> Print["alert4"]
``` ```
- **class**: bind's a list of statements to a class name in scope without output
```mcl
class foo {
# some statements go here
}
```
or
```mcl
class bar($a, $b) { # a parameterized class
# some statements go here
}
```
- **include**: include a particular class at this location producing output
```mcl
include foo
include bar("hello", 42)
include bar("world", 13) # an include can be called multiple times
```
All statements produce _output_. Output consists of between zero and more All statements produce _output_. Output consists of between zero and more
`edges` and `resources`. A resource statement can produce a resource, whereas an `edges` and `resources`. A resource statement can produce a resource, whereas an
`if` statement produces whatever the chosen branch produces. Ultimately the goal `if` statement produces whatever the chosen branch produces. Ultimately the goal
@@ -215,6 +241,82 @@ to express a relationship between three resources. The first character in the
resource kind must be capitalized so that the parser can't ascertain resource kind must be capitalized so that the parser can't ascertain
unambiguously that we are referring to a dependency relationship. unambiguously that we are referring to a dependency relationship.
#### Class
A class is a grouping structure that bind's a list of statements to a name in
the scope where it is defined. It doesn't directly produce any output. To
produce output it must be called via the `include` statement.
Defining classes follows the same scoping and shadowing rules that is applied to
the `bind` statement, although they exist in a separate namespace. In other
words you can have a variable named `foo` and a class named `foo` in the same
scope without any conflicts.
Classes can be both parameterized or naked. If a parameterized class is defined,
then the argument types must be either specified manually, or inferred with the
type unification algorithm. One interesting property is that the same class
definition can be used with `include` via two different input signatures,
although in practice this is probably fairly rare. Some usage examples include:
A naked class definition:
```mcl
class foo {
# some statements go here
}
```
A parameterized class with both input types being inferred if possible:
```mcl
class bar($a, $b) {
# some statements go here
}
```
A parameterized class with one type specified statically and one being inferred:
```mcl
class baz($a str, $b) {
# some statements go here
}
```
Classes can also be nested within other classes. Here's a contrived example:
```mcl
class c1($a, $b) {
# nested class definition
class c2($c) {
test $a {
stringptr => printf("%s is %d", $b, $c),
}
}
if $a == "t1" {
include c2(42)
}
}
```
Defining polymorphic classes was considered but is not currently allowed at this
time.
Recursive classes are not currently supported and it is not clear if they will
be in the future. Discussion about this topic is welcome on the mailing list.
#### Include
The `include` statement causes the previously defined class to produce the
contained output. This statement must be called with parameters if the named
class is defined with those.
The defined class can be called as many times as you'd like either within the
same scope or within different scopes. If a class uses inferred type input
parameters, then the same class can even be called with different signatures.
Whether the output is useful and whether there is a unique type unification
solution is dependent on your code.
### Stages ### Stages
The mgmt compiler runs in a number of stages. In order of execution they are: The mgmt compiler runs in a number of stages. In order of execution they are:
@@ -596,6 +698,39 @@ someListOfStrings := &types.ListValue{
If you don't build these properly, then you will cause a panic! Even empty lists If you don't build these properly, then you will cause a panic! Even empty lists
have a type. have a type.
### Is the `class` statement a singleton?
Not really, but practically it can be used as such. The `class` statement is not
a singleton since it can be called multiple times in different locations, and it
can also be parameterized and called multiple times (with `include`) using
different input parameters. The reason it can be used as such is that statement
output (from multple classes) that is compatible (and usually identical) will
be automatically collated and have the duplicates removed. In that way, you can
assume that an unparameterized class is always a singleton, and that
parameterized classes can often be singletons depending on their contents and if
they are called in an identical way or not. In reality the de-duplication
actually happens at the resource output level, so anything that produces
multiple compatible resources is allowed.
### Are recursive `class` definitions supported?
Recursive class definitions where the contents of a `class` contain a
self-referential `include`, either directly, or with indirection via any other
number of classes is not supported. It's not clear if it ever will be in the
future, unless we decide it's worth the extra complexity. The reason is that our
FRP actually generates a static graph which doesn't change unless the code does.
To support dynamic graphs would require our FRP to be a "higher-order" FRP,
instead of the simpler "first-order" FRP that it is now. You might want to
verify that I got the [nomenclature](https://github.com/gelisam/frp-zoo)
correct. If it turns out that there's an important advantage to supporting a
higher-order FRP in mgmt, then we can consider that in the future.
I realized that recursion would require a static graph when I considered the
structure required for a simple recursive class definition. If some "depth"
value wasn't known statically by compile time, then there would be no way to
know how large the graph would grow, and furthermore, the graph would need to
change if that "depth" value changed.
### I don't like the mgmt language, is there an alternative? ### I don't like the mgmt language, is there an alternative?
Yes, the language is just one of the available "frontends" that passes a stream Yes, the language is just one of the available "frontends" that passes a stream

View File

@@ -34,7 +34,7 @@ if we missed something that you think is relevant!
| James Shubin | video | [Recording from Incontro DevOps 2017](https://vimeo.com/212241877) | | James Shubin | video | [Recording from Incontro DevOps 2017](https://vimeo.com/212241877) |
| Yves Brissaud | blog | [mgmt aux HumanTalks Grenoble (french)](http://log.winsos.net/2017/04/12/mgmt-aux-human-talks-grenoble.html) | | Yves Brissaud | blog | [mgmt aux HumanTalks Grenoble (french)](http://log.winsos.net/2017/04/12/mgmt-aux-human-talks-grenoble.html) |
| James Shubin | video | [Recording from OSDC Berlin 2017](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1) | | James Shubin | video | [Recording from OSDC Berlin 2017](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1) |
| Jonathan Gold | blog | [AWS:EC2 in mgmt](http://jonathangold.ca/awsec2-in-mgmt/) | | Jonathan Gold | blog | [AWS:EC2 in mgmt](https://jonathangold.ca/blog/aws-ec2-in-mgmt/) |
| James Shubin | video | [Recording from OSMC Nuremberg 2017](https://www.youtube.com/watch?v=hSVadQLeplU&html5=1) | | James Shubin | video | [Recording from OSMC Nuremberg 2017](https://www.youtube.com/watch?v=hSVadQLeplU&html5=1) |
| James Shubin | video | [Recording from LCA 2018, Developers Miniconf](https://www.youtube.com/watch?v=OvgGfW0ilbE) | | James Shubin | video | [Recording from LCA 2018, Developers Miniconf](https://www.youtube.com/watch?v=OvgGfW0ilbE) |
| James Shubin | video | [Recording from LCA 2018, Sysadmin Miniconf](https://www.youtube.com/watch?v=ELq1XOJMIPY) | | James Shubin | video | [Recording from LCA 2018, Sysadmin Miniconf](https://www.youtube.com/watch?v=ELq1XOJMIPY) |
@@ -43,3 +43,4 @@ if we missed something that you think is relevant!
| James Shubin | video | [Recording from FOSDEM 2018, Config Management Devroom](https://video.fosdem.org/2018/UA2.114/mgmt.webm) | | James Shubin | video | [Recording from FOSDEM 2018, Config Management Devroom](https://video.fosdem.org/2018/UA2.114/mgmt.webm) |
| James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) | | James Shubin | blog | [Mgmt Configuration Language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/) |
| James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) | | James Shubin | video | [Recording from CfgMgmtCamp.eu 2018](https://www.youtube.com/watch?v=NxObmwZDyrI) |
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |

View File

@@ -119,8 +119,11 @@ To build `mgmt` without augeas support please run:
To build `mgmt` without libvirt support please run: To build `mgmt` without libvirt support please run:
`GOTAGS='novirt' make build` `GOTAGS='novirt' make build`
To build `mgmt` without augeas or libvirt support please run: To build `mgmt` without docker support please run:
`GOTAGS='noaugeas novirt' make build` `GOTAGS='nodocker' make build`
To build `mgmt` without augeas, libvirt or docker support please run:
`GOTAGS='noaugeas novirt nodocker' make build`
## Binary Package Installation ## Binary Package Installation

View File

@@ -19,17 +19,80 @@ on this design, please read the
[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) [original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
on the subject. on the subject.
## Resource Prerequisites
### Imports
You'll need to import a few packages to make writing your resource easier. Here
is the list:
```
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
```
The `engine` package contains most of the interfaces and helper functions that
you'll need to use. The `traits` package contains some base functionality which
you can use to easily add functionality to your resource without needing to
implement it from scratch.
### Resource struct
Each resource will implement methods as pointer receivers on a resource struct.
The naming convention for resources is that they end with a `Res` suffix.
The resource struct should include an anonymous reference to the `Base` trait.
Other `traits` can be added to the resource to add additional functionality.
They are discussed below.
You'll most likely want to store a reference to the `*Init` struct type as
defined by the engine. This is data that the engine will provide to your
resource on Init.
Lastly you should define the public fields that make up your resource API, as
well as any private fields that you might want to use throughout your resource.
Do _not_ depend on global variables, since multiple copies of your resource
could get instantiated.
You'll want to add struct tags based on the different frontends that you want
your resources to be able to use. Some frontends can infer this information if
it is not specified, but others cannot, and some might poorly infer if the
struct name is ambiguous.
If you'd like your resource to be accessible by the `YAML` graph API (GAPI),
then you'll need to include the appropriate YAML fields as shown below. This is
used by the `Puppet` compiler as well, so make sure you include these struct
tags if you want existing `Puppet` code to be able to run using the `mgmt`
engine.
#### Example
```golang
type FooRes struct {
traits.Base // add the base methods without re-implementation
traits.Groupable
traits.Refreshable
init *engine.Init
Whatever string `lang:"whatever" yaml:"whatever"` // you pick!
Baz bool `lang:"baz" yaml:"baz"` // something else
something string // some private field
}
```
## Resource API ## Resource API
To implement a resource in `mgmt` it must satisfy the To implement a resource in `mgmt` it must satisfy the
[`Res`](https://github.com/purpleidea/mgmt/blob/master/resources/resources.go) [`Res`](https://github.com/purpleidea/mgmt/blob/master/engine/resources.go)
interface. What follows are each of the method signatures and a description of interface. What follows are each of the method signatures and a description of
each. each.
### Default ### Default
```golang ```golang
Default() Res Default() engine.Res
``` ```
This returns a populated resource struct as a `Res`. It shouldn't populate any This returns a populated resource struct as a `Res`. It shouldn't populate any
@@ -55,9 +118,12 @@ Validate() error
This method is used to validate if the populated resource struct is a valid This method is used to validate if the populated resource struct is a valid
representation of the resource kind. If it does not conform to the resource representation of the resource kind. If it does not conform to the resource
specifications, it should generate an error. If you notice that this method is specifications, it should return an error. If you notice that this method is
quite large, it might be an indication that you should reconsider the parameter quite large, it might be an indication that you should reconsider the parameter
list and interface to this resource. This method is called _before_ `Init`. list and interface to this resource. This method is called by the engine
_before_ `Init`. It can also be called occasionally after a Send/Recv operation
to verify that the newly populated parameters are valid. Remember not to expect
access to the outside world when using this.
#### Example #### Example
@@ -67,7 +133,7 @@ func (obj *FooRes) Validate() error {
if obj.Answer != 42 { // validate whatever you want if obj.Answer != 42 { // validate whatever you want
return fmt.Errorf("expected an answer of 42") return fmt.Errorf("expected an answer of 42")
} }
return obj.BaseRes.Validate() // remember to call the base method! return nil
} }
``` ```
@@ -78,19 +144,28 @@ Init() error
``` ```
This is called to initialize the resource. If something goes wrong, it should This is called to initialize the resource. If something goes wrong, it should
return an error. It should do any resource specific work, and finish by calling return an error. It should do any resource specific work such as initializing
the `Init` method of the base resource. channels, sync primitives, or anything else that is relevant to your resource.
If it is not need throughout, it might be preferable to do some initialization
and tear down locally in either the Watch method or CheckApply method. The
choice depends on your particular resource and making the best decision requires
some experience with mgmt. If you are unsure, feel free to ask an existing
`mgmt` contributor. During `Init`, the engine will pass your resource a struct
containing some useful data and pointers. You should save a copy of this pointer
since you will need to use it in other parts of your resource.
#### Example #### Example
```golang ```golang
// Init initializes the Foo resource. // Init initializes the Foo resource.
func (obj *FooRes) Init() error { func (obj *FooRes) Init(init *engine.Init) error
obj.init = init // save for later
// run the resource specific initialization, and error if anything fails // run the resource specific initialization, and error if anything fails
if some_error { if some_error {
return err // something went wrong! return err // something went wrong!
} }
return obj.BaseRes.Init() // call the base resource init return nil
} }
``` ```
@@ -108,7 +183,9 @@ Close() error
This is called to cleanup after the resource. It is usually not necessary, but This is called to cleanup after the resource. It is usually not necessary, but
can be useful if you'd like to properly close a persistent connection that you can be useful if you'd like to properly close a persistent connection that you
opened in the `Init` method and were using throughout the resource. opened in the `Init` method and were using throughout the resource. It is *not*
the shutdown signal that tells the resource to exit. That happens in the Watch
loop.
#### Example #### Example
@@ -116,21 +193,13 @@ opened in the `Init` method and were using throughout the resource.
// Close runs some cleanup code for this resource. // Close runs some cleanup code for this resource.
func (obj *FooRes) Close() error { func (obj *FooRes) Close() error {
err := obj.conn.Close() // close some internal connection err := obj.conn.Close() // close some internal connection
obj.someMap = nil // free up some large data structure from memory
// call base close, b/c we're overriding
if e := obj.BaseRes.Close(); err == nil {
err = e
} else if e != nil {
err = multierr.Append(err, e) // list of errors
}
return err return err
} }
``` ```
You should probably check the return errors of your internal methods, and pass You should probably check the return errors of your internal methods, and pass
on an error if something went wrong. Remember to always call the base `Close` on an error if something went wrong.
method! If you plan to return early if you hit an internal error, then at least
call it with a defer!
### CheckApply ### CheckApply
@@ -143,7 +212,8 @@ function should check if the state of this resource is correct, and if so, it
should return: `(true, nil)`. If the `apply` variable is set to `true`, then should return: `(true, nil)`. If the `apply` variable is set to `true`, then
this means that we should then proceed to run the changes required to bring the this means that we should then proceed to run the changes required to bring the
resource into the correct state. If the `apply` variable is set to `false`, then resource into the correct state. If the `apply` variable is set to `false`, then
the resource is operating in _noop_ mode and _no operations_ should be executed! the resource is operating in _noop_ mode and _no operational changes_ should be
made!
After having executed the necessary operations to bring the resource back into After having executed the necessary operations to bring the resource back into
the desired state, or after having detected that the state was incorrect, but the desired state, or after having detected that the state was incorrect, but
@@ -155,8 +225,8 @@ function. If you cannot, then you must return an error! The exception to this
rule is that if an external force changes the state of the resource while it is rule is that if an external force changes the state of the resource while it is
being remedied, it is possible to return from this function even though the being remedied, it is possible to return from this function even though the
resource isn't now converged. This is not a bug, as the resources `Watch` resource isn't now converged. This is not a bug, as the resources `Watch`
facility will detect the change, ultimately resulting in a subsequent call to facility will detect the new change, ultimately resulting in a subsequent call
`CheckApply`. to `CheckApply`.
#### Example #### Example
@@ -165,11 +235,15 @@ facility will detect the change, ultimately resulting in a subsequent call to
func (obj *FooRes) CheckApply(apply bool) (bool, error) { func (obj *FooRes) CheckApply(apply bool) (bool, error) {
// check the state // check the state
if state_is_okay { return true, nil } // done early! :) if state_is_okay { return true, nil } // done early! :)
// state was bad // state was bad
if !apply { return false, nil } // don't apply; !stateok, nil
if !apply { return false, nil } // don't apply, we're in noop mode
if any_error { return false, err } // anytime there's an err!
// do the apply! // do the apply!
return false, nil // after success applying return false, nil // after success applying
if any_error { return false, err } // anytime there's an err!
} }
``` ```
@@ -180,20 +254,6 @@ skipped. This is an engine optimization, and not a bug. It is mentioned here in
the documentation in case you are confused as to why a debug message you've the documentation in case you are confused as to why a debug message you've
added to the code isn't always printed. added to the code isn't always printed.
#### Refresh notifications
Some resources may choose to support receiving refresh notifications. In general
these should be avoided if possible, but nevertheless, they do make sense in
certain situations. Resources that support these need to verify if one was sent
during the CheckApply phase of execution. This is accomplished by calling the
`Refresh() bool` method of the resource, and inspecting the return value. This
is only necessary if you plan to perform a refresh action. Refresh actions
should still respect the `apply` variable, and no system changes should be made
if it is `false`. Refresh notifications are generated by any resource when an
action is applied by that resource and are transmitted through graph edges which
have enabled their propagation. Resources that currently perform some refresh
action include `svc`, `timer`, and `password`.
#### Paired execution #### Paired execution
For many resources it is not uncommon to see `CheckApply` run twice in rapid For many resources it is not uncommon to see `CheckApply` run twice in rapid
@@ -210,7 +270,7 @@ will likely find the state to now be correct.
* If the state is correct and no changes are needed, return `(true, nil)`. * If the state is correct and no changes are needed, return `(true, nil)`.
* You should only make changes to the system if `apply` is set to `true`. * You should only make changes to the system if `apply` is set to `true`.
* After checking the state and possibly applying the fix, return `(false, nil)`. * After checking the state and possibly applying the fix, return `(false, nil)`.
* Returning `(true, err)` is a programming error and will cause a `Fatal`. * Returning `(true, err)` is a programming error and can have a negative effect.
### Watch ### Watch
@@ -223,7 +283,7 @@ state of the resource might have changed. To send a message you should write to
the input event channel using the `Event` helper method. The Watch function the input event channel using the `Event` helper method. The Watch function
should run continuously until a shutdown message is received. If at any time should run continuously until a shutdown message is received. If at any time
something goes wrong, you should return an error, and the `mgmt` engine will something goes wrong, you should return an error, and the `mgmt` engine will
handle possibly restarting the main loop based on the `retry` meta parameters. handle possibly restarting the main loop based on the `retry` meta parameter.
It is better to send an event notification which turns out to be spurious, than It is better to send an event notification which turns out to be spurious, than
to miss a possible event. Resources which can miss events are incorrect and need to miss a possible event. Resources which can miss events are incorrect and need
@@ -248,17 +308,20 @@ The lifetime of most resources `Watch` method should be spent in an infinite
loop that is bounded by a `select` call. The `select` call is the point where loop that is bounded by a `select` call. The `select` call is the point where
our method hands back control to the engine (and the kernel) so that we can our method hands back control to the engine (and the kernel) so that we can
sleep until something of interest wakes us up. In this loop we must process sleep until something of interest wakes us up. In this loop we must process
events from the engine via the `<-obj.Events()` call, and receive events for our events from the engine via the `<-obj.init.Events` channel, and receive events
resource itself! for our resource itself!
#### Events #### Events
If we receive an internal event from the `<-obj.Events()` method, we can read it If we receive an internal event from the `<-obj.init.Events` channel, we should
with the ReadEvent helper function. This function tells us if we should shutdown read it with the `obj.init.Read` helper function. This function tells us if we
our resource, and if we should generate an event. When we want to send an event, should shutdown our resource. It also handles pause functionality which blocks
we use the `Event` helper function. It is also important to mark the resource our resource temporarily in this method. If this channel shuts down, then we
state as `dirty` if we believe it might have changed. We do this with the should treat that as an exit signal.
`StateOK(false)` function.
When we want to send an event, we use the `Event` helper function. It is also
important to mark the resource state as `dirty` if we believe it might have
changed. We do this by calling the `obj.init.Dirty` function.
#### Startup #### Startup
@@ -266,22 +329,17 @@ Once the `Watch` function has finished starting up successfully, it is important
to generate one event to notify the `mgmt` engine that we're now listening to generate one event to notify the `mgmt` engine that we're now listening
successfully, so that it can run an initial `CheckApply` to ensure we're safely successfully, so that it can run an initial `CheckApply` to ensure we're safely
tracking a healthy state and that we didn't miss anything when `Watch` was down tracking a healthy state and that we didn't miss anything when `Watch` was down
or from before `mgmt` was running. It does this by calling the `Running` method. or from before `mgmt` was running. You must do this by calling the
`obj.init.Running` method. If it returns an error, you must exit and return that
error.
#### Converged #### Converged
The engine might be asked to shutdown when the entire state of the system has The engine might be asked to shutdown when the entire state of the system has
not seen any changes for some duration of time. The engine can determine this not seen any changes for some duration of time. The engine can determine this
automatically, but each resource can block this if it is absolutely necessary. automatically, but each resource can block this if it is absolutely necessary.
To do this, the `Watch` method should get the `ConvergedUID` handle that has If you need this functionality, please contact one of the maintainers and ask
been prepared for it by the engine. This is done by calling the `ConvergerUID` about adding this feature and improving these docs right here.
method on the resource object. The result can be used to set the converged
status with `SetConverged`, and to notify when the particular timeout has been
reached by waiting on `ConvergedTimer`.
Instead of interacting with the `ConvergedUID` with these two methods, we can
instead use the `StartTimer` and `ResetTimer` methods which accomplish the same
thing, but provide a `select`-free interface for different coding situations.
This particular facility is most likely not required for most resources. It may This particular facility is most likely not required for most resources. It may
prove to be useful if a resource wants to start off a long operation, but avoid prove to be useful if a resource wants to start off a long operation, but avoid
@@ -297,28 +355,31 @@ func (obj *FooRes) Watch() error {
if err, obj.foo = OpenFoo(); err != nil { if err, obj.foo = OpenFoo(); err != nil {
return err // we couldn't startup return err // we couldn't startup
} }
defer obj.whatever.CloseFoo() // shutdown our defer obj.whatever.CloseFoo() // shutdown our Foo
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { // shutdown engine
return *exit // exit // (it is okay if some `defer` code runs first)
return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
// the actual events! // the actual events!
case event := <-obj.foo.Events: case event := <-obj.foo.Events:
if is_an_event { if is_an_event {
send = true // used below send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
} }
// event errors // event errors
@@ -329,7 +390,9 @@ func (obj *FooRes) Watch() error {
// 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
obj.Event() // send the event! if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -337,87 +400,259 @@ func (obj *FooRes) Watch() error {
#### Summary #### Summary
* Remember to call the appropriate `converger` methods throughout the resource. * Remember to call `Running` when the `Watch` is running successfully.
* Remember to call `Startup` when the `Watch` is running successfully.
* Remember to process internal events and shutdown promptly if asked to. * Remember to process internal events and shutdown promptly if asked to.
* Ensure the design of your resource is well thought out. * Ensure the design of your resource is well thought out.
* Have a look at the existing resources for a rough idea of how this all works. * Have a look at the existing resources for a rough idea of how this all works.
### Compare ### Cmp
```golang ```golang
Compare(Res) bool Cmp(engine.Res) error
``` ```
Each resource must have a `Compare` method. This takes as input another resource Each resource must have a `Cmp` method. It is an abbreviation for `Compare`. It
and must return whether they are identical or not. This is used for identifying takes as input another resource and must return whether they are identical or
if an existing resource can be used in place of a new one with a similar set of not. This is used for identifying if an existing resource can be used in place
parameters. In particular, when switching from one graph to a new (possibly of a new one with a similar set of parameters. In particular, when switching
identical) graph, this avoids recomputing the state for resources which don't from one graph to a new (possibly identical) graph, this avoids recomputing the
change or that are sufficiently similar that they don't need to be swapped out. state for resources which don't change or that are sufficiently similar that
they don't need to be swapped out.
In general if all the resource properties are identical, then they usually don't In general if all the resource properties are identical, then they usually don't
need to be changed. On occasion, not all of them need to be compared, in need to be changed. On occasion, not all of them need to be compared, in
particular if they store some generated state, or if they aren't significant in particular if they store some generated state, or if they aren't significant in
some way. some way.
If the resource is identical, then you should return `nil`. If it is not, then
you should return a short error message which gives the reason it differs.
#### Example #### Example
```golang ```golang
// Compare two resources and return if they are equivalent. // Cmp compares two resources and returns if they are equivalent.
func (obj *FooRes) Compare(r Res) bool { func (obj *FooRes) Cmp(r engine.Res) error {
// we can only compare FooRes to others of the same resource kind // we can only compare FooRes to others of the same resource kind
res, ok := r.(*FooRes) res, ok := r.(*FooRes)
if !ok { if !ok {
return false return fmt.Errorf("not a %s", obj.Kind())
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
} }
if obj.whatever != res.whatever { if obj.Whatever != res.Whatever {
return false return fmt.Errorf("the Whatever param differs")
} }
if obj.Flag != res.Flag { if obj.Flag != res.Flag {
return false return fmt.Errorf("the Flag param differs")
} }
return true // they must match! return nil // they must match!
} }
``` ```
### UIDs ## Traits
Resources can have different `traits`, which means they can be extended to have
additional functionality or special properties. Those special properties are
usually added by extending your resource so that it is compatible with
additional interface that contain the `Res` interface. Each of these interfaces
represents the additional functionality. Since in most cases this requires some
common boilerplate, you can usually get some or most of the functionality by
embedding the correct trait struct anonymously in your struct. This is shown in
the struct example above. You'll always want to include the `Base` trait in all
resources. This provides some basics which you'll always need.
What follows are a list of available traits.
### Refreshable
Some resources may choose to support receiving refresh notifications. In general
these should be avoided if possible, but nevertheless, they do make sense in
certain situations. Resources that support these need to verify if one was sent
during the CheckApply phase of execution. This is accomplished by calling the
`obj.init.Refresh() bool` method, and inspecting the return value. This is only
necessary if you plan to perform a refresh action. Refresh actions should still
respect the `apply` variable, and no system changes should be made if it is
`false`. Refresh notifications are generated by any resource when an action is
applied by that resource and are transmitted through graph edges which have
enabled their propagation. Resources that currently perform some refresh action
include `svc`, `timer`, and `password`.
It is very important that you include the `traits.Refreshable` struct in your
resource. If you do not include this, then calling `obj.init.Refresh` may
trigger a panic. This is programmer error.
### Edgeable
Edgeable is a trait that allows your resource to automatically connect itself to
other resources that use this trait to add edge dependencies between the two. An
older blog post on this topic is
[available](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
After you've included this trait, you'll need to implement two methods on your
resource.
#### UIDs
```golang ```golang
UIDs() []ResUID UIDs() []engine.ResUID
``` ```
The `UIDs` method returns a list of `ResUID` interfaces that represent the The `UIDs` method returns a list of `ResUID` interfaces that represent the
particular resource uniquely. This is used with the AutoEdges API to determine particular resource uniquely. This is used with the AutoEdges API to determine
if another resource can match a dependency to this one. if another resource can match a dependency to this one.
### AutoEdges #### AutoEdges
```golang ```golang
AutoEdges() (AutoEdge, error) AutoEdges() (engine.AutoEdge, error)
``` ```
This returns a struct that implements the `AutoEdge` interface. This struct This returns a struct that implements the `AutoEdge` interface. This struct
is used to match other resources that might be relevant dependencies for this is used to match other resources that might be relevant dependencies for this
resource. resource.
### CollectPattern ### Groupable
```golang Groupable is a trait that can allow your resource automatically group itself to
CollectPattern() string other resources. Doing so can reduce the resource or runtime burden on the
``` engine, and improve performance in some scenarios. An older blog post on this
topic is
[available](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
### Sendable
Sendable is a trait that allows your resource to send values through the graph
edges to another resource. These values are produced during `CheckApply`. They
can be sent to any resource that has an appropriate parameter and that has the
`Recvable` trait. You can read more about this in the Send/Recv section below.
### Recvable
Recvable is a trait that allows your resource to receive values through the
graph edges from another resource. These values are consumed during the
`CheckApply` phase, and can be detected there as well. They can be received from
any resource that has an appropriate value and that has the `Sendable` trait.
You can read more about this in the Send/Recv section below.
### Collectable
This is currently a stub and will be updated once the DSL is further along. This is currently a stub and will be updated once the DSL is further along.
### UnmarshalYAML ## Resource Initialization
During the resource initialization in `Init`, the engine will pass in a struct
containing a bunch of data and methods. What follows is a description of each
one and how it is used.
### Program
Program is a string containing the name of the program. Very few resources need
this.
### Hostname
Hostname is the uuid for the host. It will be occasionally useful in some
resources. It is preferable if you can avoid depending on this. It is possible
that in the future this will be a channel which changes if the local hostname
changes.
### Running
Running must be called after your watches are all started and ready. It is only
called from within `Watch`. It is used to notify the engine that you're now
ready to detect changes.
### Event
Event sends an event notifying the engine of a possible state change. It is
only called from within `Watch`.
### Events
Events is a channel that we must watch for messages from the engine. When it
closes, this is a signal to shutdown. It is
only called from within `Watch`.
### Read
Read processes messages that come in from the `Events` channel. It is a helper
method that knows how to handle the pause mechanism correctly. It is
only called from within `Watch`.
### Dirty
Dirty marks the resource state as dirty. This signals to the engine that
CheckApply will have some work to do in order to converge it. It is
only called from within `Watch`.
### Refresh
Refresh returns whether the resource received a notification. This flag can be
used to tell a `svc` to reload, or to perform some state change that wouldn't
otherwise be noticed by inspection alone. You must implement the `Refreshable`
trait for this to work. It is only called from within `CheckApply`.
### Send
Send exposes some variables you wish to send via the `Send/Recv` mechanism. You
must implement the `Sendable` trait for this to work. It is only called from
within `CheckApply`.
### Recv
Recv provides a map of variables which were sent to this resource via the
`Send/Recv` mechanism. You must implement the `Recvable` trait for this to work.
It is only called from within `CheckApply`.
### World
World provides a connection to the outside world. This is most often used for
communicating with the distributed database. It can be used in `Init`,
`CheckApply` and `Watch`. Use with discretion and understanding of the internals
if needed in `Close`.
### VarDir
VarDir is a facility for local storage. It is used to return a path to a
directory which may be used for temporary storage. It should be cleaned up on
resource `Close` if the resource would like to delete the contents. The resource
should not assume that the initial directory is empty, and it should be cleaned
on `Init` if that is a requirement.
### Debug
Debug signals whether we are running in debugging mode. In this case, we might
want to log additional messages.
### Logf
Logf is a logging facility which will correctly namespace any messages which you
wish to pass on. You should use this instead of the log package directly for
production quality resources.
## Further considerations
There is some additional information that any resource writer will need to know.
Each issue is listed separately below!
### Resource registration
All resources must be registered with the engine so that they can be found. This
also ensures they can be encoded and decoded. Make sure to include the following
code snippet for this to work.
```golang
func init() { // special golang method that runs once
// set your resource kind and struct here (the kind must be lower case)
engine.RegisterResource("foo", func() engine.Res { return &FooRes{} })
}
```
### YAML Unmarshalling
To support YAML unmarshalling for your resource, you must implement an
additional method. It is recommended if you want to use your resource with the
`Puppet` compiler.
```golang ```golang
UnmarshalYAML(unmarshal func(interface{}) error) error // optional UnmarshalYAML(unmarshal func(interface{}) error) error // optional
@@ -455,105 +690,34 @@ func (obj *FooRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
``` ```
## Further considerations
There is some additional information that any resource writer will need to know.
Each issue is listed separately below!
### Resource struct
Each resource will implement methods as pointer receivers on a resource struct.
The resource struct must include an anonymous reference to the `BaseRes` struct.
The naming convention for resources is that they end with a `Res` suffix. If
you'd like your resource to be accessible by the `YAML` graph API (GAPI), then
you'll need to include the appropriate YAML fields as shown below.
#### Example
```golang
type FooRes struct {
BaseRes `yaml:",inline"` // base properties
Whatever string `yaml:"whatever"` // you pick!
Bar int // no yaml, used as public output value for send/recv
Baz bool `yaml:"baz"` // something else
something string // some private field
}
```
### Resource registration
All resources must be registered with the engine so that they can be found. This
also ensures they can be encoded and decoded. Make sure to include the following
code snippet for this to work.
```golang
func init() { // special golang method that runs once
// set your resource kind and struct here (the kind must be lower case)
RegisterResource("foo", func() Res { return &FooRes{} })
}
```
## Automatic edges
Automatic edges in `mgmt` are well described in [this article](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/).
The best example of this technique can be seen in the `svc` resource.
Unfortunately no further documentation about this subject has been written. To
expand this section, please send a patch! Please contact us if you'd like to
work on a resource that uses this feature, or to add it to an existing one!
## Automatic grouping
Automatic grouping in `mgmt` is well described in [this article](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/).
The best example of this technique can be seen in the `pkg` resource.
Unfortunately no further documentation about this subject has been written. To
expand this section, please send a patch! Please contact us if you'd like to
work on a resource that uses this feature, or to add it to an existing one!
## Send/Recv ## Send/Recv
In `mgmt` there is a novel concept called _Send/Recv_. For some background, In `mgmt` there is a novel concept called _Send/Recv_. For some background,
please [read the introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/). please read the [introductory article](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/).
When using this feature, the engine will automatically send the user specified When using this feature, the engine will automatically send the user specified
value to the intended destination without requiring any resource specific code. value to the intended destination without requiring much resource specific code.
Any time that one of the destination values is changed, the engine automatically Any time that one of the destination values is changed, the engine automatically
marks the resource state as `dirty`. To detect if a particular value was marks the resource state as `dirty`. To detect if a particular value was
received, and if it changed (during this invocation of CheckApply) from the received, and if it changed (during this invocation of `CheckApply`) from the
previous value, you can query the Recv parameter. It will contain a `map` of all previous value, you can query the `obj.init.Recv()` method. It will contain a
the keys which can be received on, and the value has a `Changed` property which `map` of all the keys which can be received on, and the value has a `Changed`
will indicate whether the value was updated on this particular `CheckApply` property which will indicate whether the value was updated on this particular
invocation. The type of the sending key must match that of the receiving one. `CheckApply` invocation. The type of the sending key must match that of the
This can _only_ be done inside of the `CheckApply` function! receiving one. This can _only_ be done inside of the `CheckApply` function!
```golang ```golang
// inside CheckApply, probably near the top // inside CheckApply, probably near the top
if val, exists := obj.Recv["SomeKey"]; exists { if val, exists := obj.init.Recv()["SomeKey"]; exists {
log.Printf("SomeKey was sent to us from: %s.%s", val.Res, val.Key) obj.init.Logf("the SomeKey param was sent to us from: %s.%s", val.Res, val.Key)
if val.Changed { if val.Changed {
log.Printf("SomeKey was just updated!") obj.init.Logf("the SomeKey param was just updated!")
// you may want to invalidate some local cache // you may want to invalidate some local cache
} }
} }
``` ```
Astute readers will note that there isn't anything that prevents a user from The specifics of resource sending are not currently documented. Please send a
sending an identically typed value to some arbitrary (public) key that the patch here!
resource author hadn't considered! While this is true, resources should probably
work within this problem space anyways. The rule of thumb is that any public
parameter which is normally used in a resource can be used safely.
One subtle scenario is that if a resource creates a local cache or stores a
computation that depends on the value of a public parameter and will require
invalidation should that public parameter change, then you must detect that
scenario and invalidate the cache when it occurs. This *must* be processed
before there is a possibility of failure in CheckApply, because if we fail (and
possibly run again) the subsequent send->recv transfer might not have a new
value to copy, and therefore we won't see this notification of change.
Therefore, it is important to process these promptly, if they must not be lost,
such as for cache invalidation.
Remember, `Send/Recv` only changes your resource code if you cache state.
## Composite resources ## Composite resources
@@ -624,6 +788,15 @@ us know!
There are still many ideas for new resources that haven't been written yet. If There are still many ideas for new resources that haven't been written yet. If
you'd like to contribute one, please contact us and tell us about your idea! you'd like to contribute one, please contact us and tell us about your idea!
### Is the resource API stable? Does it ever change?
Since we are pre 1.0, the resource API is not guaranteed to be stable, however
it is not expected to change significantly. The last major change kept the
core functionality nearly identical, simplified the implementation of all the
resources, and took about five to ten minutes to port each resource to the new
API. The fundamental logic and behaviour behind the resource API has not changed
since it was initially introduced.
### Where can I find more information about mgmt? ### Where can I find more information about mgmt?
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md). Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).

View File

@@ -13,21 +13,27 @@ separately. Certain meta parameters aren't very useful when combined with
certain resources, but in general, it should be fairly obvious, such as when certain resources, but in general, it should be fairly obvious, such as when
combining the `noop` meta parameter with the [Noop](#Noop) resource. combining the `noop` meta parameter with the [Noop](#Noop) resource.
You might want to look at the [generated documentation](https://godoc.org/github.com/purpleidea/mgmt/resources) You might want to look at the [generated documentation](https://godoc.org/github.com/purpleidea/mgmt/engine/resources)
for more up-to-date information about these resources. for more up-to-date information about these resources.
* [Augeas](#Augeas): Manipulate files using augeas. * [Augeas](#Augeas): Manipulate files using augeas.
* [Docker](#Docker):[Container](#Container) Manage docker containers.
* [Exec](#Exec): Execute shell commands on the system. * [Exec](#Exec): Execute shell commands on the system.
* [File](#File): Manage files and directories. * [File](#File): Manage files and directories.
* [Group](#Group): Manage system groups.
* [Hostname](#Hostname): Manages the hostname on the system. * [Hostname](#Hostname): Manages the hostname on the system.
* [KV](#KV): Set a key value pair in our shared world database. * [KV](#KV): Set a key value pair in our shared world database.
* [Msg](#Msg): Send log messages. * [Msg](#Msg): Send log messages.
* [Net](#Net): Manage a local network interface.
* [Noop](#Noop): A simple resource that does nothing. * [Noop](#Noop): A simple resource that does nothing.
* [Nspawn](#Nspawn): Manage systemd-machined nspawn containers. * [Nspawn](#Nspawn): Manage systemd-machined nspawn containers.
* [Password](#Password): Create random password strings. * [Password](#Password): Create random password strings.
* [Pkg](#Pkg): Manage system packages with PackageKit. * [Pkg](#Pkg): Manage system packages with PackageKit.
* [Print](#Print): Print messages to the console.
* [Svc](#Svc): Manage system systemd services. * [Svc](#Svc): Manage system systemd services.
* [Test](#Test): A mostly harmless resource that is used for internal testing.
* [Timer](#Timer): Manage system systemd services. * [Timer](#Timer): Manage system systemd services.
* [User](#User): Manage system users.
* [Virt](#Virt): Manage virtual machines with libvirt. * [Virt](#Virt): Manage virtual machines with libvirt.
## Augeas ## Augeas
@@ -35,6 +41,22 @@ for more up-to-date information about these resources.
The augeas resource uses [augeas](http://augeas.net/) commands to manipulate The augeas resource uses [augeas](http://augeas.net/) commands to manipulate
files. files.
## Docker
### Container
The docker:container resource manages docker containers.
It has the following properties:
* `state`: either `running`, `stopped`, or `removed`
* `image`: docker `image` or `image:tag`
* `cmd`: a command or list of commands to run on the container
* `env`: a list of environment variables, e.g. `["VAR=val",],`
* `ports`: a map of portmappings, e.g. `{"tcp" => {80 => 8080, 443 => 8443,},},`
* `apiversion:` override the host's default docker version, e.g. `"v1.35"`
* `force`: destroy and rebuild the container instead of erroring on wrong image
## Exec ## Exec
The exec resource can execute commands on your system. The exec resource can execute commands on your system.
@@ -82,6 +104,10 @@ The force property is required if we want the file resource to be able to change
a file into a directory or vice-versa. If such a change is needed, but the force a file into a directory or vice-versa. If such a change is needed, but the force
property is not set to `true`, then this file resource will error. property is not set to `true`, then this file resource will error.
## Group
The group resource manages the system groups from `/etc/group`.
## Hostname ## Hostname
The hostname resource manages static, transient/dynamic and pretty hostnames The hostname resource manages static, transient/dynamic and pretty hostnames
@@ -143,6 +169,10 @@ would expect.
The msg resource sends messages to the main log, or an external service such The msg resource sends messages to the main log, or an external service such
as systemd's journal. as systemd's journal.
## Net
The net resource manages a local network interface using netlink.
## Noop ## Noop
The noop resource does absolutely nothing. It does have some utility in testing The noop resource does absolutely nothing. It does have some utility in testing
@@ -164,13 +194,25 @@ different distributions because it uses the underlying packagekit facility which
supports different backends for different environments. This ensures that we supports different backends for different environments. This ensures that we
have great Debian (deb/dpkg) and Fedora (rpm/dnf) support simultaneously. have great Debian (deb/dpkg) and Fedora (rpm/dnf) support simultaneously.
## Print
The print resource prints messages to the console.
## Svc ## Svc
The service resource is still very WIP. Please help us my improving it! The service resource is still very WIP. Please help us by improving it!
## Test
The test resource is mostly harmless and is used for internal tests.
## Timer ## Timer
This resource needs better documentation. Please help us my improving it! This resource needs better documentation. Please help us by improving it!
## User
The user resource manages the system users from `/etc/passwd`.
## Virt ## Virt

View File

@@ -15,18 +15,61 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package engine
import ( import (
"fmt" "fmt"
"log"
) )
// EdgeableRes is the interface a resource must implement to support automatic
// edges. Both the vertices involved in an edge need to implement this for it to
// be able to work.
type EdgeableRes interface {
Res // implement everything in Res but add the additional requirements
// AutoEdgeMeta lets you get or set meta params for the automatic edges
// trait.
AutoEdgeMeta() *AutoEdgeMeta
// UIDs includes all params to make a unique identification of this
// object.
UIDs() []ResUID // most resources only return one
// AutoEdges returns a struct that implements the AutoEdge interface.
// This interface can be used to generate automatic edges to other
// resources.
AutoEdges() (AutoEdge, error)
}
// AutoEdgeMeta provides some parameters specific to automatic edges.
// TODO: currently this only supports disabling the feature per-resource, but in
// the future you could conceivably have some small pattern to control it better
type AutoEdgeMeta struct {
// Disabled specifies that automatic edges should be disabled for this
// resource.
Disabled bool
}
// Cmp compares two AutoEdgeMeta structs and determines if they're equivalent.
func (obj *AutoEdgeMeta) Cmp(aem *AutoEdgeMeta) error {
if obj.Disabled != aem.Disabled {
return fmt.Errorf("values for Disabled are different")
}
return nil
}
// The AutoEdge interface is used to implement the autoedges feature.
type AutoEdge interface {
Next() []ResUID // call to get list of edges to add
Test([]bool) bool // call until false
}
// ResUID is a unique identifier for a resource, namely it's name, and the kind ("type"). // ResUID is a unique identifier for a resource, namely it's name, and the kind ("type").
type ResUID interface { type ResUID interface {
fmt.Stringer // String() string
GetName() string GetName() string
GetKind() string GetKind() string
fmt.Stringer // String() string
IFF(ResUID) bool IFF(ResUID) bool
@@ -72,7 +115,7 @@ func (obj *BaseUID) IFF(uid ResUID) bool {
// happens before the generator. // happens before the generator.
func (obj *BaseUID) IsReversed() bool { func (obj *BaseUID) IsReversed() bool {
if obj.Reversed == nil { if obj.Reversed == nil {
log.Fatal("Programming error!") panic("programming error!")
} }
return *obj.Reversed return *obj.Reversed
} }

38
engine/autoedge_test.go Normal file
View File

@@ -0,0 +1,38 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package engine
import (
"testing"
)
func TestIFF1(t *testing.T) {
uid := &BaseUID{Name: "/tmp/unit-test"}
same := &BaseUID{Name: "/tmp/unit-test"}
diff := &BaseUID{Name: "/tmp/other-file"}
if !uid.IFF(same) {
t.Errorf("basic resource UIDs with the same name should satisfy each other's IFF condition")
}
if uid.IFF(diff) {
t.Errorf("basic resource UIDs with different names should NOT satisfy each other's IFF condition")
}
}

84
engine/autogroup.go Normal file
View File

@@ -0,0 +1,84 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"fmt"
"github.com/purpleidea/mgmt/pgraph"
)
// GroupableRes is the interface a resource must implement to support automatic
// grouping. Default implementations for most of the methods declared in this
// interface can be obtained for your resource by anonymously adding the
// traits.Groupable struct to your resource implementation.
type GroupableRes interface {
Res // implement everything in Res but add the additional requirements
// AutoGroupMeta lets you get or set meta params for the automatic
// grouping trait.
AutoGroupMeta() *AutoGroupMeta
// GroupCmp compares two resources and decides if they're suitable for
//grouping. This usually needs to be unique to your resource.
GroupCmp(res GroupableRes) error
// GroupRes groups resource argument (res) into self.
GroupRes(res GroupableRes) error
// IsGrouped determines if we are grouped.
IsGrouped() bool // am I grouped?
// SetGrouped sets a flag to tell if we are grouped.
SetGrouped(bool)
// GetGroup returns everyone grouped inside me.
GetGroup() []GroupableRes // return everyone grouped inside me
// SetGroup sets the grouped resources into me.
SetGroup([]GroupableRes)
}
// AutoGroupMeta provides some parameters specific to automatic grouping.
// TODO: currently this only supports disabling the feature per-resource, but in
// the future you could conceivably have some small pattern to control it better
type AutoGroupMeta struct {
// Disabled specifies that automatic grouping should be disabled for
// this resource.
Disabled bool
}
// Cmp compares two AutoGroupMeta structs and determines if they're equivalent.
func (obj *AutoGroupMeta) Cmp(agm *AutoGroupMeta) error {
if obj.Disabled != agm.Disabled {
return fmt.Errorf("values for Disabled are different")
}
return nil
}
// AutoGrouper is the required interface to implement an autogrouping algorithm.
type AutoGrouper interface {
// listed in the order these are typically called in...
Name() string // friendly identifier
Init(*pgraph.Graph) error // only call once
VertexNext() (pgraph.Vertex, pgraph.Vertex, error) // mostly algorithmic
VertexCmp(pgraph.Vertex, pgraph.Vertex) error // can we merge these ?
VertexMerge(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error) // vertex merge fn to use
EdgeMerge(pgraph.Edge, pgraph.Edge) pgraph.Edge // edge merge fn to use
VertexTest(bool) (bool, error) // call until false
}

126
engine/cmp.go Normal file
View File

@@ -0,0 +1,126 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"fmt"
"github.com/purpleidea/mgmt/pgraph"
)
// ResCmp compares two resources by checking multiple aspects. This is the main
// entry point for running all the compare steps on two resource.
func ResCmp(r1, r2 Res) error {
if r1.Kind() != r2.Kind() {
return fmt.Errorf("kind differs")
}
if r1.Name() != r2.Name() {
return fmt.Errorf("name differs")
}
if err := r1.Cmp(r2); err != nil {
return err
}
// compare meta params for resources with auto edges
r1e, ok1 := r1.(EdgeableRes)
r2e, ok2 := r2.(EdgeableRes)
if ok1 != ok2 {
return fmt.Errorf("edgeable differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1e.AutoEdgeMeta().Cmp(r2e.AutoEdgeMeta()) != nil {
return fmt.Errorf("autoedge differs")
}
}
// compare meta params for resources with auto grouping
r1g, ok1 := r1.(GroupableRes)
r2g, ok2 := r2.(GroupableRes)
if ok1 != ok2 {
return fmt.Errorf("groupable differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1g.AutoGroupMeta().Cmp(r2g.AutoGroupMeta()) != nil {
return fmt.Errorf("autogroup differs")
}
// if resources are grouped, are the groups the same?
if i, j := r1g.GetGroup(), r2g.GetGroup(); len(i) != len(j) {
return fmt.Errorf("autogroup groups differ")
} else if len(i) > 0 { // trick the golinter
// Sort works with Res, so convert the lists to that
iRes := []Res{}
for _, r := range i {
res := r.(Res)
iRes = append(iRes, res)
}
jRes := []Res{}
for _, r := range j {
res := r.(Res)
jRes = append(jRes, res)
}
ix, jx := Sort(iRes), Sort(jRes) // now sort :)
for k := range ix {
// compare sub resources
if err := ResCmp(ix[k], jx[k]); err != nil {
return err
}
}
}
}
return nil
}
// VertexCmpFn returns if two vertices are equivalent. It errors if they can't
// be compared because one is not a vertex. This returns true if equal.
// TODO: shouldn't the first argument be an `error` instead?
func VertexCmpFn(v1, v2 pgraph.Vertex) (bool, error) {
r1, ok := v1.(Res)
if !ok {
return false, fmt.Errorf("v1 is not a Res")
}
r2, ok := v2.(Res)
if !ok {
return false, fmt.Errorf("v2 is not a Res")
}
if ResCmp(r1, r2) != nil {
return false, nil
}
return true, nil
}
// EdgeCmpFn returns if two edges are equivalent. It errors if they can't be
// compared because one is not an edge. This returns true if equal.
// TODO: shouldn't the first argument be an `error` instead?
func EdgeCmpFn(e1, e2 pgraph.Edge) (bool, error) {
edge1, ok := e1.(*Edge)
if !ok {
return false, fmt.Errorf("e1 is not an Edge")
}
edge2, ok := e2.(*Edge)
if !ok {
return false, fmt.Errorf("e2 is not an Edge")
}
return edge1.Cmp(edge2) == nil, nil
}

View File

@@ -15,7 +15,11 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package engine
import (
"fmt"
)
// Edge is a struct that represents a graph's edge. // Edge is a struct that represents a graph's edge.
type Edge struct { type Edge struct {
@@ -30,19 +34,19 @@ func (obj *Edge) String() string {
return obj.Name return obj.Name
} }
// Compare returns true if two edges are equivalent. Otherwise it returns false. // Cmp compares this edge to another. It returns nil if they are equivalent.
func (obj *Edge) Compare(edge *Edge) bool { func (obj *Edge) Cmp(edge *Edge) error {
if obj.Name != edge.Name { if obj.Name != edge.Name {
return false return fmt.Errorf("edge names differ")
} }
if obj.Notify != edge.Notify { if obj.Notify != edge.Notify {
return false return fmt.Errorf("notify values differ")
} }
// FIXME: should we compare this as well? // FIXME: should we compare this as well?
//if obj.refresh != edge.refresh { //if obj.refresh != edge.refresh {
// return false // return fmt.Errorf("refresh values differ")
//} //}
return true return nil
} }
// Refresh returns the pending refresh status of this edge. // Refresh returns the pending refresh status of this edge.

32
engine/error.go Normal file
View File

@@ -0,0 +1,32 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
// Error is a constant error type that implements error.
type Error string
// Error fulfills the error interface of this type.
func (e Error) Error() string { return string(e) }
const (
// ErrWatchExit represents an exit from the Watch loop via chan closure.
ErrWatchExit = Error("watch exit")
// ErrSignalExit represents an exit from the Watch loop via exit signal.
ErrSignalExit = Error("signal exit")
)

33
engine/event/event.go Normal file
View File

@@ -0,0 +1,33 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package event provides some primitives that are used for message passing.
package event
//go:generate stringer -type=Kind -output=kind_stringer.go
// Kind represents the type of event being passed.
type Kind int
// The different event kinds are used in different contexts.
const (
EventNil Kind = iota
EventStart
EventPause
EventPoke
EventExit
)

View File

@@ -15,42 +15,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package engine
import ( import (
"os" "os"
"github.com/purpleidea/mgmt/etcd/scheduler"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
// World is an interface to the rest of the different graph state. It allows
// the GAPI to store state and exchange information throughout the cluster. It
// is the interface each machine uses to communicate with the rest of the world.
type World interface { // TODO: is there a better name for this interface?
ResWatch() chan error
ResExport([]Res) error
// FIXME: should this method take a "filter" data struct instead of many args?
ResCollect(hostnameFilter, kindFilter []string) ([]Res, error)
StrWatch(namespace string) chan error
StrIsNotExist(error) bool
StrGet(namespace string) (string, error)
StrSet(namespace, value string) error
StrDel(namespace string) error
// XXX: add the exchange primitives in here directly?
StrMapWatch(namespace string) chan error
StrMapGet(namespace string) (map[string]string, error)
StrMapSet(namespace, value string) error
StrMapDel(namespace string) error
Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error)
Fs(uri string) (Fs, error)
}
// from the ioutil package: // from the ioutil package:
// NopCloser(r io.Reader) io.ReadCloser // not implemented here // NopCloser(r io.Reader) io.ReadCloser // not implemented here
// ReadAll(r io.Reader) ([]byte, error) // ReadAll(r io.Reader) ([]byte, error)

474
engine/graph/actions.go Normal file
View File

@@ -0,0 +1,474 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"strings"
"sync"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/event"
"github.com/purpleidea/mgmt/pgraph"
//multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
"golang.org/x/time/rate"
)
// OKTimestamp returns true if this vertex can run right now.
func (obj *Engine) OKTimestamp(vertex pgraph.Vertex) bool {
return len(obj.BadTimestamps(vertex)) == 0
}
// BadTimestamps returns the list of vertices that are causing our timestamp to
// be bad.
func (obj *Engine) BadTimestamps(vertex pgraph.Vertex) []pgraph.Vertex {
vs := []pgraph.Vertex{}
ts := obj.state[vertex].timestamp
// these are all the vertices pointing TO vertex, eg: ??? -> vertex
for _, v := range obj.graph.IncomingGraphVertices(vertex) {
// If the vertex has a greater timestamp than any prerequisite,
// then we can't run right now. If they're equal (eg: initially
// with a value of 0) then we also can't run because we should
// let our pre-requisites go first.
t := obj.state[v].timestamp
if obj.Debug {
obj.Logf("OKTimestamp: %d >= %d (%s): !%t", ts, t, v.String(), ts >= t)
}
if ts >= t {
//return false
vs = append(vs, v)
}
}
return vs // formerly "true" if empty
}
// Process is the primary function to execute a particular vertex in the graph.
func (obj *Engine) Process(vertex pgraph.Vertex) error {
res, isRes := vertex.(engine.Res)
if !isRes {
return fmt.Errorf("vertex is not a Res")
}
// Engine Guarantee: Do not allow CheckApply to run while we are paused.
// This makes the resource able to know that synchronous channel sending
// to the main loop select in Watch from within CheckApply, will succeed
// without blocking because the resource went into a paused state. If we
// are using the Poll metaparam, then Watch will (of course) not be run.
// FIXME: should this lock be here, or wrapped right around CheckApply ?
obj.state[vertex].eventsLock.Lock() // this lock is taken within Event()
defer obj.state[vertex].eventsLock.Unlock()
// backpoke! (can be async)
if vs := obj.BadTimestamps(vertex); len(vs) > 0 {
// back poke in parallel (sync b/c of waitgroup)
for _, v := range obj.graph.IncomingGraphVertices(vertex) {
if !pgraph.VertexContains(v, vs) { // only poke what's needed
continue
}
go obj.state[v].Poke() // async
}
return nil // can't continue until timestamp is in sequence
}
// semaphores!
// These shouldn't ever block an exit, since the graph should eventually
// converge causing their them to unlock. More interestingly, since they
// run in a DAG alphabetically, there is no way to permanently deadlock,
// assuming that resources individually don't ever block from finishing!
// The exception is that semaphores with a zero count will always block!
// TODO: Add a close mechanism to close/unblock zero count semaphores...
semas := res.MetaParams().Sema
if obj.Debug && len(semas) > 0 {
obj.Logf("%s: Sema: P(%s)", res, strings.Join(semas, ", "))
}
if err := obj.semaLock(semas); err != nil { // lock
// NOTE: in practice, this might not ever be truly necessary...
return fmt.Errorf("shutdown of semaphores")
}
defer obj.semaUnlock(semas) // unlock
if obj.Debug && len(semas) > 0 {
defer obj.Logf("%s: Sema: V(%s)", res, strings.Join(semas, ", "))
}
// sendrecv!
// connect any senders to receivers and detect if values changed
if res, ok := vertex.(engine.RecvableRes); ok {
if updated, err := obj.SendRecv(res); err != nil {
return errwrap.Wrapf(err, "could not SendRecv")
} else if len(updated) > 0 {
for _, changed := range updated {
if changed { // at least one was updated
// invalidate cache, mark as dirty
obj.state[vertex].isStateOK = false
break
}
}
// re-validate after we change any values
if err := engine.Validate(res); err != nil {
return errwrap.Wrapf(err, "failed Validate after SendRecv")
}
}
}
var ok = true
var applied = false // did we run an apply?
var noop = res.MetaParams().Noop // lookup the noop value
var refresh bool
var checkOK bool
var err error
// lookup the refresh (notification) variable
refresh = obj.RefreshPending(vertex) // do i need to perform a refresh?
refreshableRes, isRefreshableRes := vertex.(engine.RefreshableRes)
if isRefreshableRes {
refreshableRes.SetRefresh(refresh) // tell the resource
}
// Check cached state, to skip CheckApply, but can't skip if refreshing!
// If the resource doesn't implement refresh, skip the refresh test.
// FIXME: if desired, check that we pass through refresh notifications!
if (!refresh || !isRefreshableRes) && obj.state[vertex].isStateOK {
checkOK, err = true, nil
} else if noop && (refresh && isRefreshableRes) { // had a refresh to do w/ noop!
checkOK, err = false, nil // therefore the state is wrong
// run the CheckApply!
} else {
obj.Logf("%s: CheckApply(%t)", res, !noop)
// if this fails, don't UpdateTimestamp()
checkOK, err = res.CheckApply(!noop)
obj.Logf("%s: CheckApply(%t): Return(%t, %+v)", res, !noop, checkOK, err)
}
if checkOK && err != nil { // should never return this way
return fmt.Errorf("%s: resource programming error: CheckApply(%t): %t, %+v", res, !noop, checkOK, err)
}
if !checkOK { // something changed, restart timer
obj.state[vertex].cuid.ResetTimer() // activity!
if obj.Debug {
obj.Logf("%s: converger: reset timer", res)
}
}
// if CheckApply ran without noop and without error, state should be good
if !noop && err == nil { // aka !noop || checkOK
obj.state[vertex].isStateOK = true // reset
if refresh {
obj.SetUpstreamRefresh(vertex, false) // refresh happened, clear the request
if isRefreshableRes {
refreshableRes.SetRefresh(false)
}
}
}
if !checkOK { // if state *was* not ok, we had to have apply'ed
if err != nil { // error during check or apply
ok = false
} else {
applied = true
}
}
// when noop is true we always want to update timestamp
if noop && err == nil {
ok = true
}
if ok {
// did we actually do work?
activity := applied
if noop {
activity = false // no we didn't do work...
}
if activity { // add refresh flag to downstream edges...
obj.SetDownstreamRefresh(vertex, true)
}
// poke! (should (must?) be sync)
wg := &sync.WaitGroup{}
// update this timestamp *before* we poke or the poked
// nodes might fail due to having a too old timestamp!
obj.state[vertex].timestamp = time.Now().UnixNano() // update timestamp
for _, v := range obj.graph.OutgoingGraphVertices(vertex) {
if !obj.OKTimestamp(v) {
// there is at least another one that will poke this...
continue
}
// If we're pausing (or exiting) then we can skip poking
// so that the graph doesn't go on running forever until
// it's completely done. This is an optional feature and
// we can select it via ^C on user exit or via the GAPI.
if obj.fastPause {
obj.Logf("%s: fast pausing, poke skipped", res)
continue
}
// poke each vertex individually, in parallel...
wg.Add(1)
go func(vv pgraph.Vertex) {
defer wg.Done()
obj.state[vv].Poke()
}(v)
}
wg.Wait()
}
return errwrap.Wrapf(err, "error during Process()")
}
// Worker is the common run frontend of the vertex. It handles all of the retry
// and retry delay common code, and ultimately returns the final status of this
// vertex execution.
func (obj *Engine) Worker(vertex pgraph.Vertex) error {
res, isRes := vertex.(engine.Res)
if !isRes {
return fmt.Errorf("vertex is not a resource")
}
defer close(obj.state[vertex].stopped) // done signal
obj.state[vertex].cuid = obj.Converger.Register()
// must wait for all users of the cuid to finish *before* we unregister!
// as a result, this defer happens *before* the below wait group Wait...
defer obj.state[vertex].cuid.Unregister()
defer obj.state[vertex].wg.Wait() // this Worker is the last to exit!
obj.state[vertex].wg.Add(1)
go func() {
defer obj.state[vertex].wg.Done()
defer close(obj.state[vertex].outputChan) // we close this on behalf of res
var err error
var retry = res.MetaParams().Retry // lookup the retry value
var delay uint64
for { // retry loop
// a retry-delay was requested, wait, but don't block events!
if delay > 0 {
errDelayExpired := engine.Error("delay exit")
err = func() error { // slim watch main loop
timer := time.NewTimer(time.Duration(delay) * time.Millisecond)
defer obj.state[vertex].init.Logf("the Watch delay expired!")
defer timer.Stop() // it's nice to cleanup
for {
select {
case <-timer.C: // the wait is over
return errDelayExpired // special
case event, ok := <-obj.state[vertex].init.Events:
if !ok {
return nil
}
if err := obj.state[vertex].init.Read(event); err != nil {
return err
}
}
}
}()
if err == errDelayExpired {
delay = 0 // reset
continue
}
} else if interval := res.MetaParams().Poll; interval > 0 { // poll instead of watching :(
obj.state[vertex].cuid.StartTimer()
err = obj.state[vertex].poll(interval)
obj.state[vertex].cuid.StopTimer() // clean up nicely
} else {
obj.state[vertex].cuid.StartTimer()
obj.Logf("Watch(%s)", vertex)
err = res.Watch() // run the watch normally
obj.Logf("Watch(%s): Exited(%+v)", vertex, err)
obj.state[vertex].cuid.StopTimer() // clean up nicely
}
if err == nil || err == engine.ErrWatchExit || err == engine.ErrSignalExit {
return // exited cleanly, we're done
}
// we've got an error...
delay = res.MetaParams().Delay
if retry < 0 { // infinite retries
obj.state[vertex].reset()
continue
}
if retry > 0 { // don't decrement past 0
retry--
obj.state[vertex].init.Logf("retrying Watch after %.4f seconds (%d left)", float64(delay)/1000, retry)
obj.state[vertex].reset()
continue
}
//if retry == 0 { // optional
// err = errwrap.Wrapf(err, "permanent watch error")
//}
break // break out of this and send the error
}
// this section sends an error...
// If the CheckApply loop exits and THEN the Watch fails with an
// error, then we'd be stuck here if exit signal didn't unblock!
select {
case obj.state[vertex].outputChan <- errwrap.Wrapf(err, "watch failed"):
// send
case <-obj.state[vertex].exit.Signal():
// pass
}
}()
// bonus safety check
if res.MetaParams().Burst == 0 && !(res.MetaParams().Limit == rate.Inf) { // blocked
return fmt.Errorf("permanently limited (rate != Inf, burst = 0)")
}
var limiter = rate.NewLimiter(res.MetaParams().Limit, res.MetaParams().Burst)
// It is important that we shutdown the Watch loop if this exits.
// Example, if Process errors permanently, we should ask Watch to exit.
defer obj.state[vertex].Event(event.EventExit) // signal an exit
for {
select {
case err, ok := <-obj.state[vertex].outputChan: // read from watch channel
if !ok {
return nil
}
if err != nil {
return err // permanent failure
}
// safe to go run the process...
case <-obj.state[vertex].exit.Signal(): // TODO: is this needed?
return nil
}
now := time.Now()
r := limiter.ReserveN(now, 1) // one event
// r.OK() seems to always be true here!
d := r.DelayFrom(now)
if d > 0 { // delay
obj.state[vertex].init.Logf("limited (rate: %v/sec, burst: %d, next: %v)", res.MetaParams().Limit, res.MetaParams().Burst, d)
var count int
timer := time.NewTimer(time.Duration(d) * time.Millisecond)
LimitWait:
for {
select {
case <-timer.C: // the wait is over
break LimitWait
// consume other events while we're waiting...
case e, ok := <-obj.state[vertex].outputChan: // read from watch channel
if !ok {
// FIXME: is this logic correct?
if count == 0 {
return nil
}
// loop, because we have
// the previous event to
// run process on first!
continue
}
if e != nil {
return e // permanent failure
}
count++ // count the events...
limiter.ReserveN(time.Now(), 1) // one event
}
}
timer.Stop() // it's nice to cleanup
obj.state[vertex].init.Logf("rate limiting expired!")
}
var err error
var retry = res.MetaParams().Retry // lookup the retry value
var delay uint64
Loop:
for { // retry loop
if delay > 0 {
var count int
timer := time.NewTimer(time.Duration(delay) * time.Millisecond)
RetryWait:
for {
select {
case <-timer.C: // the wait is over
break RetryWait
// consume other events while we're waiting...
case e, ok := <-obj.state[vertex].outputChan: // read from watch channel
if !ok {
// FIXME: is this logic correct?
if count == 0 {
// last process error
return err
}
// loop, because we have
// the previous event to
// run process on first!
continue
}
if e != nil {
return e // permanent failure
}
count++ // count the events...
limiter.ReserveN(time.Now(), 1) // one event
}
}
timer.Stop() // it's nice to cleanup
delay = 0 // reset
obj.state[vertex].init.Logf("the CheckApply delay expired!")
}
if obj.Debug {
obj.Logf("Process(%s)", vertex)
}
err = obj.Process(vertex)
if obj.Debug {
obj.Logf("Process(%s): Return(%+v)", vertex, err)
}
if err == nil {
break Loop
}
// we've got an error...
delay = res.MetaParams().Delay
if retry < 0 { // infinite retries
continue
}
if retry > 0 { // don't decrement past 0
retry--
obj.state[vertex].init.Logf("retrying CheckApply after %.4f seconds (%d left)", float64(delay)/1000, retry)
continue
}
//if retry == 0 { // optional
// err = errwrap.Wrapf(err, "permanent process error")
//}
// If this exits, defer calls Event(event.EventExit),
// which will cause the Watch loop to shutdown. Also,
// if the Watch loop shuts down, that will cause this
// Process loop to shut down. Also the graph sync can
// run an Event(event.EventExit) which causes this to
// shutdown as well. Lastly, it is possible that more
// that one of these scenarios happens simultaneously.
return err
}
}
//return nil // unreachable
}

30
engine/graph/autoedge.go Normal file
View File

@@ -0,0 +1,30 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"github.com/purpleidea/mgmt/engine/graph/autoedge"
)
// AutoEdge adds the automatic edges to the graph.
func (obj *Engine) AutoEdge() error {
logf := func(format string, v ...interface{}) {
obj.Logf("autoedge: "+format, v...)
}
return autoedge.AutoEdge(obj.nextGraph, obj.Debug, logf)
}

View File

@@ -15,38 +15,88 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package autoedge
import ( import (
"fmt" "fmt"
"log"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util"
multierr "github.com/hashicorp/go-multierror" multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
// The AutoEdge interface is used to implement the autoedges feature. // AutoEdge adds the automatic edges to the graph.
type AutoEdge interface { func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
Next() []ResUID // call to get list of edges to add logf("adding autoedges...")
Test([]bool) bool // call until false
}
// UIDExistsInUIDs wraps the IFF method when used with a list of UID's. // initially get all of the autoedges to seek out all possible errors
func UIDExistsInUIDs(uid ResUID, uids []ResUID) bool { var err error
for _, u := range uids { autoEdgeObjMap := make(map[engine.EdgeableRes]engine.AutoEdge)
if uid.IFF(u) { sorted := []engine.EdgeableRes{}
return true for _, v := range graph.VerticesSorted() {
res, ok := v.(engine.EdgeableRes)
if !ok {
continue
}
if res.AutoEdgeMeta().Disabled { // skip if this res is disabled
continue
}
sorted = append(sorted, res)
}
for _, res := range sorted { // for each vertexes autoedges
autoEdgeObj, e := res.AutoEdges()
if e != nil {
err = multierr.Append(err, e) // collect all errors
continue
}
if autoEdgeObj == nil {
logf("no auto edges were found for: %s", res)
continue // next vertex
}
autoEdgeObjMap[res] = autoEdgeObj // save for next loop
}
if err != nil {
return errwrap.Wrapf(err, "the auto edges had errors")
}
// now that we're guaranteed error free, we can modify the graph safely
for _, res := range sorted { // stable sort order for determinism in logs
autoEdgeObj, exists := autoEdgeObjMap[res]
if !exists {
continue
}
for { // while the autoEdgeObj has more uids to add...
uids := autoEdgeObj.Next() // get some!
if uids == nil {
logf("the auto edge list is empty for: %s", res)
break // inner loop
}
if debug {
logf("autoedge: UIDS:")
for i, u := range uids {
logf("autoedge: UID%d: %v", i, u)
} }
} }
return false
// match and add edges
result := addEdgesByMatchingUIDS(res, uids, graph, debug, logf)
// report back, and find out if we should continue
if !autoEdgeObj.Test(result) {
break
}
}
}
return nil
} }
// addEdgesByMatchingUIDS adds edges to the vertex in a graph based on if it // addEdgesByMatchingUIDS adds edges to the vertex in a graph based on if it
// matches a uid list. // matches a uid list.
func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []bool { func addEdgesByMatchingUIDS(res engine.EdgeableRes, uids []engine.ResUID, graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) []bool {
// search for edges and see what matches! // search for edges and see what matches!
var result []bool var result []bool
@@ -54,29 +104,36 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
for _, uid := range uids { for _, uid := range uids {
var found = false var found = false
// uid is a ResUID object // uid is a ResUID object
for _, vv := range g.Vertices() { // search for _, v := range graph.Vertices() { // search
if v == vv { // skip self r, ok := v.(engine.EdgeableRes)
if !ok {
continue continue
} }
if b, ok := g.Value("debug"); ok && util.Bool(b) { if r.AutoEdgeMeta().Disabled { // skip if this res is disabled
log.Printf("Compile: AutoEdge: Match: %s with UID: %s", vv, uid) continue
}
if res == r { // skip self
continue
}
if debug {
logf("autoedge: Match: %s with UID: %s", r, uid)
} }
// we must match to an effective UID for the resource, // we must match to an effective UID for the resource,
// that is to say, the name value of a res is a helpful // that is to say, the name value of a res is a helpful
// handle, but it is not necessarily a unique identity! // handle, but it is not necessarily a unique identity!
// remember, resources can return multiple UID's each! // remember, resources can return multiple UID's each!
if UIDExistsInUIDs(uid, VtoR(vv).UIDs()) { if UIDExistsInUIDs(uid, r.UIDs()) {
// add edge from: vv -> v // add edge from: r -> res
if uid.IsReversed() { if uid.IsReversed() {
txt := fmt.Sprintf("AutoEdge: %s -> %s", vv, v) txt := fmt.Sprintf("%s -> %s (autoedge)", r, res)
log.Printf("Compile: Adding %s", txt) logf("autoedge: adding: %s", txt)
edge := &Edge{Name: txt} edge := &engine.Edge{Name: txt}
g.AddEdge(vv, v, edge) graph.AddEdge(r, res, edge)
} else { // edges go the "normal" way, eg: pkg resource } else { // edges go the "normal" way, eg: pkg resource
txt := fmt.Sprintf("AutoEdge: %s -> %s", v, vv) txt := fmt.Sprintf("%s -> %s (autoedge)", res, r)
log.Printf("Compile: Adding %s", txt) logf("autoedge: adding: %s", txt)
edge := &Edge{Name: txt} edge := &engine.Edge{Name: txt}
g.AddEdge(v, vv, edge) graph.AddEdge(res, r, edge)
} }
found = true found = true
break break
@@ -87,62 +144,12 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
return result return result
} }
// AutoEdges adds the automatic edges to the graph. // UIDExistsInUIDs wraps the IFF method when used with a list of UID's.
func AutoEdges(g *pgraph.Graph) error { func UIDExistsInUIDs(uid engine.ResUID, uids []engine.ResUID) bool {
log.Println("Compile: Adding AutoEdges...") for _, u := range uids {
if uid.IFF(u) {
// initially get all of the autoedges to seek out all possible errors return true
var err error
autoEdgeObjVertexMap := make(map[pgraph.Vertex]AutoEdge)
sorted := g.VerticesSorted()
for _, v := range sorted { // for each vertexes autoedges
if !VtoR(v).Meta().AutoEdge { // is the metaparam true?
continue
}
autoEdgeObj, e := VtoR(v).AutoEdges()
if e != nil {
err = multierr.Append(err, e) // collect all errors
continue
}
if autoEdgeObj == nil {
log.Printf("%s: No auto edges were found!", v)
continue // next vertex
}
autoEdgeObjVertexMap[v] = autoEdgeObj // save for next loop
}
if err != nil {
return errwrap.Wrapf(err, "the auto edges had errors")
}
// now that we're guaranteed error free, we can modify the graph safely
for _, v := range sorted { // stable sort order for determinism in logs
autoEdgeObj, exists := autoEdgeObjVertexMap[v]
if !exists {
continue
}
for { // while the autoEdgeObj has more uids to add...
uids := autoEdgeObj.Next() // get some!
if uids == nil {
log.Printf("%s: The auto edge list is empty!", v)
break // inner loop
}
if b, ok := g.Value("debug"); ok && util.Bool(b) {
log.Println("Compile: AutoEdge: UIDS:")
for i, u := range uids {
log.Printf("Compile: AutoEdge: UID%d: %v", i, u)
} }
} }
return false
// match and add edges
result := addEdgesByMatchingUIDS(g, v, uids)
// report back, and find out if we should continue
if !autoEdgeObj.Test(result) {
break
}
}
}
return nil
} }

141
engine/graph/autogroup.go Normal file
View File

@@ -0,0 +1,141 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autogroup"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
)
// AutoGroup runs the auto grouping on the loaded graph.
func (obj *Engine) AutoGroup(ag engine.AutoGrouper) error {
if obj.nextGraph == nil {
return fmt.Errorf("there is no active graph to autogroup")
}
logf := func(format string, v ...interface{}) {
obj.Logf("autogroup: "+format, v...)
}
// wrap ag with our own vertexCmp, vertexMerge and edgeMerge
wrapped := &wrappedGrouper{
AutoGrouper: ag, // pass in the existing autogrouper
}
if err := autogroup.AutoGroup(wrapped, obj.nextGraph, obj.Debug, logf); err != nil {
return errwrap.Wrapf(err, "autogrouping failed")
}
return nil
}
// wrappedGrouper is an autogrouper which adds our own Cmp and Merge functions
// on top of the desired AutoGrouper that was specified.
type wrappedGrouper struct {
engine.AutoGrouper // anonymous interface
}
func (obj *wrappedGrouper) Name() string {
return fmt.Sprintf("wrappedGrouper: %s", obj.AutoGrouper.Name())
}
func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
// call existing vertexCmp first
if err := obj.AutoGrouper.VertexCmp(v1, v2); err != nil {
return err
}
r1, ok := v1.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v1 is not a GroupableRes")
}
r2, ok := v2.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v2 is not a GroupableRes")
}
if r1.Kind() != r2.Kind() { // we must group similar kinds
// TODO: maybe future resources won't need this limitation?
return fmt.Errorf("the two resources aren't the same kind")
}
// someone doesn't want to group!
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
return fmt.Errorf("one of the autogroup flags is false")
}
if r1.IsGrouped() { // already grouped!
return fmt.Errorf("already grouped")
}
if len(r2.GetGroup()) > 0 { // already has children grouped!
return fmt.Errorf("already has groups")
}
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
return errwrap.Wrapf(err, "the GroupCmp failed")
}
return nil
}
func (obj *wrappedGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
r1, ok := v1.(engine.GroupableRes)
if !ok {
return nil, fmt.Errorf("v1 is not a GroupableRes")
}
r2, ok := v2.(engine.GroupableRes)
if !ok {
return nil, fmt.Errorf("v2 is not a GroupableRes")
}
if err = r1.GroupRes(r2); err != nil { // GroupRes skips stupid groupings
return // return early on error
}
// merging two resources into one should yield the sum of their semas
if semas := r2.MetaParams().Sema; len(semas) > 0 {
r1.MetaParams().Sema = append(r1.MetaParams().Sema, semas...)
r1.MetaParams().Sema = util.StrRemoveDuplicatesInList(r1.MetaParams().Sema)
}
return // success or fail, and no need to merge the actual vertices!
}
func (obj *wrappedGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
e1x, ok := e1.(*engine.Edge)
if !ok {
return e2 // just return something to avoid needing to error
}
e2x, ok := e2.(*engine.Edge)
if !ok {
return e1 // just return something to avoid needing to error
}
// TODO: should we merge the edge.Notify or edge.refresh values?
edge := &engine.Edge{
Notify: e1x.Notify || e2x.Notify, // TODO: should we merge this?
}
refresh := e1x.Refresh() || e2x.Refresh() // TODO: should we merge this?
edge.SetRefresh(refresh)
return edge
}

View File

@@ -0,0 +1,71 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
errwrap "github.com/pkg/errors"
)
// AutoGroup is the mechanical auto group "runner" that runs the interface spec.
// TODO: this algorithm may not be correct in all cases. replace if needed!
func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
logf("algorithm: %s...", ag.Name())
if err := ag.Init(g); err != nil {
return errwrap.Wrapf(err, "error running autoGroup(init)")
}
for {
var v, w pgraph.Vertex
v, w, err := ag.VertexNext() // get pair to compare
if err != nil {
return errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
}
merged := false
// save names since they change during the runs
vStr := fmt.Sprintf("%v", v) // valid even if it is nil
wStr := fmt.Sprintf("%v", w)
if err := ag.VertexCmp(v, w); err != nil { // cmp ?
if debug {
logf("!GroupCmp for: %s into: %s", wStr, vStr)
}
// remove grouped vertex and merge edges (res is safe)
} else if err := VertexMerge(g, v, w, ag.VertexMerge, ag.EdgeMerge); err != nil { // merge...
logf("!VertexMerge for: %s into: %s", wStr, vStr)
} else { // success!
logf("success for: %s into: %s", wStr, vStr)
merged = true // woo
}
// did these get used?
if ok, err := ag.VertexTest(merged); err != nil {
return errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
} else if !ok {
break // done!
}
}
return nil
}

View File

@@ -15,7 +15,9 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources // +build !root
package autogroup
import ( import (
"fmt" "fmt"
@@ -25,13 +27,114 @@ import (
"testing" "testing"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
) )
func init() {
engine.RegisterResource("nooptest", func() engine.Res { return &NoopResTest{} })
}
// NoopResTest is a no-op resource that groups strangely.
type NoopResTest struct {
traits.Base // add the base methods without re-implementation
traits.Groupable
init *engine.Init
Comment string
}
func (obj *NoopResTest) Default() engine.Res {
return &NoopResTest{}
}
func (obj *NoopResTest) Validate() error {
return nil
}
func (obj *NoopResTest) Init(init *engine.Init) error {
obj.init = init // save for later
return nil
}
func (obj *NoopResTest) Close() error {
return nil
}
func (obj *NoopResTest) Watch() error {
return nil // not needed
}
func (obj *NoopResTest) CheckApply(apply bool) (checkOK bool, err error) {
return true, nil // state is always okay
}
func (obj *NoopResTest) Cmp(r engine.Res) error {
// we can only compare NoopRes to others of the same resource kind
res, ok := r.(*NoopResTest)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Comment != res.Comment {
return fmt.Errorf("comment differs")
}
return nil
}
func (obj *NoopResTest) GroupCmp(r engine.GroupableRes) error {
res, ok := r.(*NoopResTest)
if !ok {
return fmt.Errorf("resource is not the same kind")
}
// TODO: implement this in vertexCmp for *testGrouper instead?
if strings.Contains(res.Name(), ",") { // HACK
return fmt.Errorf("already grouped") // element to be grouped is already grouped!
}
// group if they start with the same letter! (helpful hack for testing)
if obj.Name()[0] != res.Name()[0] {
return fmt.Errorf("different starting letter")
}
return nil
}
func NewNoopResTest(name string) *NoopResTest {
n, err := engine.NewNamedResource("nooptest", name)
if err != nil {
panic(fmt.Sprintf("unexpected error: %+v", err))
}
//x := n.(*resources.NoopRes)
g, ok := n.(engine.GroupableRes)
if !ok {
panic("not a GroupableRes")
}
g.AutoGroupMeta().Disabled = false // always autogroup
//x := g.(*NoopResTest)
x := n.(*NoopResTest)
return x
}
func NewNoopResTestSema(name string, semas []string) *NoopResTest {
n := NewNoopResTest(name)
n.MetaParams().Sema = semas
return n
}
// NE is a helper function to make testing easier. It creates a new noop edge. // NE is a helper function to make testing easier. It creates a new noop edge.
func NE(s string) pgraph.Edge { func NE(s string) pgraph.Edge {
obj := &Edge{Name: s} obj := &engine.Edge{Name: s}
return obj return obj
} }
@@ -40,41 +143,96 @@ type testGrouper struct {
NonReachabilityGrouper // "inherit" what we want, and reimplement the rest NonReachabilityGrouper // "inherit" what we want, and reimplement the rest
} }
func (ag *testGrouper) name() string { func (obj *testGrouper) Name() string {
return "testGrouper" return "testGrouper"
} }
func (ag *testGrouper) vertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) { func (obj *testGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
if err := VtoR(v1).GroupRes(VtoR(v2)); err != nil { // group them first // call existing vertexCmp first
if err := obj.NonReachabilityGrouper.VertexCmp(v1, v2); err != nil {
return err
}
r1, ok := v1.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v1 is not a GroupableRes")
}
r2, ok := v2.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v2 is not a GroupableRes")
}
if r1.Kind() != r2.Kind() { // we must group similar kinds
// TODO: maybe future resources won't need this limitation?
return fmt.Errorf("the two resources aren't the same kind")
}
// someone doesn't want to group!
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
return fmt.Errorf("one of the autogroup flags is false")
}
if r1.IsGrouped() { // already grouped!
return fmt.Errorf("already grouped")
}
if len(r2.GetGroup()) > 0 { // already has children grouped!
return fmt.Errorf("already has groups")
}
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
return errwrap.Wrapf(err, "the GroupCmp failed")
}
return nil
}
func (obj *testGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
r1 := v1.(engine.GroupableRes)
r2 := v2.(engine.GroupableRes)
if err := r1.GroupRes(r2); err != nil { // group them first
return nil, err return nil, err
} }
// HACK: update the name so it matches full list of self+grouped // HACK: update the name so it matches full list of self+grouped
obj := VtoR(v1) res := v1.(engine.GroupableRes)
names := strings.Split(obj.GetName(), ",") // load in stored names names := strings.Split(res.Name(), ",") // load in stored names
for _, n := range obj.GetGroup() { for _, n := range res.GetGroup() {
names = append(names, n.GetName()) // add my contents names = append(names, n.Name()) // add my contents
} }
names = util.StrRemoveDuplicatesInList(names) // remove duplicates names = util.StrRemoveDuplicatesInList(names) // remove duplicates
sort.Strings(names) sort.Strings(names)
obj.SetName(strings.Join(names, ",")) res.SetName(strings.Join(names, ","))
// TODO: copied from autogroup.go, so try and build a better test...
// merging two resources into one should yield the sum of their semas
if semas := r2.MetaParams().Sema; len(semas) > 0 {
r1.MetaParams().Sema = append(r1.MetaParams().Sema, semas...)
r1.MetaParams().Sema = util.StrRemoveDuplicatesInList(r1.MetaParams().Sema)
}
return // success or fail, and no need to merge the actual vertices! return // success or fail, and no need to merge the actual vertices!
} }
func (ag *testGrouper) edgeMerge(e1, e2 pgraph.Edge) pgraph.Edge { func (obj *testGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
edge1 := e1.(*Edge) // panic if wrong edge1 := e1.(*engine.Edge) // panic if wrong
edge2 := e2.(*Edge) // panic if wrong edge2 := e2.(*engine.Edge) // panic if wrong
// HACK: update the name so it makes a union of both names // HACK: update the name so it makes a union of both names
n1 := strings.Split(edge1.Name, ",") // load n1 := strings.Split(edge1.Name, ",") // load
n2 := strings.Split(edge2.Name, ",") // load n2 := strings.Split(edge2.Name, ",") // load
names := append(n1, n2...) names := append(n1, n2...)
names = util.StrRemoveDuplicatesInList(names) // remove duplicates names = util.StrRemoveDuplicatesInList(names) // remove duplicates
sort.Strings(names) sort.Strings(names)
return &Edge{Name: strings.Join(names, ",")} return &engine.Edge{Name: strings.Join(names, ",")}
} }
// helper function // helper function
func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) { func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) {
AutoGroup(g1, &testGrouper{}) // edits the graph debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
if err := AutoGroup(&testGrouper{}, g1, debug, logf); err != nil { // edits the graph
t.Errorf("%v", err)
return
}
err := GraphCmp(g1, g2) err := GraphCmp(g1, g2)
if err != nil { if err != nil {
t.Logf(" actual (g1): %v%v", g1, fullPrint(g1)) t.Logf(" actual (g1): %v%v", g1, fullPrint(g1))
@@ -84,40 +242,6 @@ func runGraphCmp(t *testing.T, g1, g2 *pgraph.Graph) {
} }
} }
type NoopResTest struct {
NoopRes
}
func (obj *NoopResTest) GroupCmp(r Res) bool {
res, ok := r.(*NoopResTest)
if !ok {
return false
}
// TODO: implement this in vertexCmp for *testGrouper instead?
if strings.Contains(res.Name, ",") { // HACK
return false // element to be grouped is already grouped!
}
// group if they start with the same letter! (helpful hack for testing)
return obj.Name[0] == res.Name[0]
}
func NewNoopResTest(name string) *NoopResTest {
obj := &NoopResTest{
NoopRes: NoopRes{
BaseRes: BaseRes{
Name: name,
Kind: "noop",
MetaParams: MetaParams{
AutoGroup: true, // always autogroup
},
},
},
}
return obj
}
// GraphCmp compares the topology of two graphs and returns nil if they're // GraphCmp compares the topology of two graphs and returns nil if they're
// equal. It also compares if grouped element groups are identical. // equal. It also compares if grouped element groups are identical.
// TODO: port this to use the pgraph.GraphCmp function instead. // TODO: port this to use the pgraph.GraphCmp function instead.
@@ -133,20 +257,20 @@ func GraphCmp(g1, g2 *pgraph.Graph) error {
Loop: Loop:
// check vertices // check vertices
for v1 := range g1.Adjacency() { // for each vertex in g1 for v1 := range g1.Adjacency() { // for each vertex in g1
r1 := v1.(engine.GroupableRes)
l1 := strings.Split(VtoR(v1).GetName(), ",") // make list of everyone's names... l1 := strings.Split(r1.Name(), ",") // make list of everyone's names...
for _, x1 := range VtoR(v1).GetGroup() { for _, x1 := range r1.GetGroup() {
l1 = append(l1, x1.GetName()) // add my contents l1 = append(l1, x1.Name()) // add my contents
} }
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
sort.Strings(l1) sort.Strings(l1)
// inner loop // inner loop
for v2 := range g2.Adjacency() { // does it match in g2 ? for v2 := range g2.Adjacency() { // does it match in g2 ?
r2 := v2.(engine.GroupableRes)
l2 := strings.Split(VtoR(v2).GetName(), ",") l2 := strings.Split(r2.Name(), ",")
for _, x2 := range VtoR(v2).GetGroup() { for _, x2 := range r2.GetGroup() {
l2 = append(l2, x2.GetName()) l2 = append(l2, x2.Name())
} }
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
sort.Strings(l2) sort.Strings(l2)
@@ -157,7 +281,7 @@ Loop:
continue Loop continue Loop
} }
} }
return fmt.Errorf("graph g1, has no match in g2 for: %v", VtoR(v1).GetName()) return fmt.Errorf("graph g1, has no match in g2 for: %v", r1.Name())
} }
// vertices (and groups) match :) // vertices (and groups) match :)
@@ -166,35 +290,40 @@ Loop:
v2 := m[v1] // lookup in map to get correspondance v2 := m[v1] // lookup in map to get correspondance
// g1.Adjacency()[v1] corresponds to g2.Adjacency()[v2] // g1.Adjacency()[v1] corresponds to g2.Adjacency()[v2]
if e1, e2 := len(g1.Adjacency()[v1]), len(g2.Adjacency()[v2]); e1 != e2 { if e1, e2 := len(g1.Adjacency()[v1]), len(g2.Adjacency()[v2]); e1 != e2 {
return fmt.Errorf("graph g1, vertex(%v) has %d edges, while g2, vertex(%v) has %d", VtoR(v1).GetName(), e1, VtoR(v2).GetName(), e2) r1 := v1.(engine.Res)
r2 := v2.(engine.Res)
return fmt.Errorf("graph g1, vertex(%v) has %d edges, while g2, vertex(%v) has %d", r1.Name(), e1, r2.Name(), e2)
} }
for vv1, ee1 := range g1.Adjacency()[v1] { for vv1, ee1 := range g1.Adjacency()[v1] {
vv2 := m[vv1] vv2 := m[vv1]
ee1 := ee1.(*Edge) ee1 := ee1.(*engine.Edge)
ee2 := g2.Adjacency()[v2][vv2].(*Edge) ee2 := g2.Adjacency()[v2][vv2].(*engine.Edge)
// these are edges from v1 -> vv1 via ee1 (graph 1) // these are edges from v1 -> vv1 via ee1 (graph 1)
// to cmp to edges from v2 -> vv2 via ee2 (graph 2) // to cmp to edges from v2 -> vv2 via ee2 (graph 2)
// check: (1) vv1 == vv2 ? (we've already checked this!) // check: (1) vv1 == vv2 ? (we've already checked this!)
l1 := strings.Split(VtoR(vv1).GetName(), ",") // make list of everyone's names... rr1 := vv1.(engine.GroupableRes)
for _, x1 := range VtoR(vv1).GetGroup() { rr2 := vv2.(engine.GroupableRes)
l1 = append(l1, x1.GetName()) // add my contents
l1 := strings.Split(rr1.Name(), ",") // make list of everyone's names...
for _, x1 := range rr1.GetGroup() {
l1 = append(l1, x1.Name()) // add my contents
} }
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
sort.Strings(l1) sort.Strings(l1)
l2 := strings.Split(VtoR(vv2).GetName(), ",") l2 := strings.Split(rr2.Name(), ",")
for _, x2 := range VtoR(vv2).GetGroup() { for _, x2 := range rr2.GetGroup() {
l2 = append(l2, x2.GetName()) l2 = append(l2, x2.Name())
} }
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
sort.Strings(l2) sort.Strings(l2)
// does l1 match l2 ? // does l1 match l2 ?
if !ListStrCmp(l1, l2) { // cmp! if !ListStrCmp(l1, l2) { // cmp!
return fmt.Errorf("graph g1 and g2 don't agree on: %v and %v", VtoR(vv1).GetName(), VtoR(vv2).GetName()) return fmt.Errorf("graph g1 and g2 don't agree on: %v and %v", rr1.Name(), rr2.Name())
} }
// check: (2) ee1 == ee2 // check: (2) ee1 == ee2
@@ -207,11 +336,13 @@ Loop:
// check meta parameters // check meta parameters
for v1 := range g1.Adjacency() { // for each vertex in g1 for v1 := range g1.Adjacency() { // for each vertex in g1
for v2 := range g2.Adjacency() { // does it match in g2 ? for v2 := range g2.Adjacency() { // does it match in g2 ?
s1, s2 := VtoR(v1).Meta().Sema, VtoR(v2).Meta().Sema r1 := v1.(engine.Res)
r2 := v2.(engine.Res)
s1, s2 := r1.MetaParams().Sema, r2.MetaParams().Sema
sort.Strings(s1) sort.Strings(s1)
sort.Strings(s2) sort.Strings(s2)
if !reflect.DeepEqual(s1, s2) { if !reflect.DeepEqual(s1, s2) {
return fmt.Errorf("vertex %s and vertex %s have different semaphores", VtoR(v1).GetName(), VtoR(v2).GetName()) return fmt.Errorf("vertex %s and vertex %s have different semaphores", r1.Name(), r2.Name())
} }
} }
} }
@@ -242,17 +373,20 @@ func ListStrCmp(a, b []string) bool {
func fullPrint(g *pgraph.Graph) (str string) { func fullPrint(g *pgraph.Graph) (str string) {
str += "\n" str += "\n"
for v := range g.Adjacency() { for v := range g.Adjacency() {
if semas := VtoR(v).Meta().Sema; len(semas) > 0 { r := v.(engine.Res)
str += fmt.Sprintf("* v: %v; sema: %v\n", VtoR(v).GetName(), semas) if semas := r.MetaParams().Sema; len(semas) > 0 {
str += fmt.Sprintf("* v: %v; sema: %v\n", r.Name(), semas)
} else { } else {
str += fmt.Sprintf("* v: %v\n", VtoR(v).GetName()) str += fmt.Sprintf("* v: %v\n", r.Name())
} }
// TODO: add explicit grouping data? // TODO: add explicit grouping data?
} }
for v1 := range g.Adjacency() { for v1 := range g.Adjacency() {
for v2, e := range g.Adjacency()[v1] { for v2, e := range g.Adjacency()[v1] {
edge := e.(*Edge) r1 := v1.(engine.Res)
str += fmt.Sprintf("* e: %v -> %v # %v\n", VtoR(v1).GetName(), VtoR(v2).GetName(), edge.Name) r2 := v2.(engine.Res)
edge := e.(*engine.Edge)
str += fmt.Sprintf("* e: %v -> %v # %v\n", r1.Name(), r2.Name(), edge.Name)
} }
} }
return return
@@ -731,3 +865,57 @@ func TestPgraphGroupingConnected1(t *testing.T) {
} }
runGraphCmp(t, g1, g2) runGraphCmp(t, g1, g2)
} }
func TestPgraphSemaphoreGrouping1(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewNoopResTestSema("a1", []string{"s:1"})
a2 := NewNoopResTestSema("a2", []string{"s:2"})
a3 := NewNoopResTestSema("a3", []string{"s:3"})
g1.AddVertex(a1)
g1.AddVertex(a2)
g1.AddVertex(a3)
}
g2, _ := pgraph.NewGraph("g2") // expected result
{
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:1", "s:2", "s:3"})
g2.AddVertex(a123)
}
runGraphCmp(t, g1, g2)
}
func TestPgraphSemaphoreGrouping2(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewNoopResTestSema("a1", []string{"s:10", "s:11"})
a2 := NewNoopResTestSema("a2", []string{"s:2"})
a3 := NewNoopResTestSema("a3", []string{"s:3"})
g1.AddVertex(a1)
g1.AddVertex(a2)
g1.AddVertex(a3)
}
g2, _ := pgraph.NewGraph("g2") // expected result
{
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:10", "s:11", "s:2", "s:3"})
g2.AddVertex(a123)
}
runGraphCmp(t, g1, g2)
}
func TestPgraphSemaphoreGrouping3(t *testing.T) {
g1, _ := pgraph.NewGraph("g1") // original graph
{
a1 := NewNoopResTestSema("a1", []string{"s:1", "s:2"})
a2 := NewNoopResTestSema("a2", []string{"s:2"})
a3 := NewNoopResTestSema("a3", []string{"s:3"})
g1.AddVertex(a1)
g1.AddVertex(a2)
g1.AddVertex(a3)
}
g2, _ := pgraph.NewGraph("g2") // expected result
{
a123 := NewNoopResTestSema("a1,a2,a3", []string{"s:1", "s:2", "s:3"})
g2.AddVertex(a123)
}
runGraphCmp(t, g1, g2)
}

View File

@@ -0,0 +1,127 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"fmt"
"github.com/purpleidea/mgmt/pgraph"
)
// baseGrouper is the base type for implementing the AutoGrouper interface.
type baseGrouper struct {
graph *pgraph.Graph // store a pointer to the graph
vertices []pgraph.Vertex // cached list of vertices
i int
j int
done bool
}
// Name provides a friendly name for the logs to see.
func (ag *baseGrouper) Name() string {
return "baseGrouper"
}
// Init is called only once and before using other AutoGrouper interface methods
// the name method is the only exception: call it any time without side effects!
func (ag *baseGrouper) Init(g *pgraph.Graph) error {
if ag.graph != nil {
return fmt.Errorf("the init method has already been called")
}
ag.graph = g // pointer
ag.vertices = ag.graph.VerticesSorted() // cache in deterministic order!
ag.i = 0
ag.j = 0
if len(ag.vertices) == 0 { // empty graph
ag.done = true
return nil
}
return nil
}
// VertexNext is a simple iterator that loops through vertex (pair) combinations
// an intelligent algorithm would selectively offer only valid pairs of vertices
// these should satisfy logical grouping requirements for the autogroup designs!
// the desired algorithms can override, but keep this method as a base iterator!
func (ag *baseGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
// this does a for v... { for w... { return v, w }} but stepwise!
l := len(ag.vertices)
if ag.i < l {
v1 = ag.vertices[ag.i]
}
if ag.j < l {
v2 = ag.vertices[ag.j]
}
// in case the vertex was deleted
if !ag.graph.HasVertex(v1) {
v1 = nil
}
if !ag.graph.HasVertex(v2) {
v2 = nil
}
// two nested loops...
if ag.j < l {
ag.j++
}
if ag.j == l {
ag.j = 0
if ag.i < l {
ag.i++
}
if ag.i == l {
ag.done = true
}
}
return
}
// VertexCmp can be used in addition to an overridding implementation.
func (ag *baseGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
if v1 == nil || v2 == nil {
return fmt.Errorf("the vertex is nil")
}
if v1 == v2 { // skip yourself
return fmt.Errorf("the vertices are the same")
}
return nil // success
}
// VertexMerge needs to be overridden to add the actual merging functionality.
func (ag *baseGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
return nil, fmt.Errorf("vertexMerge needs to be overridden")
}
// EdgeMerge can be overridden, since it just simple returns the first edge.
func (ag *baseGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
return e1 // noop
}
// VertexTest processes the results of the grouping for the algorithm to know
// return an error if something went horribly wrong, and bool false to stop.
func (ag *baseGrouper) VertexTest(b bool) (bool, error) {
// NOTE: this particular baseGrouper version doesn't track what happens
// because since we iterate over every pair, we don't care which merge!
if ag.done {
return false, nil
}
return true, nil
}

View File

@@ -0,0 +1,73 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"github.com/purpleidea/mgmt/pgraph"
errwrap "github.com/pkg/errors"
)
// NonReachabilityGrouper is the most straight-forward algorithm for grouping.
// TODO: this algorithm may not be correct in all cases. replace if needed!
type NonReachabilityGrouper struct {
baseGrouper // "inherit" what we want, and reimplement the rest
}
// Name returns the name for the grouper algorithm.
func (ag *NonReachabilityGrouper) Name() string {
return "NonReachabilityGrouper"
}
// VertexNext iteratively finds vertex pairs with simple graph reachability...
// This algorithm relies on the observation that if there's a path from a to b,
// then they *can't* be merged (b/c of the existing dependency) so therefore we
// merge anything that *doesn't* satisfy this condition or that of the reverse!
func (ag *NonReachabilityGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
for {
v1, v2, err = ag.baseGrouper.VertexNext() // get all iterable pairs
if err != nil {
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
}
// ignore self cmp early (perf optimization)
if v1 != v2 && v1 != nil && v2 != nil {
// if NOT reachable, they're viable...
out1, e1 := ag.graph.Reachability(v1, v2)
if e1 != nil {
return nil, nil, e1
}
out2, e2 := ag.graph.Reachability(v2, v1)
if e2 != nil {
return nil, nil, e2
}
if len(out1) == 0 && len(out2) == 0 {
return // return v1 and v2, they're viable
}
}
// if we got here, it means we're skipping over this candidate!
if ok, err := ag.baseGrouper.VertexTest(false); err != nil {
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
} else if !ok {
return nil, nil, nil // done!
}
// the vertexTest passed, so loop and try with a new pair...
}
}

View File

@@ -0,0 +1,127 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"github.com/purpleidea/mgmt/pgraph"
errwrap "github.com/pkg/errors"
)
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate,
// and then by deleting v2 from the graph. Since more than one edge between two
// vertices is not allowed, duplicate edges are merged as well. an edge merge
// function can be provided if you'd like to control how you merge the edges!
func VertexMerge(g *pgraph.Graph, v1, v2 pgraph.Vertex, vertexMergeFn func(pgraph.Vertex, pgraph.Vertex) (pgraph.Vertex, error), edgeMergeFn func(pgraph.Edge, pgraph.Edge) pgraph.Edge) error {
// methodology
// 1) edges between v1 and v2 are removed
//Loop:
for k1 := range g.Adjacency() {
for k2 := range g.Adjacency()[k1] {
// v1 -> v2 || v2 -> v1
if (k1 == v1 && k2 == v2) || (k1 == v2 && k2 == v1) {
delete(g.Adjacency()[k1], k2) // delete map & edge
// NOTE: if we assume this is a DAG, then we can
// assume only v1 -> v2 OR v2 -> v1 exists, and
// we can break out of these loops immediately!
//break Loop
break
}
}
}
// 2) edges that point towards v2 from X now point to v1 from X (no dupes)
for _, x := range g.IncomingGraphVertices(v2) { // all to vertex v (??? -> v)
e := g.Adjacency()[x][v2] // previous edge
r, err := g.Reachability(x, v1)
if err != nil {
return err
}
// merge e with ex := g.Adjacency()[x][v1] if it exists!
if ex, exists := g.Adjacency()[x][v1]; exists && edgeMergeFn != nil && len(r) == 0 {
e = edgeMergeFn(e, ex)
}
if len(r) == 0 { // if not reachable, add it
g.AddEdge(x, v1, e) // overwrite edge
} else if edgeMergeFn != nil { // reachable, merge e through...
prev := x // initial condition
for i, next := range r {
if i == 0 {
// next == prev, therefore skip
continue
}
// this edge is from: prev, to: next
ex, _ := g.Adjacency()[prev][next] // get
ex = edgeMergeFn(ex, e)
g.Adjacency()[prev][next] = ex // set
prev = next
}
}
delete(g.Adjacency()[x], v2) // delete old edge
}
// 3) edges that point from v2 to X now point from v1 to X (no dupes)
for _, x := range g.OutgoingGraphVertices(v2) { // all from vertex v (v -> ???)
e := g.Adjacency()[v2][x] // previous edge
r, err := g.Reachability(v1, x)
if err != nil {
return err
}
// merge e with ex := g.Adjacency()[v1][x] if it exists!
if ex, exists := g.Adjacency()[v1][x]; exists && edgeMergeFn != nil && len(r) == 0 {
e = edgeMergeFn(e, ex)
}
if len(r) == 0 {
g.AddEdge(v1, x, e) // overwrite edge
} else if edgeMergeFn != nil { // reachable, merge e through...
prev := v1 // initial condition
for i, next := range r {
if i == 0 {
// next == prev, therefore skip
continue
}
// this edge is from: prev, to: next
ex, _ := g.Adjacency()[prev][next]
ex = edgeMergeFn(ex, e)
g.Adjacency()[prev][next] = ex
prev = next
}
}
delete(g.Adjacency()[v2], x)
}
// 4) merge and then remove the (now merged/grouped) vertex
if vertexMergeFn != nil { // run vertex merge function
if v, err := vertexMergeFn(v1, v2); err != nil {
return err
} else if v != nil { // replace v1 with the "merged" version...
// note: This branch isn't used if the vertexMergeFn
// decides to just merge logically on its own instead
// of actually returning something that we then merge.
v1 = v // TODO: ineffassign?
//*v1 = *v
}
}
g.DeleteVertex(v2) // remove grouped vertex
// 5) creation of a cyclic graph should throw an error
if _, err := g.TopologicalSort(); err != nil { // am i a dag or not?
return errwrap.Wrapf(err, "the TopologicalSort failed") // not a dag
}
return nil // success
}

336
engine/graph/engine.go Normal file
View File

@@ -0,0 +1,336 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"os"
"path"
"sync"
"github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/event"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util/semaphore"
multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
)
// Engine encapsulates a generic graph and manages its operations.
type Engine struct {
Program string
Hostname string
World engine.World
// Prefix is a unique directory prefix which can be used. It should be
// created if needed.
Prefix string
Converger converger.Converger
Debug bool
Logf func(format string, v ...interface{})
graph *pgraph.Graph
nextGraph *pgraph.Graph
state map[pgraph.Vertex]*State
waits map[pgraph.Vertex]*sync.WaitGroup
slock *sync.Mutex // semaphore lock
semas map[string]*semaphore.Semaphore
wg *sync.WaitGroup
fastPause bool
}
// Init initializes the internal structures and starts this the graph running.
// If the struct does not validate, or it cannot initialize, then this errors.
// Initially it will contain an empty graph.
func (obj *Engine) Init() error {
var err error
if obj.graph, err = pgraph.NewGraph("graph"); err != nil {
return err
}
if obj.Prefix == "" || obj.Prefix == "/" {
return fmt.Errorf("the prefix of `%s` is invalid", obj.Prefix)
}
if err := os.MkdirAll(obj.Prefix, 0770); err != nil {
return errwrap.Wrapf(err, "can't create prefix")
}
obj.state = make(map[pgraph.Vertex]*State)
obj.waits = make(map[pgraph.Vertex]*sync.WaitGroup)
obj.slock = &sync.Mutex{}
obj.semas = make(map[string]*semaphore.Semaphore)
obj.wg = &sync.WaitGroup{}
return nil
}
// Load a new graph into the engine. Offline graph operations will be performed
// on this graph. To switch it to the active graph, and run it, use Commit.
func (obj *Engine) Load(newGraph *pgraph.Graph) error {
if obj.nextGraph != nil {
return fmt.Errorf("can't overwrite pending graph, use abort")
}
obj.nextGraph = newGraph
return nil
}
// Abort the pending graph and any work in progress on it. After this call you
// may Load a new graph.
func (obj *Engine) Abort() error {
if obj.nextGraph == nil {
return fmt.Errorf("there is no pending graph to abort")
}
obj.nextGraph = nil
return nil
}
// Validate validates the pending graph to ensure it is appropriate for the
// engine. This should be called before Commit to avoid any surprises there!
// This prevents an error on Commit which could cause an engine shutdown.
func (obj *Engine) Validate() error {
for _, vertex := range obj.nextGraph.Vertices() {
res, ok := vertex.(engine.Res)
if !ok {
return fmt.Errorf("not a Res")
}
if err := engine.Validate(res); err != nil {
return errwrap.Wrapf(err, "the Res did not Validate")
}
}
return nil
}
// Apply a function to the pending graph. You must pass in a function which will
// receive this graph as input, and return an error if it something does not
// succeed.
func (obj *Engine) Apply(fn func(*pgraph.Graph) error) error {
return fn(obj.nextGraph)
}
// Commit runs a graph sync and swaps the loaded graph with the current one. If
// it errors, then the running graph wasn't changed. It is recommended that you
// pause the engine before running this, and resume it after you're done.
func (obj *Engine) Commit() error {
// TODO: Does this hurt performance or graph changes ?
vertexAddFn := func(vertex pgraph.Vertex) error {
// some of these validation steps happen before this Commit step
// in Validate() to avoid erroring here. These are redundant.
// FIXME: should we get rid of this redundant validation?
res, ok := vertex.(engine.Res)
if !ok { // should not happen, previously validated
return fmt.Errorf("not a Res")
}
if obj.Debug {
obj.Logf("loading resource `%s`", res)
}
if _, exists := obj.state[vertex]; exists {
return fmt.Errorf("the Res state already exists")
}
if obj.Debug {
obj.Logf("Validate(%s)", res)
}
err := engine.Validate(res)
if obj.Debug {
obj.Logf("Validate(%s): Return(%+v)", res, err)
}
if err != nil {
return errwrap.Wrapf(err, "the Res did not Validate")
}
// FIXME: is res.Name() sufficiently unique to use as a UID here?
pathUID := fmt.Sprintf("%s-%s", res.Kind(), res.Name())
statePrefix := fmt.Sprintf("%s/", path.Join(obj.Prefix, "state", pathUID))
// don't create this unless it *will* be used
//if err := os.MkdirAll(statePrefix, 0770); err != nil {
// return errwrap.Wrapf(err, "can't create state prefix")
//}
obj.waits[vertex] = &sync.WaitGroup{}
obj.state[vertex] = &State{
//Graph: obj.graph, // TODO: what happens if we swap the graph?
Vertex: vertex,
Program: obj.Program,
Hostname: obj.Hostname,
World: obj.World,
Prefix: statePrefix,
//Converger: obj.Converger,
Debug: obj.Debug,
Logf: func(format string, v ...interface{}) {
obj.Logf(res.String()+": "+format, v...)
},
}
if err := obj.state[vertex].Init(); err != nil {
return errwrap.Wrapf(err, "the Res did not Init")
}
return nil
}
vertexRemoveFn := func(vertex pgraph.Vertex) error {
// wait for exit before starting new graph!
obj.state[vertex].Event(event.EventExit) // signal an exit
obj.waits[vertex].Wait() // sync
// close the state and resource
// FIXME: will this mess up the sync and block the engine?
if err := obj.state[vertex].Close(); err != nil {
return errwrap.Wrapf(err, "the Res did not Close")
}
// delete to free up memory from old graphs
delete(obj.state, vertex)
delete(obj.waits, vertex)
return nil
}
// If GraphSync succeeds, it updates the receiver graph accordingly...
// Running the shutdown in vertexRemoveFn does not need to happen in a
// topologically sorted order because it already paused in that order.
obj.Logf("graph sync...")
if err := obj.graph.GraphSync(obj.nextGraph, engine.VertexCmpFn, vertexAddFn, vertexRemoveFn, engine.EdgeCmpFn); err != nil {
return errwrap.Wrapf(err, "error running graph sync")
}
obj.nextGraph = nil
// After this point, we must not error or we'd need to restore all of
// the changes that we'd made to the previously primary graph. This is
// because this function is meant to atomically swap the graphs safely.
// TODO: update all the `State` structs with the new Graph pointer
//for _, vertex := range obj.graph.Vertices() {
// state, exists := obj.state[vertex]
// if !exists {
// continue
// }
// state.Graph = obj.graph // update pointer to graph
//}
return nil
}
// Start runs the currently active graph. It also un-pauses the graph if it was
// paused.
func (obj *Engine) Start() error {
topoSort, err := obj.graph.TopologicalSort()
if err != nil {
return err
}
indegree := obj.graph.InDegree() // compute all of the indegree's
reversed := pgraph.Reverse(topoSort)
for _, vertex := range reversed {
state := obj.state[vertex]
state.starter = (indegree[vertex] == 0)
var unpause = true // assume true
if !state.working { // if not running...
state.working = true
unpause = false // doesn't need unpausing if starting
obj.wg.Add(1)
obj.waits[vertex].Add(1)
go func(v pgraph.Vertex) {
defer obj.wg.Done()
defer obj.waits[vertex].Done()
defer func() {
obj.state[v].working = false
}()
obj.Logf("Worker(%s)", v)
// contains the Watch and CheckApply loops
err := obj.Worker(v)
obj.Logf("Worker(%s): Exited(%+v)", v, err)
}(vertex)
}
select {
case <-state.started:
case <-state.stopped: // we failed on Watch start
}
if unpause { // unpause (if needed)
obj.state[vertex].Event(event.EventStart)
}
}
// we wait for everyone to start before exiting!
return nil
}
// SetFastPause puts the graph into fast pause mode. This is usually done via
// the argument to the Pause command, but this method can be used if a pause was
// already started, and you'd like subsequent parts to pause quickly. Once in
// fast pause mode for a given pause action, you cannot switch to regular pause.
// This is because once you've started a fast pause, some dependencies might
// have been skipped when fast pausing, and future resources might have missed a
// poke. In general this is only called when you're trying to hurry up the exit.
func (obj *Engine) SetFastPause() {
obj.fastPause = true
}
// Pause the active, running graph. At the moment this cannot error.
func (obj *Engine) Pause(fastPause bool) {
obj.fastPause = fastPause
topoSort, _ := obj.graph.TopologicalSort()
for _, vertex := range topoSort { // squeeze out the events...
// The Event is sent to an unbuffered channel, so this event is
// synchronous, and as a result it blocks until it is received.
obj.state[vertex].Event(event.EventPause)
}
// we are now completely paused...
obj.fastPause = false // reset
}
// Close triggers a shutdown. Engine must be already paused before this is run.
func (obj *Engine) Close() error {
var reterr error
emptyGraph, err := pgraph.NewGraph("empty")
if err != nil {
reterr = multierr.Append(reterr, err) // list of errors
}
// this is a graph switch (graph sync) that switches to an empty graph!
if err := obj.Load(emptyGraph); err != nil { // copy in empty graph
reterr = multierr.Append(reterr, err)
}
// the commit will cause the graph sync to shut things down cleverly...
if err := obj.Commit(); err != nil {
reterr = multierr.Append(reterr, err)
}
obj.wg.Wait() // for now, this doesn't need to be a separate Wait() method
return reterr
}
// Graph returns the running graph.
func (obj *Engine) Graph() *pgraph.Graph {
return obj.graph
}

59
engine/graph/refresh.go Normal file
View File

@@ -0,0 +1,59 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
)
// RefreshPending determines if any previous nodes have a refresh pending here.
// If this is true, it means I am expected to apply a refresh when I next run.
func (obj *Engine) RefreshPending(vertex pgraph.Vertex) bool {
var refresh bool
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
// if we asked for a notify *and* if one is pending!
edge := e.(*engine.Edge) // panic if wrong
if edge.Notify && edge.Refresh() {
refresh = true
break
}
}
return refresh
}
// SetUpstreamRefresh sets the refresh value to any upstream vertices.
func (obj *Engine) SetUpstreamRefresh(vertex pgraph.Vertex, b bool) {
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
edge := e.(*engine.Edge) // panic if wrong
if edge.Notify {
edge.SetRefresh(b)
}
}
}
// SetDownstreamRefresh sets the refresh value to any downstream vertices.
func (obj *Engine) SetDownstreamRefresh(vertex pgraph.Vertex, b bool) {
for _, e := range obj.graph.OutgoingGraphEdges(vertex) {
edge := e.(*engine.Edge) // panic if wrong
// if we asked for a notify *and* if one is pending!
if edge.Notify {
edge.SetRefresh(b)
}
}
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package graph
import ( import (
"fmt" "fmt"
@@ -31,8 +31,8 @@ import (
// SemaSep is the trailing separator to split the semaphore id from the size. // SemaSep is the trailing separator to split the semaphore id from the size.
const SemaSep = ":" const SemaSep = ":"
// SemaLock acquires the list of semaphores in the graph. // semaLock acquires the list of semaphores in the graph.
func (obj *MGraph) SemaLock(semas []string) error { func (obj *Engine) semaLock(semas []string) error {
var reterr error var reterr error
sort.Strings(semas) // very important to avoid deadlock in the dag! sort.Strings(semas) // very important to avoid deadlock in the dag!
@@ -53,8 +53,8 @@ func (obj *MGraph) SemaLock(semas []string) error {
return reterr return reterr
} }
// SemaUnlock releases the list of semaphores in the graph. // semaUnlock releases the list of semaphores in the graph.
func (obj *MGraph) SemaUnlock(semas []string) error { func (obj *Engine) semaUnlock(semas []string) error {
var reterr error var reterr error
sort.Strings(semas) // unlock in the same order to remove partial locks sort.Strings(semas) // unlock in the same order to remove partial locks

View File

@@ -15,11 +15,23 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build novirt // +build !root
package resources package graph
// VirtRes represents the fields of the Virt resource. Since this file is import (
// only invoked with the tag "novirt", we do not need any fields here. "testing"
type VirtRes struct { )
func TestSemaSize(t *testing.T) {
pairs := map[string]int{
"id:42": 42,
":13": 13,
"some_id": 1,
}
for id, size := range pairs {
if i := SemaSize(id); i != size {
t.Errorf("sema id `%s`, expected: `%d`, got: `%d`", id, size, i)
}
}
} }

118
engine/graph/sendrecv.go Normal file
View File

@@ -0,0 +1,118 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"reflect"
"github.com/purpleidea/mgmt/engine"
multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
)
// SendRecv pulls in the sent values into the receive slots. It is called by the
// receiver and must be given as input the full resource struct to receive on.
// It applies the loaded values to the resource.
func (obj *Engine) SendRecv(res engine.RecvableRes) (map[string]bool, error) {
recv := res.Recv()
if obj.Debug {
// NOTE: this could expose private resource data like passwords
obj.Logf("%s: SendRecv: %+v", res, recv)
}
var updated = make(map[string]bool) // list of updated keys
var err error
for k, v := range recv {
updated[k] = false // default
v.Changed = false // reset to the default
var st interface{} = v.Res // old style direct send/recv
if true { // new style send/recv API
st = v.Res.Sent()
}
// send
obj1 := reflect.Indirect(reflect.ValueOf(st))
type1 := obj1.Type()
value1 := obj1.FieldByName(v.Key)
kind1 := value1.Kind()
// recv
obj2 := reflect.Indirect(reflect.ValueOf(res)) // pass in full struct
type2 := obj2.Type()
value2 := obj2.FieldByName(k)
kind2 := value2.Kind()
if obj.Debug {
obj.Logf("Send(%s) has %v: %v", type1, kind1, value1)
obj.Logf("Recv(%s) has %v: %v", type2, kind2, value2)
}
// i think we probably want the same kind, at least for now...
if kind1 != kind2 {
e := fmt.Errorf("kind mismatch between %s: %s and %s: %s", v.Res, kind1, res, kind2)
err = multierr.Append(err, e) // list of errors
continue
}
// if the types don't match, we can't use send->recv
// FIXME: do we want to relax this for string -> *string ?
if e := TypeCmp(value1, value2); e != nil {
e := errwrap.Wrapf(e, "type mismatch between %s and %s", v.Res, res)
err = multierr.Append(err, e) // list of errors
continue
}
// if we can't set, then well this is pointless!
if !value2.CanSet() {
e := fmt.Errorf("can't set %s.%s", res, k)
err = multierr.Append(err, e) // list of errors
continue
}
// if we can't interface, we can't compare...
if !value1.CanInterface() || !value2.CanInterface() {
e := fmt.Errorf("can't interface %s.%s", res, k)
err = multierr.Append(err, e) // list of errors
continue
}
// if the values aren't equal, we're changing the receiver
if !reflect.DeepEqual(value1.Interface(), value2.Interface()) {
// TODO: can we catch the panics here in case they happen?
value2.Set(value1) // do it for all types that match
updated[k] = true // we updated this key!
v.Changed = true // tag this key as updated!
obj.Logf("SendRecv: %s.%s -> %s.%s", v.Res, v.Key, res, k)
}
}
return updated, err
}
// TypeCmp compares two reflect values to see if they are the same Kind. It can
// look into a ptr Kind to see if the underlying pair of ptr's can TypeCmp too!
func TypeCmp(a, b reflect.Value) error {
ta, tb := a.Type(), b.Type()
if ta != tb {
return fmt.Errorf("type mismatch: %s != %s", ta, tb)
}
// NOTE: it seems we don't need to recurse into pointers to sub check!
return nil // identical Type()'s
}

436
engine/graph/state.go Normal file
View File

@@ -0,0 +1,436 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"os"
"path"
"sync"
"time"
"github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/event"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
)
// State stores some state about the resource it is mapped to.
type State struct {
// Graph is a pointer to the graph that this vertex is part of.
//Graph pgraph.Graph
// Vertex is the pointer in the graph that this state corresponds to. It
// can be converted to a `Res` if necessary.
// TODO: should this be passed in on Init instead?
Vertex pgraph.Vertex
Program string
Hostname string
World engine.World
// Prefix is a unique directory prefix which can be used. It should be
// created if needed.
Prefix string
//Converger converger.Converger
// Debug turns on additional output and behaviours.
Debug bool
// Logf is the logging function that should be used to display messages.
Logf func(format string, v ...interface{})
timestamp int64 // last updated timestamp
isStateOK bool // is state OK or do we need to run CheckApply ?
// events is a channel of incoming events which is read by the Watch
// loop for that resource. It receives events like pause, start, and
// poke. The channel shuts down to signal for Watch to exit.
eventsChan chan event.Kind // incoming to resource
eventsLock *sync.Mutex // lock around sending and closing of events channel
eventsDone bool // is channel closed?
// outputChan is the channel that the engine listens on for events from
// the Watch loop for that resource. The event is nil normally, except
// when events are sent on this channel from the engine. This only
// happens as a signaling mechanism when Watch has shutdown and we want
// to notify the Process loop which reads from this.
outputChan chan error // outgoing from resource
wg *sync.WaitGroup
exit *util.EasyExit
started chan struct{} // closes when it's started
stopped chan struct{} // closes when it's stopped
starter bool // do we have an indegree of 0 ?
working bool // is the Main() loop running ?
cuid converger.UID // primary converger
init *engine.Init // a copy of the init struct passed to res Init
}
// Init initializes structures like channels.
func (obj *State) Init() error {
obj.eventsChan = make(chan event.Kind)
obj.eventsLock = &sync.Mutex{}
obj.outputChan = make(chan error)
obj.wg = &sync.WaitGroup{}
obj.exit = util.NewEasyExit()
obj.started = make(chan struct{})
obj.stopped = make(chan struct{})
res, isRes := obj.Vertex.(engine.Res)
if !isRes {
return fmt.Errorf("vertex is not a Res")
}
if obj.Hostname == "" {
return fmt.Errorf("the Hostname is empty")
}
if obj.Prefix == "" {
return fmt.Errorf("the Prefix is empty")
}
if obj.Prefix == "/" {
return fmt.Errorf("the Prefix is root")
}
if obj.Logf == nil {
return fmt.Errorf("the Logf function is missing")
}
//obj.cuid = obj.Converger.Register() // gets registered in Worker()
obj.init = &engine.Init{
Program: obj.Program,
Hostname: obj.Hostname,
// Watch:
Running: func() error {
close(obj.started) // this is reset in the reset func
obj.isStateOK = false // assume we're initially dirty
// optimization: skip the initial send if not a starter
// because we'll get poked from a starter soon anyways!
if !obj.starter {
return nil
}
return obj.event()
},
Event: obj.event,
Events: obj.eventsChan,
Read: obj.read,
Dirty: func() { // TODO: should we rename this SetDirty?
obj.isStateOK = false
},
// CheckApply:
Refresh: func() bool {
res, ok := obj.Vertex.(engine.RefreshableRes)
if !ok {
panic("res does not support the Refreshable trait")
}
return res.Refresh()
},
Send: func(st interface{}) error {
res, ok := obj.Vertex.(engine.SendableRes)
if !ok {
panic("res does not support the Sendable trait")
}
// XXX: type check this
//expected := res.Sends()
//if err := XXX_TYPE_CHECK(expected, st); err != nil {
// return err
//}
return res.Send(st) // send the struct
},
Recv: func() map[string]*engine.Send { // TODO: change this API?
res, ok := obj.Vertex.(engine.RecvableRes)
if !ok {
panic("res does not support the Recvable trait")
}
return res.Recv()
},
World: obj.World,
VarDir: obj.varDir,
Debug: obj.Debug,
Logf: func(format string, v ...interface{}) {
obj.Logf("resource: "+format, v...)
},
}
// run the init
if obj.Debug {
obj.Logf("Init(%s)", res)
}
err := res.Init(obj.init)
if obj.Debug {
obj.Logf("Init(%s): Return(%+v)", res, err)
}
if err != nil {
return errwrap.Wrapf(err, "could not Init() resource")
}
return nil
}
// Close shuts down and performs any cleanup. This is most akin to a "post" or
// cleanup command as the initiator for closing a vertex happens in graph sync.
func (obj *State) Close() error {
res, isRes := obj.Vertex.(engine.Res)
if !isRes {
return fmt.Errorf("vertex is not a Res")
}
//if obj.cuid != nil {
// obj.cuid.Unregister() // gets unregistered in Worker()
//}
// redundant safety
obj.wg.Wait() // wait until all poke's and events on me have exited
// run the close
if obj.Debug {
obj.Logf("Close(%s)", res)
}
err := res.Close()
if obj.Debug {
obj.Logf("Close(%s): Return(%+v)", res, err)
}
return err
}
// reset is run to reset the state so that Watch can run a second time. Thus is
// needed for the Watch retry in particular.
func (obj *State) reset() {
obj.started = make(chan struct{})
obj.stopped = make(chan struct{})
}
// Poke sends a nil message on the outputChan. This channel is used by the
// resource to signal a possible change. This will cause the Process loop to
// run if it can.
func (obj *State) Poke() {
// add a wait group on the vertex we're poking!
obj.wg.Add(1)
defer obj.wg.Done()
select {
case obj.outputChan <- nil:
case <-obj.exit.Signal():
}
}
// Event sends a Pause or Start event to the resource. It can also be used to
// send Poke events, but it's much more efficient to send them directly instead
// of passing them through the resource.
func (obj *State) Event(kind event.Kind) {
// TODO: should these happen after the lock?
obj.wg.Add(1)
defer obj.wg.Done()
obj.eventsLock.Lock()
defer obj.eventsLock.Unlock()
if obj.eventsDone { // closing, skip events...
return
}
if kind == event.EventExit { // set this so future events don't deadlock
obj.Logf("exit event...")
obj.eventsDone = true
close(obj.eventsChan) // causes resource Watch loop to close
obj.exit.Done(nil) // trigger exit signal to unblock some cases
return
}
select {
case obj.eventsChan <- kind:
case <-obj.exit.Signal():
}
}
// read is a helper function used inside the main select statement of resources.
// If it returns an error, then this is a signal for the resource to exit.
func (obj *State) read(kind event.Kind) error {
switch kind {
case event.EventPoke:
return obj.event() // a poke needs to cause an event...
case event.EventStart:
return fmt.Errorf("unexpected start")
case event.EventPause:
// pass
case event.EventExit:
return engine.ErrSignalExit
default:
return fmt.Errorf("unhandled event: %+v", kind)
}
// we're paused now
select {
case kind, ok := <-obj.eventsChan:
if !ok {
return engine.ErrWatchExit
}
switch kind {
case event.EventPoke:
return fmt.Errorf("unexpected poke")
case event.EventPause:
return fmt.Errorf("unexpected pause")
case event.EventStart:
// resumed
return nil
case event.EventExit:
return engine.ErrSignalExit
default:
return fmt.Errorf("unhandled event: %+v", kind)
}
}
}
// event is a helper function to send an event from the resource Watch loop. It
// can be used for the initial `running` event, or any regular event. If it
// returns an error, then the Watch loop must return this error and shutdown.
func (obj *State) event() error {
// loop until we sent on obj.outputChan or exit with error
for {
select {
// send "activity" event
case obj.outputChan <- nil:
return nil // sent event!
// make sure to keep handling incoming
case kind, ok := <-obj.eventsChan:
if !ok {
return engine.ErrWatchExit
}
switch kind {
case event.EventPoke:
// we're trying to send an event, so swallow the
// poke: it's what we wanted to have happen here
continue
case event.EventStart:
return fmt.Errorf("unexpected start")
case event.EventPause:
// pass
case event.EventExit:
return engine.ErrSignalExit
default:
return fmt.Errorf("unhandled event: %+v", kind)
}
}
// we're paused now
select {
case kind, ok := <-obj.eventsChan:
if !ok {
return engine.ErrWatchExit
}
switch kind {
case event.EventPoke:
return fmt.Errorf("unexpected poke")
case event.EventPause:
return fmt.Errorf("unexpected pause")
case event.EventStart:
// resumed
case event.EventExit:
return engine.ErrSignalExit
default:
return fmt.Errorf("unhandled event: %+v", kind)
}
}
}
}
// varDir returns the path to a working directory for the resource. It will try
// and create the directory first, and return an error if this failed. The dir
// should be cleaned up by the resource on Close if it wishes to discard the
// contents. If it does not, then a future resource with the same kind and name
// may see those contents in that directory. The resource should clean up the
// contents before use if it is important that nothing exist. It is always
// possible that contents could remain after an abrupt crash, so do not store
// overly sensitive data unless you're aware of the risks.
func (obj *State) varDir(extra string) (string, error) {
// Using extra adds additional dirs onto our namespace. An empty extra
// adds no additional directories.
if obj.Prefix == "" { // safety
return "", fmt.Errorf("the VarDir prefix is empty")
}
// an empty string at the end has no effect
p := fmt.Sprintf("%s/", path.Join(obj.Prefix, extra))
if err := os.MkdirAll(p, 0770); err != nil {
return "", errwrap.Wrapf(err, "can't create prefix in: %s", p)
}
// returns with a trailing slash as per the mgmt file res convention
return p, nil
}
// poll is a replacement for Watch when the Poll metaparameter is used.
func (obj *State) poll(interval uint32) error {
// create a time.Ticker for the given interval
ticker := time.NewTicker(time.Duration(interval) * time.Second)
defer ticker.Stop()
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // exit if requested
}
var send = false // send event?
for {
select {
case <-ticker.C: // received the timer event
obj.init.Logf("polling...")
send = true
obj.init.Dirty() // dirty
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}

169
engine/metaparams.go Normal file
View File

@@ -0,0 +1,169 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"fmt"
"strconv"
"github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
"golang.org/x/time/rate"
)
// DefaultMetaParams are the defaults that are used for undefined metaparams.
// Don't modify this variable. Use .Copy() if you'd like some for yourself.
var DefaultMetaParams = &MetaParams{
Noop: false,
Retry: 0,
Delay: 0,
Poll: 0, // defaults to watching for events
Limit: rate.Inf, // defaults to no limit
Burst: 0, // no burst needed on an infinite rate
//Sema: []string{},
}
// MetaRes is the interface a resource must implement to support meta params.
// All resources must implement this.
type MetaRes interface {
// MetaParams lets you get or set meta params for the resource.
MetaParams() *MetaParams
}
// MetaParams provides some meta parameters that apply to every resource.
type MetaParams struct {
// Noop specifies that no changes should be made by the resource. It
// relies on the individual resource implementation, and can't protect
// you from a poorly or maliciously implemented resource.
Noop bool `yaml:"noop"`
// NOTE: there are separate Watch and CheckApply retry and delay values,
// but I've decided to use the same ones for both until there's a proper
// reason to want to do something differently for the Watch errors.
// Retry is the number of times to retry on error. Use -1 for infinite.
Retry int16 `yaml:"retry"`
// Delay is the number of milliseconds to wait between retries.
Delay uint64 `yaml:"delay"`
// Poll is the number of seconds between poll intervals. Use 0 to Watch.
Poll uint32 `yaml:"poll"`
// Limit is the number of events per second to allow through.
Limit rate.Limit `yaml:"limit"`
// Burst is the number of events to allow in a burst.
Burst int `yaml:"burst"`
// Sema is a list of semaphore ids in the form `id` or `id:count`. If
// you don't specify a count, then 1 is assumed. The sema of `foo` which
// has a count equal to 1, is different from a sema named `foo:1` which
// also has a count equal to 1, but is a different semaphore.
Sema []string `yaml:"sema"`
}
// Cmp compares two AutoGroupMeta structs and determines if they're equivalent.
func (obj *MetaParams) Cmp(meta *MetaParams) error {
if obj.Noop != meta.Noop {
return fmt.Errorf("values for Noop are different")
}
// XXX: add a one way cmp like we used to have ?
//if obj.Noop != meta.Noop {
// // obj is the existing res, res is the *new* resource
// // if we go from no-noop -> noop, we can re-use the obj
// // if we go from noop -> no-noop, we need to regenerate
// if obj.Noop { // asymmetrical
// return fmt.Errorf("values for Noop are different") // going from noop to no-noop!
// }
//}
if obj.Retry != meta.Retry {
return fmt.Errorf("values for Retry are different")
}
if obj.Delay != meta.Delay {
return fmt.Errorf("values for Delay are different")
}
if obj.Poll != meta.Poll {
return fmt.Errorf("values for Poll are different")
}
if obj.Limit != meta.Limit {
return fmt.Errorf("values for Limit are different")
}
if obj.Burst != meta.Burst {
return fmt.Errorf("values for Burst are different")
}
if err := util.SortedStrSliceCompare(obj.Sema, meta.Sema); err != nil {
return errwrap.Wrapf(err, "values for Sema are different")
}
return nil
}
// Validate runs some validation on the meta params.
func (obj *MetaParams) Validate() error {
if obj.Burst == 0 && !(obj.Limit == rate.Inf) { // blocked
return fmt.Errorf("permanently limited (rate != Inf, burst = 0)")
}
for _, s := range obj.Sema {
if s == "" {
return fmt.Errorf("semaphore is empty")
}
if _, err := strconv.Atoi(s); err == nil { // standalone int
return fmt.Errorf("semaphore format is invalid")
}
}
return nil
}
// Copy copies this struct and returns a new one.
func (obj *MetaParams) Copy() *MetaParams {
sema := []string{}
if obj.Sema != nil {
sema = make([]string, len(obj.Sema))
copy(sema, obj.Sema)
}
return &MetaParams{
Noop: obj.Noop,
Retry: obj.Retry,
Delay: obj.Delay,
Poll: obj.Poll,
Limit: obj.Limit, // FIXME: can we copy this type like this? test me!
Burst: obj.Burst,
Sema: sema,
}
}
// UnmarshalYAML is the custom unmarshal handler for the MetaParams struct. It
// is primarily useful for setting the defaults.
// TODO: this is untested
func (obj *MetaParams) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawMetaParams MetaParams // indirection to avoid infinite recursion
raw := rawMetaParams(*DefaultMetaParams) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = MetaParams(raw) // restore from indirection with type conversion!
return nil
}

42
engine/metaparams_test.go Normal file
View File

@@ -0,0 +1,42 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package engine
import (
"testing"
)
func TestMetaCmp1(t *testing.T) {
m1 := &MetaParams{
Noop: true,
}
m2 := &MetaParams{
Noop: false,
}
// TODO: should we allow this? Maybe only with the future Mutate API?
//if err := m2.Cmp(m1); err != nil { // going from noop(false) -> noop(true) is okay!
// t.Errorf("the two resources do not match")
//}
if m1.Cmp(m2) == nil { // going from noop(true) -> noop(false) is not okay!
t.Errorf("the two resources should not match")
}
}

32
engine/refresh.go Normal file
View File

@@ -0,0 +1,32 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
// RefreshableRes is the interface a resource must implement to support refresh
// notifications. Default implementations for all of the methods declared in
// this interface can be obtained for your resource by anonymously adding the
// traits.Refreshable struct to your resource implementation.
type RefreshableRes interface {
Res // implement everything in Res but add the additional requirements
// Refresh returns the refresh notification state.
Refresh() bool
// SetRefresh sets the refresh notification state.
SetRefresh(bool)
}

271
engine/resources.go Normal file
View File

@@ -0,0 +1,271 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"encoding/gob"
"fmt"
"github.com/purpleidea/mgmt/engine/event"
errwrap "github.com/pkg/errors"
"gopkg.in/yaml.v2"
)
// TODO: should each resource be a sub-package?
var registeredResources = map[string]func() Res{}
// RegisterResource registers a new resource by providing a constructor
// function that returns a resource object ready to be unmarshalled from YAML.
func RegisterResource(kind string, fn func() Res) {
f := fn()
if kind == "" {
panic("can't register a resource with an empty kind")
}
if _, ok := registeredResources[kind]; ok {
panic(fmt.Sprintf("a resource kind of %s is already registered", kind))
}
gob.Register(f)
registeredResources[kind] = fn
}
// RegisteredResourcesNames returns the kind of the registered resources.
func RegisteredResourcesNames() []string {
kinds := []string{}
for k := range registeredResources {
kinds = append(kinds, k)
}
return kinds
}
// NewResource returns an empty resource object from a registered kind. It
// errors if the resource kind doesn't exist.
func NewResource(kind string) (Res, error) {
fn, ok := registeredResources[kind]
if !ok {
return nil, fmt.Errorf("no resource kind `%s` available", kind)
}
res := fn().Default()
res.SetKind(kind)
return res, nil
}
// NewNamedResource returns an empty resource object from a registered kind. It
// also sets the name. It is a wrapper around NewResource. It also errors if the
// name is empty.
func NewNamedResource(kind, name string) (Res, error) {
if name == "" {
return nil, fmt.Errorf("resource name is empty")
}
res, err := NewResource(kind)
if err != nil {
return nil, err
}
res.SetName(name)
return res, nil
}
// Init is the structure of values and references which is passed into all
// resources on initialization. None of these are available in Validate, or
// before Init runs.
type Init struct {
// Program is the name of the program.
Program string
// Hostname is the uuid for the host.
Hostname string
// Called from within Watch:
// Running must be called after your watches are all started and ready.
Running func() error
// Event sends an event notifying the engine of a possible state change.
Event func() error
// Events returns a channel that we must watch for messages from the
// engine. When it closes, this is a signal to shutdown.
Events chan event.Kind
// Read processes messages that come in from the Events channel. It is a
// helper method that knows how to handle the pause mechanism correctly.
Read func(event.Kind) error
// Dirty marks the resource state as dirty. This signals to the engine
// that CheckApply will have some work to do in order to converge it.
Dirty func()
// Called from within CheckApply:
// Refresh returns whether the resource received a notification. This
// flag can be used to tell a svc to reload, or to perform some state
// change that wouldn't otherwise be noticed by inspection alone. You
// must implement the Refreshable trait for this to work.
Refresh func() bool
// Send exposes some variables you wish to send via the Send/Recv
// mechanism. You must implement the Sendable trait for this to work.
Send func(interface{}) error
// Recv provides a map of variables which were sent to this resource via
// the Send/Recv mechanism. You must implement the Recvable trait for
// this to work.
Recv func() map[string]*Send
// Other functionality:
// World provides a connection to the outside world. This is most often
// used for communicating with the distributed database.
World World
// VarDir is a facility for local storage. It is used to return a path
// to a directory which may be used for temporary storage. It should be
// cleaned up on resource Close if the resource would like to delete the
// contents. The resource should not assume that the initial directory
// is empty, and it should be cleaned on Init if that is a requirement.
VarDir func(string) (string, error)
// Debug signals whether we are running in debugging mode. In this case,
// we might want to log additional messages.
Debug bool
// Logf is a logging facility which will correctly namespace any
// messages which you wish to pass on. You should use this instead of
// the log package directly for production quality resources.
Logf func(format string, v ...interface{})
}
// KindedRes is an interface that is required for a resource to have a kind.
type KindedRes interface {
// Kind returns a string representing the kind of resource this is.
Kind() string
// SetKind sets the resource kind and should only be called by the
// engine.
SetKind(string)
}
// NamedRes is an interface that is used so a resource can have a unique name.
type NamedRes interface {
Name() string
SetName(string)
}
// Res is the minimum interface you need to implement to define a new resource.
type Res interface {
fmt.Stringer // String() string
KindedRes
NamedRes // TODO: consider making this optional in the future
MetaRes // All resources must have meta params.
// Default returns a struct with sane defaults for this resource.
Default() Res
// Validate determines if the struct has been defined in a valid state.
Validate() error
// Init initializes the resource and passes in some external information
// and data from the engine.
Init(*Init) error
// Close is run by the engine to clean up after the resource is done.
Close() error
// Watch is run by the engine to monitor for state changes. If it
// detects any, it notifies the engine which will usually run CheckApply
// in response.
Watch() error
// CheckApply determines if the state of the resource is connect and if
// asked to with the `apply` variable, applies the requested state.
CheckApply(apply bool) (checkOK bool, err error)
// Cmp compares itself to another resource and returns an error if they
// are not equivalent.
Cmp(Res) error
}
// Repr returns a representation of a resource from its kind and name. This is
// used as the definitive format so that it can be changed in one place.
func Repr(kind, name string) string {
return fmt.Sprintf("%s[%s]", kind, name)
}
// Stringer returns a consistent and unique string representation of a resource.
func Stringer(res Res) string {
return Repr(res.Kind(), res.Name())
}
// Validate validates a resource by checking multiple aspects. This is the main
// entry point for running all the validation steps on a resource.
func Validate(res Res) error {
if res.Kind() == "" { // shouldn't happen IIRC
return fmt.Errorf("the Res has an empty Kind")
}
if res.Name() == "" {
return fmt.Errorf("the Res has an empty Name")
}
if err := res.MetaParams().Validate(); err != nil {
return errwrap.Wrapf(err, "the Res has an invalid meta param")
}
return res.Validate()
}
// InterruptableRes is an interface that adds interrupt functionality to
// resources. If the resource implements this interface, the engine will call
// the Interrupt method to shutdown the resource quickly. Running this method
// may leave the resource in a partial state, however this may be desired if you
// want a faster exit or if you'd prefer a partial state over letting the
// resource complete in a situation where you made an error and you wish to
// exit quickly to avoid data loss. It is usually triggered after multiple ^C
// signals.
type InterruptableRes interface {
Res
// Ask the resource to shutdown quickly. This can be called at any point
// in the resource lifecycle after Init. Close will still be called. It
// will only get called after an exit or pause request has been made. It
// is designed to unblock any long running operation that is occurring
// in the CheckApply portion of the life cycle. If the resource has
// already exited, running this method should not block. (That is to say
// that you should not expect CheckApply or Watch to be able to alive
// and able to read from a channel to satisfy your request.) It is best
// to probably have this close a channel to multicast that signal around
// to anyone who can detect it in a select. If you are in a situation
// which cannot interrupt, then you can return an error.
// FIXME: implement, and check the above description is what we expect!
Interrupt() error
}
// CollectableRes is an interface for resources that support collection. It is
// currently temporary until a proper API for all resources is invented.
type CollectableRes interface {
Res
CollectPattern(string) // XXX: temporary until Res collection is more advanced
}
// YAMLRes is a resource that supports creation by unmarshalling.
type YAMLRes interface {
Res
yaml.Unmarshaler // UnmarshalYAML(unmarshal func(interface{}) error) error
}

View File

@@ -21,10 +21,11 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"os" "os"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -39,13 +40,15 @@ const (
) )
func init() { func init() {
RegisterResource("augeas", func() Res { return &AugeasRes{} }) engine.RegisterResource("augeas", func() engine.Res { return &AugeasRes{} })
} }
// AugeasRes is a resource that enables you to use the augeas resource. // AugeasRes is a resource that enables you to use the augeas resource.
// Currently only allows you to change simple files (e.g sshd_config). // Currently only allows you to change simple files (e.g sshd_config).
type AugeasRes struct { type AugeasRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
init *engine.Init
// File is the path to the file targeted by this resource. // File is the path to the file targeted by this resource.
File string `yaml:"file"` File string `yaml:"file"`
@@ -57,7 +60,7 @@ type AugeasRes struct {
// Sets is a list of changes that will be applied to the file, in the form of // Sets is a list of changes that will be applied to the file, in the form of
// ["path", "value"]. mgmt will run augeas.Get() before augeas.Set(), to // ["path", "value"]. mgmt will run augeas.Get() before augeas.Set(), to
// prevent changing the file when it is not needed. // prevent changing the file when it is not needed.
Sets []AugeasSet `yaml:"sets"` Sets []*AugeasSet `yaml:"sets"`
recWatcher *recwatch.RecWatcher // used to watch the changed files recWatcher *recwatch.RecWatcher // used to watch the changed files
} }
@@ -68,13 +71,31 @@ type AugeasSet struct {
Value string `yaml:"value"` // The value to be set on the given Path. Value string `yaml:"value"` // The value to be set on the given Path.
} }
// Default returns some sensible defaults for this resource. // Cmp compares this set with another one.
func (obj *AugeasRes) Default() Res { func (obj *AugeasSet) Cmp(set *AugeasSet) error {
return &AugeasRes{ if obj == nil && set == nil {
BaseRes: BaseRes{ return nil
MetaParams: DefaultMetaParams, // force a default
},
} }
if obj == nil && set != nil {
return fmt.Errorf("can't compare nil set to set")
}
if obj != nil && set == nil {
return fmt.Errorf("can't compare set to nil set")
}
if obj.Path != set.Path {
return fmt.Errorf("the Path values differ")
}
if obj.Value != set.Value {
return fmt.Errorf("the Value values differ")
}
return nil
}
// Default returns some sensible defaults for this resource.
func (obj *AugeasRes) Default() engine.Res {
return &AugeasRes{}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -88,12 +109,19 @@ func (obj *AugeasRes) Validate() error {
if (obj.Lens == "") != (obj.File == "") { if (obj.Lens == "") != (obj.File == "") {
return fmt.Errorf("the File and Lens params must be specified together") return fmt.Errorf("the File and Lens params must be specified together")
} }
return obj.BaseRes.Validate() return nil
} }
// Init initiates the resource. // Init initializes the resource.
func (obj *AugeasRes) Init() error { func (obj *AugeasRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *AugeasRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -108,16 +136,14 @@ func (obj *AugeasRes) Watch() error {
defer obj.recWatcher.Close() defer obj.recWatcher.Close()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
if obj.debug { if obj.init.Debug {
log.Printf("%s: Watching: %s", obj, obj.File) // attempting to watch... obj.init.Logf("Watching: %s", obj.File) // attempting to watch...
} }
select { select {
@@ -128,29 +154,33 @@ func (obj *AugeasRes) Watch() error {
if err := event.Error; err != nil { if err := event.Error; err != nil {
return errwrap.Wrapf(err, "Unknown %s watcher error", obj) return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
} }
if obj.debug { // don't access event.Body if event.Error isn't nil if obj.init.Debug { // don't access event.Body if event.Error isn't nil
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op) obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
//obj.StateOK(false) // dirty // these events don't invalidate state
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// checkApplySet runs CheckApply for one element of the AugeasRes.Set // checkApplySet runs CheckApply for one element of the AugeasRes.Set
func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set AugeasSet) (bool, error) { func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set *AugeasSet) (bool, error) {
fullpath := fmt.Sprintf("/files/%v/%v", obj.File, set.Path) fullpath := fmt.Sprintf("/files/%v/%v", obj.File, set.Path)
// We do not check for errors because errors are also thrown when // We do not check for errors because errors are also thrown when
@@ -176,7 +206,7 @@ func (obj *AugeasRes) checkApplySet(apply bool, ag *augeas.Augeas, set AugeasSet
// CheckApply method for Augeas resource. // CheckApply method for Augeas resource.
func (obj *AugeasRes) CheckApply(apply bool) (bool, error) { func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
log.Printf("%s: CheckApply: %s", obj, obj.File) obj.init.Logf("CheckApply: %s", obj.File)
// By default we do not set any option to augeas, we use the defaults. // By default we do not set any option to augeas, we use the defaults.
opts := augeas.None opts := augeas.None
if obj.Lens != "" { if obj.Lens != "" {
@@ -224,7 +254,7 @@ func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
return checkOK, nil return checkOK, nil
} }
log.Printf("%s: changes needed, saving", obj) obj.init.Logf("changes needed, saving")
if err = ag.Save(); err != nil { if err = ag.Save(); err != nil {
return false, errwrap.Wrapf(err, "augeas: error while saving augeas values") return false, errwrap.Wrapf(err, "augeas: error while saving augeas values")
} }
@@ -240,41 +270,46 @@ func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
return false, nil return false, nil
} }
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *AugeasRes) Cmp(r engine.Res) error {
// we can only compare to others of the same resource kind
res, ok := r.(*AugeasRes)
if !ok {
return fmt.Errorf("resource is not the same kind")
}
if obj.File != res.File {
return fmt.Errorf("the File params differ")
}
if obj.Lens != res.Lens {
return fmt.Errorf("the Lens params differ")
}
if len(obj.Sets) != len(res.Sets) {
return fmt.Errorf("the length of the two Sets params differs")
}
for i := 0; i < len(obj.Sets); i++ {
if err := obj.Sets[i].Cmp(res.Sets[i]); err != nil {
return errwrap.Wrapf(err, "the Sets item at index %d differs", i)
}
}
return nil
}
// AugeasUID is the UID struct for AugeasRes. // AugeasUID is the UID struct for AugeasRes.
type AugeasUID struct { type AugeasUID struct {
BaseUID engine.BaseUID
name string name string
} }
// UIDs includes all params to make a unique identification of this object. // UIDs includes all params to make a unique identification of this object.
func (obj *AugeasRes) UIDs() []ResUID { func (obj *AugeasRes) UIDs() []engine.ResUID {
x := &AugeasUID{ x := &AugeasUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name, name: obj.Name(),
} }
return []ResUID{x} return []engine.ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *AugeasRes) GroupCmp(r Res) bool {
return false // Augeas commands can not be grouped together.
}
// Compare two resources and return if they are equivalent.
func (obj *AugeasRes) Compare(r Res) bool {
// we can only compare AugeasRes to others of the same resource kind
res, ok := r.(*AugeasRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
return true
} }
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.

View File

@@ -25,7 +25,6 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"net/http" "net/http"
"regexp" "regexp"
@@ -33,6 +32,9 @@ import (
"sync" "sync"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/request"
@@ -45,7 +47,7 @@ import (
) )
func init() { func init() {
RegisterResource("aws:ec2", func() Res { return &AwsEc2Res{} }) engine.RegisterResource("aws:ec2", func() engine.Res { return &AwsEc2Res{} })
} }
const ( const (
@@ -145,7 +147,10 @@ var AwsRegions = []string{
// AWS credentials must be present in ~/.aws - For detailed instructions see // AWS credentials must be present in ~/.aws - For detailed instructions see
// http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html // http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
type AwsEc2Res struct { type AwsEc2Res struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
init *engine.Init
State string `yaml:"state"` // state: running, stopped, terminated State string `yaml:"state"` // state: running, stopped, terminated
Region string `yaml:"region"` // region must match an element of AwsRegions Region string `yaml:"region"` // region must match an element of AwsRegions
Type string `yaml:"type"` // type of ec2 instance, eg: t2.micro Type string `yaml:"type"` // type of ec2 instance, eg: t2.micro
@@ -250,12 +255,8 @@ type postMsg struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *AwsEc2Res) Default() Res { func (obj *AwsEc2Res) Default() engine.Res {
return &AwsEc2Res{ return &AwsEc2Res{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -314,11 +315,13 @@ func (obj *AwsEc2Res) Validate() error {
return fmt.Errorf("you must set watchendpoint with watchlistenaddr to use http watch") return fmt.Errorf("you must set watchendpoint with watchlistenaddr to use http watch")
} }
return obj.BaseRes.Validate() return nil
} }
// Init initializes the resource. // Init initializes the resource.
func (obj *AwsEc2Res) Init() error { func (obj *AwsEc2Res) Init(init *engine.Init) error {
obj.init = init // save for later
// create a client session for the AWS API // create a client session for the AWS API
sess, err := session.NewSession(&aws.Config{ sess, err := session.NewSession(&aws.Config{
Region: aws.String(obj.Region), Region: aws.String(obj.Region),
@@ -380,7 +383,30 @@ func (obj *AwsEc2Res) Init() error {
} }
} }
return obj.BaseRes.Init() // call base init, b/c we're overriding return nil
}
// Close cleans up when we're done. This is needed to delete some of the AWS
// objects created for the SNS endpoint.
func (obj *AwsEc2Res) Close() error {
var errList error
// clean up sns objects created by Init/snsWatch
if obj.snsClient != nil {
// delete the topic and associated subscriptions
if err := obj.snsDeleteTopic(obj.snsTopicArn); err != nil {
errList = multierr.Append(errList, err)
}
// remove the target
if err := obj.cweRemoveTarget(CweTargetID, CweRuleName); err != nil {
errList = multierr.Append(errList, err)
}
// delete the cloudwatch rule
if err := obj.cweDeleteRule(CweRuleName); err != nil {
errList = multierr.Append(errList, err)
}
}
return errList
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -394,12 +420,11 @@ func (obj *AwsEc2Res) Watch() error {
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource state. // longpollWatch uses the ec2 api's built in methods to watch ec2 resource state.
func (obj *AwsEc2Res) longpollWatch() error { func (obj *AwsEc2Res) longpollWatch() error {
send := false send := false
var exit *error
// We tell the engine that we're running right away. This is not correct, // We tell the engine that we're running right away. This is not correct,
// but the api doesn't have a way to signal when the waiters are ready. // but the api doesn't have a way to signal when the waiters are ready.
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err return err // exit if requested
} }
// cancellable context used for exiting cleanly // cancellable context used for exiting cleanly
@@ -463,10 +488,14 @@ func (obj *AwsEc2Res) longpollWatch() error {
// process events from the goroutine // process events from the goroutine
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit return nil
} }
if err := obj.init.Read(event); err != nil {
return err
}
case msg, ok := <-obj.awsChan: case msg, ok := <-obj.awsChan:
if !ok { if !ok {
return nil return nil
@@ -479,103 +508,18 @@ func (obj *AwsEc2Res) longpollWatch() error {
case "", ec2.InstanceStateNamePending, ec2.InstanceStateNameStopping: case "", ec2.InstanceStateNamePending, ec2.InstanceStateNameStopping:
continue continue
default: default:
log.Printf("%s: State: %v", obj, msg.state) obj.init.Logf("State: %v", msg.state)
obj.StateOK(false) obj.init.Dirty() // dirty
send = true send = true
} }
} }
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
} }
} }
}
// stateWaiter waits for an instance to change state and returns the new state.
func stateWaiter(ctx context.Context, instance *ec2.Instance, c *ec2.EC2) (string, error) {
var err error
var name string
// these cases are not permitted
if instance == nil {
return "", fmt.Errorf("nil instance")
} }
if aws.StringValue(instance.State.Name) == "" {
return "", fmt.Errorf("nil or empty state")
}
// get the instance name
for _, tag := range instance.Tags {
if aws.StringValue(tag.Key) == nameKey {
name = aws.StringValue(tag.Value)
}
}
// error if we didn't find one
if name == "" {
return "", fmt.Errorf("name not found")
}
// build the input for the waiters
waitInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{instance.InstanceId},
Filters: []*ec2.Filter{
{
Name: aws.String(nameTag),
Values: []*string{aws.String(name)},
},
},
}
// When we are watching terminated instances and waiting for them to exist,
// we must exclude terminated instances from the waiter input. If we don't,
// the waiter will return even if it finds a terminated instance, which is
// not what we want.
existWaiterFilter := &ec2.Filter{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String(ec2.InstanceStateNameRunning),
aws.String(ec2.InstanceStateNameStopped),
},
}
// Select the appropriate waiter based on the instance state. There are
// five possible states and we will catch every pertinent state change
// (excluding transitional states) by waiting for the next state in the
// instance's lifecycle. For more information about the lifecycle, see:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
switch aws.StringValue(instance.State.Name) {
case ec2.InstanceStateNameRunning, ec2.InstanceStateNameStopping:
err = c.WaitUntilInstanceStoppedWithContext(ctx, waitInput)
case ec2.InstanceStateNameStopped, ec2.InstanceStateNamePending:
err = c.WaitUntilInstanceRunningWithContext(ctx, waitInput)
case ec2.InstanceStateNameTerminated:
waitInput.Filters = append(waitInput.Filters, existWaiterFilter)
err = c.WaitUntilInstanceExistsWithContext(ctx, waitInput)
default:
return "", fmt.Errorf("unrecognized instance state: %s", aws.StringValue(instance.State.Name))
}
if err != nil {
aerr, ok := err.(awserr.Error)
if !ok {
return "", errwrap.Wrapf(err, "error casting awserr")
}
// ignore these errors
if aerr.Code() != request.CanceledErrorCode && aerr.Code() != request.WaiterResourceNotReadyErrorCode {
return "", errwrap.Wrapf(err, "internal waiter error")
}
// If the waiter returns, because it has exceeded the maximum number of
// attempts we return an empty state, which the event processing loop
// ignores, and the longpollWatch goroutine will loop and restart
// the waiter.
if aerr.Message() == AwsErrExceededWaitAttempts {
return "", nil
}
}
// return the instance state
instance, err = describeInstanceByName(c, name)
if err != nil {
return "", errwrap.Wrapf(err, "error describing instances")
}
return aws.StringValue(instance.State.Name), nil
} }
// snsWatch uses amazon's SNS and CloudWatchEvents APIs to get instance state- // snsWatch uses amazon's SNS and CloudWatchEvents APIs to get instance state-
@@ -585,7 +529,6 @@ func stateWaiter(ctx context.Context, instance *ec2.Instance, c *ec2.EC2) (strin
// messages published to the topic and processes them accordingly. // messages published to the topic and processes them accordingly.
func (obj *AwsEc2Res) snsWatch() error { func (obj *AwsEc2Res) snsWatch() error {
send := false send := false
var exit *error
defer obj.wg.Wait() defer obj.wg.Wait()
// create the sns listener // create the sns listener
// closing is handled by http.Server.Shutdown in the defer func below // closing is handled by http.Server.Shutdown in the defer func below
@@ -603,10 +546,10 @@ func (obj *AwsEc2Res) snsWatch() error {
defer cancel() defer cancel()
if err := snsServer.Shutdown(ctx); err != nil { if err := snsServer.Shutdown(ctx); err != nil {
if err != context.Canceled { if err != context.Canceled {
log.Printf("%s: error stopping sns endpoint: %s", obj, err) obj.init.Logf("error stopping sns endpoint: %s", err)
return return
} }
log.Printf("%s: sns server shutdown cancelled", obj) obj.init.Logf("sns server shutdown cancelled")
} }
}() }()
defer close(obj.closeChan) defer close(obj.closeChan)
@@ -618,7 +561,7 @@ func (obj *AwsEc2Res) snsWatch() error {
if err := snsServer.Serve(listener); err != nil { if err := snsServer.Serve(listener); err != nil {
// when we shut down // when we shut down
if err == http.ErrServerClosed { if err == http.ErrServerClosed {
log.Printf("%s: Stopped SNS Endpoint", obj) obj.init.Logf("Stopped SNS Endpoint")
return return
} }
// any other error // any other error
@@ -630,7 +573,7 @@ func (obj *AwsEc2Res) snsWatch() error {
} }
} }
}() }()
log.Printf("%s: Started SNS Endpoint", obj) obj.init.Logf("Started SNS Endpoint")
// Subscribing the endpoint to the topic needs to happen after starting // Subscribing the endpoint to the topic needs to happen after starting
// the http server, so that the server can process the subscription // the http server, so that the server can process the subscription
// confirmation. We won't drop incoming connections from aws by this // confirmation. We won't drop incoming connections from aws by this
@@ -644,10 +587,14 @@ func (obj *AwsEc2Res) snsWatch() error {
// process events // process events
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit return nil
} }
if err := obj.init.Read(event); err != nil {
return err
}
case msg, ok := <-obj.awsChan: case msg, ok := <-obj.awsChan:
if !ok { if !ok {
return nil return nil
@@ -660,25 +607,27 @@ func (obj *AwsEc2Res) snsWatch() error {
// is confirmed, we are ready to receive events, so we // is confirmed, we are ready to receive events, so we
// can notify the engine that we're running. // can notify the engine that we're running.
if msg.event == awsEc2EventWatchReady { if msg.event == awsEc2EventWatchReady {
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err return err // exit if requested
} }
continue continue
} }
log.Printf("%s: State: %v", obj, msg.event) obj.init.Logf("State: %v", msg.event)
obj.StateOK(false) obj.init.Dirty() // dirty
send = true send = true
} }
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for AwsEc2 resource. // CheckApply method for AwsEc2 resource.
func (obj *AwsEc2Res) CheckApply(apply bool) (checkOK bool, err error) { func (obj *AwsEc2Res) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply) obj.init.Logf("CheckApply(%t)", apply)
// find the instance we need to check // find the instance we need to check
instance, err := describeInstanceByName(obj.client, obj.prependName()) instance, err := describeInstanceByName(obj.client, obj.prependName())
@@ -822,44 +771,22 @@ func (obj *AwsEc2Res) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil return false, nil
} }
// AwsEc2UID is the UID struct for AwsEc2Res. // Cmp compares two resources and returns an error if they are not equivalent.
type AwsEc2UID struct { func (obj *AwsEc2Res) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *AwsEc2Res) UIDs() []ResUID {
x := &AwsEc2UID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *AwsEc2Res) GroupCmp(r Res) bool {
_, ok := r.(*AwsEc2Res)
if !ok {
return false
}
return false
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *AwsEc2Res) Compare(r Res) bool { func (obj *AwsEc2Res) Compare(r engine.Res) bool {
// we can only compare AwsEc2Res to others of the same resource kind // we can only compare AwsEc2Res to others of the same resource kind
res, ok := r.(*AwsEc2Res) res, ok := r.(*AwsEc2Res)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State { if obj.State != res.State {
return false return false
} }
@@ -887,6 +814,27 @@ func (obj *AwsEc2Res) Compare(r Res) bool {
return true return true
} }
func (obj *AwsEc2Res) prependName() string {
return AwsPrefix + obj.Name()
}
// AwsEc2UID is the UID struct for AwsEc2Res.
type AwsEc2UID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *AwsEc2Res) UIDs() []engine.ResUID {
x := &AwsEc2UID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -907,90 +855,6 @@ func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
return nil return nil
} }
func (obj *AwsEc2Res) prependName() string {
return AwsPrefix + obj.GetName()
}
// describeInstanceByName takes an ec2 client session and an instance name, and
// returns a *ec2.Instance or an error.
func describeInstanceByName(c *ec2.EC2, name string) (*ec2.Instance, error) {
// get any instance with the specified name, that isn't terminated.
diInput := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String(nameTag),
Values: []*string{aws.String(name)},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String(ec2.InstanceStateNameRunning),
aws.String(ec2.InstanceStateNamePending),
aws.String(ec2.InstanceStateNameStopped),
aws.String(ec2.InstanceStateNameStopping),
},
},
},
}
diOutput, err := c.DescribeInstances(diInput)
if err != nil {
return nil, errwrap.Wrapf(err, "error describing instances")
}
// error if we get more than one reservation.
if len(diOutput.Reservations) > 1 {
return nil, fmt.Errorf("too many reservations")
}
// error if we got a reservation without exactly one instance.
if len(diOutput.Reservations) != 0 && len(diOutput.Reservations[0].Instances) != 1 {
return nil, fmt.Errorf("wrong number of instances")
}
// if we didn't find an instance, we consider it 'terminated'.
if len(diOutput.Reservations) == 0 {
return &ec2.Instance{
State: &ec2.InstanceState{
Name: aws.String(ec2.InstanceStateNameTerminated),
},
Tags: []*ec2.Tag{
{
Key: aws.String(nameKey),
Value: aws.String(name),
},
},
}, nil
}
return diOutput.Reservations[0].Instances[0], nil
}
// describeInstanceByID takes an ec2 client session and a pointer to an
// instanceID, and returns an *ec2.Instance or an error.
func describeInstanceByID(c *ec2.EC2, instanceID *string) (*ec2.Instance, error) {
if instanceID == nil {
return nil, fmt.Errorf("instanceID is nil")
}
// get any instance with the specified instanceID.
diInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{instanceID},
}
diOutput, err := c.DescribeInstances(diInput)
if err != nil {
return nil, errwrap.Wrapf(err, "error describing instances")
}
// error if we didn't find exactly one reservation with one instance.
if len(diOutput.Reservations) != 1 {
return nil, fmt.Errorf("wrong number of reservations")
}
if len(diOutput.Reservations[0].Instances) != 1 {
return nil, fmt.Errorf("wrong number of instances")
}
return diOutput.Reservations[0].Instances[0], nil
}
// snsListener returns a listener bound to listenAddr. // snsListener returns a listener bound to listenAddr.
func (obj *AwsEc2Res) snsListener(listenAddr string) (net.Listener, error) { func (obj *AwsEc2Res) snsListener(listenAddr string) (net.Listener, error) {
addr := listenAddr addr := listenAddr
@@ -1017,7 +881,7 @@ func (obj *AwsEc2Res) snsPostHandler(w http.ResponseWriter, req *http.Request) {
decoder := json.NewDecoder(req.Body) decoder := json.NewDecoder(req.Body)
var post postData var post postData
if err := decoder.Decode(&post); err != nil { if err := decoder.Decode(&post); err != nil {
log.Printf("%s: error decoding post: %s", obj, err) obj.init.Logf("error decoding post: %s", err)
http.Error(w, "Bad request", http.StatusBadRequest) http.Error(w, "Bad request", http.StatusBadRequest)
if obj.ErrorOnMalformedPost { if obj.ErrorOnMalformedPost {
select { select {
@@ -1032,7 +896,7 @@ func (obj *AwsEc2Res) snsPostHandler(w http.ResponseWriter, req *http.Request) {
// Verify the x509 signature. If there is an error verifying the // Verify the x509 signature. If there is an error verifying the
// signature, we print the error, ignore the event and return. // signature, we print the error, ignore the event and return.
if err := obj.snsVerifySignature(post); err != nil { if err := obj.snsVerifySignature(post); err != nil {
log.Printf("%s: error verifying signature: %s", obj, err) obj.init.Logf("error verifying signature: %s", err)
http.Error(w, "Bad request", http.StatusBadRequest) http.Error(w, "Bad request", http.StatusBadRequest)
return return
} }
@@ -1181,7 +1045,7 @@ func (obj *AwsEc2Res) snsMakeTopic() (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
log.Printf("%s: Created SNS Topic", obj) obj.init.Logf("Created SNS Topic")
if topic.TopicArn == nil { if topic.TopicArn == nil {
return "", fmt.Errorf("TopicArn is nil") return "", fmt.Errorf("TopicArn is nil")
} }
@@ -1197,7 +1061,7 @@ func (obj *AwsEc2Res) snsDeleteTopic(topicArn string) error {
if _, err := obj.snsClient.DeleteTopic(dtInput); err != nil { if _, err := obj.snsClient.DeleteTopic(dtInput); err != nil {
return err return err
} }
log.Printf("%s: Deleted SNS Topic", obj) obj.init.Logf("Deleted SNS Topic")
return nil return nil
} }
@@ -1214,7 +1078,7 @@ func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
if err != nil { if err != nil {
return err return err
} }
log.Printf("%s: Created Subscription", obj) obj.init.Logf("Created Subscription")
return nil return nil
} }
@@ -1230,7 +1094,7 @@ func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) erro
if err != nil { if err != nil {
return err return err
} }
log.Printf("%s: Subscription Confirmed", obj) obj.init.Logf("Subscription Confirmed")
return nil return nil
} }
@@ -1306,7 +1170,7 @@ func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
for _, statement := range policy.Statement { for _, statement := range policy.Statement {
if statement == permission { if statement == permission {
// if it's already there, we're done // if it's already there, we're done
log.Printf("%s: Target Already Authorized", obj) obj.init.Logf("Target Already Authorized")
return nil return nil
} }
} }
@@ -1328,7 +1192,7 @@ func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
if err != nil { if err != nil {
return err return err
} }
log.Printf("%s: Authorized Target", obj) obj.init.Logf("Authorized Target")
return nil return nil
} }
@@ -1358,7 +1222,7 @@ func (obj *AwsEc2Res) cweMakeRule(name, eventPattern string) error {
if _, err := obj.cweClient.PutRule(putRuleInput); err != nil { if _, err := obj.cweClient.PutRule(putRuleInput); err != nil {
return err return err
} }
log.Printf("%s: Created CloudWatch Rule", obj) obj.init.Logf("Created CloudWatch Rule")
return nil return nil
} }
@@ -1368,7 +1232,7 @@ func (obj *AwsEc2Res) cweDeleteRule(name string) error {
drInput := &cwe.DeleteRuleInput{ drInput := &cwe.DeleteRuleInput{
Name: aws.String(name), Name: aws.String(name),
} }
log.Printf("%s: Deleting CloudWatch Rule", obj) obj.init.Logf("Deleting CloudWatch Rule")
if _, err := obj.cweClient.DeleteRule(drInput); err != nil { if _, err := obj.cweClient.DeleteRule(drInput); err != nil {
return errwrap.Wrapf(err, "error deleting cloudwatch rule") return errwrap.Wrapf(err, "error deleting cloudwatch rule")
} }
@@ -1391,7 +1255,7 @@ func (obj *AwsEc2Res) cweTargetRule(topicArn, targetID, inputPath, ruleName stri
if err != nil { if err != nil {
return errwrap.Wrapf(err, "error putting cloudwatch target") return errwrap.Wrapf(err, "error putting cloudwatch target")
} }
log.Printf("%s: Targeted SNS Topic", obj) obj.init.Logf("Targeted SNS Topic")
return nil return nil
} }
@@ -1402,34 +1266,176 @@ func (obj *AwsEc2Res) cweRemoveTarget(targetID, ruleName string) error {
Ids: []*string{aws.String(targetID)}, Ids: []*string{aws.String(targetID)},
Rule: aws.String(ruleName), Rule: aws.String(ruleName),
} }
log.Printf("%s: Removing Target", obj) obj.init.Logf("Removing Target")
if _, err := obj.cweClient.RemoveTargets(rtInput); err != nil { if _, err := obj.cweClient.RemoveTargets(rtInput); err != nil {
return errwrap.Wrapf(err, "error removing cloudwatch target") return errwrap.Wrapf(err, "error removing cloudwatch target")
} }
return nil return nil
} }
// Close cleans up when we're done. This is needed to delete some of the AWS // stateWaiter waits for an instance to change state and returns the new state.
// objects created for the SNS endpoint. func stateWaiter(ctx context.Context, instance *ec2.Instance, c *ec2.EC2) (string, error) {
func (obj *AwsEc2Res) Close() error { var err error
var errList error var name string
// clean up sns objects created by Init/snsWatch
if obj.snsClient != nil { // these cases are not permitted
// delete the topic and associated subscriptions if instance == nil {
if err := obj.snsDeleteTopic(obj.snsTopicArn); err != nil { return "", fmt.Errorf("nil instance")
errList = multierr.Append(errList, err)
} }
// remove the target if aws.StringValue(instance.State.Name) == "" {
if err := obj.cweRemoveTarget(CweTargetID, CweRuleName); err != nil { return "", fmt.Errorf("nil or empty state")
errList = multierr.Append(errList, err)
} }
// delete the cloudwatch rule
if err := obj.cweDeleteRule(CweRuleName); err != nil { // get the instance name
errList = multierr.Append(errList, err) for _, tag := range instance.Tags {
if aws.StringValue(tag.Key) == nameKey {
name = aws.StringValue(tag.Value)
} }
} }
if err := obj.BaseRes.Close(); err != nil { // error if we didn't find one
errList = multierr.Append(errList, err) // list of errors if name == "" {
return "", fmt.Errorf("name not found")
} }
return errList
// build the input for the waiters
waitInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{instance.InstanceId},
Filters: []*ec2.Filter{
{
Name: aws.String(nameTag),
Values: []*string{aws.String(name)},
},
},
}
// When we are watching terminated instances and waiting for them to exist,
// we must exclude terminated instances from the waiter input. If we don't,
// the waiter will return even if it finds a terminated instance, which is
// not what we want.
existWaiterFilter := &ec2.Filter{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String(ec2.InstanceStateNameRunning),
aws.String(ec2.InstanceStateNameStopped),
},
}
// Select the appropriate waiter based on the instance state. There are
// five possible states and we will catch every pertinent state change
// (excluding transitional states) by waiting for the next state in the
// instance's lifecycle. For more information about the lifecycle, see:
// http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
switch aws.StringValue(instance.State.Name) {
case ec2.InstanceStateNameRunning, ec2.InstanceStateNameStopping:
err = c.WaitUntilInstanceStoppedWithContext(ctx, waitInput)
case ec2.InstanceStateNameStopped, ec2.InstanceStateNamePending:
err = c.WaitUntilInstanceRunningWithContext(ctx, waitInput)
case ec2.InstanceStateNameTerminated:
waitInput.Filters = append(waitInput.Filters, existWaiterFilter)
err = c.WaitUntilInstanceExistsWithContext(ctx, waitInput)
default:
return "", fmt.Errorf("unrecognized instance state: %s", aws.StringValue(instance.State.Name))
}
if err != nil {
aerr, ok := err.(awserr.Error)
if !ok {
return "", errwrap.Wrapf(err, "error casting awserr")
}
// ignore these errors
if aerr.Code() != request.CanceledErrorCode && aerr.Code() != request.WaiterResourceNotReadyErrorCode {
return "", errwrap.Wrapf(err, "internal waiter error")
}
// If the waiter returns, because it has exceeded the maximum number of
// attempts we return an empty state, which the event processing loop
// ignores, and the longpollWatch goroutine will loop and restart
// the waiter.
if aerr.Message() == AwsErrExceededWaitAttempts {
return "", nil
}
}
// return the instance state
instance, err = describeInstanceByName(c, name)
if err != nil {
return "", errwrap.Wrapf(err, "error describing instances")
}
return aws.StringValue(instance.State.Name), nil
}
// describeInstanceByName takes an ec2 client session and an instance name, and
// returns a *ec2.Instance or an error.
func describeInstanceByName(c *ec2.EC2, name string) (*ec2.Instance, error) {
// get any instance with the specified name, that isn't terminated.
diInput := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String(nameTag),
Values: []*string{aws.String(name)},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String(ec2.InstanceStateNameRunning),
aws.String(ec2.InstanceStateNamePending),
aws.String(ec2.InstanceStateNameStopped),
aws.String(ec2.InstanceStateNameStopping),
},
},
},
}
diOutput, err := c.DescribeInstances(diInput)
if err != nil {
return nil, errwrap.Wrapf(err, "error describing instances")
}
// error if we get more than one reservation.
if len(diOutput.Reservations) > 1 {
return nil, fmt.Errorf("too many reservations")
}
// error if we got a reservation without exactly one instance.
if len(diOutput.Reservations) != 0 && len(diOutput.Reservations[0].Instances) != 1 {
return nil, fmt.Errorf("wrong number of instances")
}
// if we didn't find an instance, we consider it 'terminated'.
if len(diOutput.Reservations) == 0 {
return &ec2.Instance{
State: &ec2.InstanceState{
Name: aws.String(ec2.InstanceStateNameTerminated),
},
Tags: []*ec2.Tag{
{
Key: aws.String(nameKey),
Value: aws.String(name),
},
},
}, nil
}
return diOutput.Reservations[0].Instances[0], nil
}
// describeInstanceByID takes an ec2 client session and a pointer to an
// instanceID, and returns an *ec2.Instance or an error.
func describeInstanceByID(c *ec2.EC2, instanceID *string) (*ec2.Instance, error) {
if instanceID == nil {
return nil, fmt.Errorf("instanceID is nil")
}
// get any instance with the specified instanceID.
diInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{instanceID},
}
diOutput, err := c.DescribeInstances(diInput)
if err != nil {
return nil, errwrap.Wrapf(err, "error describing instances")
}
// error if we didn't find exactly one reservation with one instance.
if len(diOutput.Reservations) != 1 {
return nil, fmt.Errorf("wrong number of reservations")
}
if len(diOutput.Reservations[0].Instances) != 1 {
return nil, fmt.Errorf("wrong number of instances")
}
return diOutput.Reservations[0].Instances[0], nil
} }

View File

@@ -0,0 +1,441 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !nodocker
package resources
import (
"context"
"fmt"
"io/ioutil"
"regexp"
"strings"
"time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
errwrap "github.com/pkg/errors"
)
const (
// ContainerRunning is the running container state.
ContainerRunning = "running"
// ContainerStopped is the stopped container state.
ContainerStopped = "stopped"
// ContainerRemoved is the removed container state.
ContainerRemoved = "removed"
// initCtxTimeout is the length of time, in seconds, before requests are
// cancelled in Init.
initCtxTimeout = 20
// checkApplyCtxTimeout is the length of time, in seconds, before requests
// are cancelled in CheckApply.
checkApplyCtxTimeout = 120
)
func init() {
engine.RegisterResource("docker:container", func() engine.Res { return &DockerContainerRes{} })
}
// DockerContainerRes is a docker container resource.
type DockerContainerRes struct {
traits.Base // add the base methods without re-implementation
traits.Edgeable
// State of the container must be running, stopped, or removed.
State string `yaml:"state"`
// Image is a docker image, or image:tag.
Image string `yaml:"image"`
// Cmd is a command, or list of commands to run on the container.
Cmd []string `yaml:"cmd"`
// Env is a list of environment variables. E.g. ["VAR=val",].
Env []string `yaml:"env"`
// Ports is a map of port bindings. E.g. {"tcp" => {80 => 8080},}.
Ports map[string]map[int64]int64 `yaml:"ports"`
// APIVersion allows you to override the host's default client API version.
APIVersion string `yaml:"apiversion"`
// Force, if true, will destroy and redeploy the container if the image is
// incorrect.
Force bool `yaml:"force"`
client *client.Client // docker api client
init *engine.Init
}
// Default returns some sensible defaults for this resource.
func (obj *DockerContainerRes) Default() engine.Res {
return &DockerContainerRes{}
}
// Validate if the params passed in are valid data.
func (obj *DockerContainerRes) Validate() error {
// validate state
if obj.State != ContainerRunning && obj.State != ContainerStopped && obj.State != ContainerRemoved {
return fmt.Errorf("state must be running, stopped or removed")
}
// validate env
for _, env := range obj.Env {
if !strings.Contains(env, "=") || strings.Contains(env, " ") {
return fmt.Errorf("invalid environment variable: %s", env)
}
}
// validate ports
for k, v := range obj.Ports {
if k != "tcp" && k != "udp" && k != "sctp" {
return fmt.Errorf("ports primary key should be tcp, udp or sctp")
}
for p, q := range v {
if (p < 1 || p > 65535) || (q < 1 || q > 65535) {
return fmt.Errorf("ports must be between 1 and 65535")
}
}
}
// validate APIVersion
if obj.APIVersion != "" {
verOK, err := regexp.MatchString(`^(v)[1-9]\.[0-9]\d*$`, obj.APIVersion)
if err != nil {
return errwrap.Wrapf(err, "error matching apiversion string")
}
if !verOK {
return fmt.Errorf("invalid apiversion: %s", obj.APIVersion)
}
}
return nil
}
// Init runs some startup code for this resource.
func (obj *DockerContainerRes) Init(init *engine.Init) error {
var err error
obj.init = init // save for later
ctx, cancel := context.WithTimeout(context.Background(), initCtxTimeout*time.Second)
defer cancel()
// Initialize the docker client.
obj.client, err = client.NewClient(client.DefaultDockerHost, obj.APIVersion, nil, nil)
if err != nil {
return errwrap.Wrapf(err, "error creating docker client")
}
// Validate the image.
resp, err := obj.client.ImageSearch(ctx, obj.Image, types.ImageSearchOptions{Limit: 1})
if err != nil {
return errwrap.Wrapf(err, "error searching for image")
}
if len(resp) == 0 {
return fmt.Errorf("image: %s not found", obj.Image)
}
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *DockerContainerRes) Close() error {
return obj.client.Close() // close the docker client
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *DockerContainerRes) Watch() error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
eventChan, errChan := obj.client.Events(ctx, types.EventsOptions{})
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // exit if requested
}
var send = false // send event?
for {
select {
case event, ok := <-eventChan:
if !ok { // channel shutdown
return nil
}
if obj.init.Debug {
obj.init.Logf("%+v", event)
}
send = true
obj.init.Dirty() // dirty
case err, ok := <-errChan:
if !ok {
return nil
}
return err
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}
// CheckApply method for Docker resource.
func (obj *DockerContainerRes) CheckApply(apply bool) (checkOK bool, err error) {
var id string
var destroy bool
ctx, cancel := context.WithTimeout(context.Background(), checkApplyCtxTimeout*time.Second)
defer cancel()
// List any container whose name matches this resource.
opts := types.ContainerListOptions{
All: true,
Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: obj.Name()}),
}
containerList, err := obj.client.ContainerList(ctx, opts)
if err != nil {
return false, errwrap.Wrapf(err, "error listing containers")
}
if len(containerList) > 1 {
return false, fmt.Errorf("more than one container named %s", obj.Name())
}
if len(containerList) == 0 && obj.State == ContainerRemoved {
return true, nil
}
if len(containerList) == 1 {
// If the state and image are correct, we're done.
if containerList[0].State == obj.State && containerList[0].Image == obj.Image {
return true, nil
}
id = containerList[0].ID // save the id for later
// If the image is wrong, and force is true, mark the container for
// destruction.
if containerList[0].Image != obj.Image && obj.Force {
destroy = true
}
// Otherwise return an error.
if containerList[0].Image != obj.Image && !obj.Force {
return false, fmt.Errorf("%s exists but has the wrong image: %s", obj.Name(), containerList[0].Image)
}
}
if !apply {
return false, nil
}
if obj.State == ContainerStopped { // container exists and should be stopped
return false, obj.containerStop(ctx, id, nil)
}
if obj.State == ContainerRemoved { // container exists and should be removed
if err := obj.containerStop(ctx, id, nil); err != nil {
return false, err
}
return false, obj.containerRemove(ctx, id, types.ContainerRemoveOptions{})
}
if destroy {
if err := obj.containerStop(ctx, id, nil); err != nil {
return false, err
}
if err := obj.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
return false, err
}
containerList = []types.Container{} // zero the list
}
if len(containerList) == 0 { // no container was found
// Download the specified image if it doesn't exist locally.
p, err := obj.client.ImagePull(ctx, obj.Image, types.ImagePullOptions{})
if err != nil {
return false, errwrap.Wrapf(err, "error pulling image")
}
// Wait for the image to download, EOF signals that it's done.
if _, err := ioutil.ReadAll(p); err != nil {
return false, errwrap.Wrapf(err, "error reading image pull result")
}
// set up port bindings
containerConfig := &container.Config{
Image: obj.Image,
Cmd: obj.Cmd,
Env: obj.Env,
ExposedPorts: make(map[nat.Port]struct{}),
}
hostConfig := &container.HostConfig{
PortBindings: make(map[nat.Port][]nat.PortBinding),
}
for k, v := range obj.Ports {
for p, q := range v {
containerConfig.ExposedPorts[nat.Port(k)] = struct{}{}
hostConfig.PortBindings[nat.Port(fmt.Sprintf("%d/%s", p, k))] = []nat.PortBinding{
{
HostIP: "0.0.0.0",
HostPort: fmt.Sprintf("%d", q),
},
}
}
}
c, err := obj.client.ContainerCreate(ctx, containerConfig, hostConfig, nil, obj.Name())
if err != nil {
return false, errwrap.Wrapf(err, "error creating container")
}
id = c.ID
}
return false, obj.containerStart(ctx, id, types.ContainerStartOptions{})
}
// containerStart starts the specified container, and waits for it to start.
func (obj *DockerContainerRes) containerStart(ctx context.Context, id string, opts types.ContainerStartOptions) error {
// Get an events channel for the container we're about to start.
eventOpts := types.EventsOptions{
Filters: filters.NewArgs(filters.KeyValuePair{Key: "container", Value: id}),
}
eventCh, errCh := obj.client.Events(ctx, eventOpts)
// Start the container.
if err := obj.client.ContainerStart(ctx, id, opts); err != nil {
return errwrap.Wrapf(err, "error starting container")
}
// Wait for a message on eventChan that says the container has started.
select {
case event := <-eventCh:
if event.Status != "start" {
return fmt.Errorf("unexpected event: %+v", event)
}
case err := <-errCh:
return errwrap.Wrapf(err, "error waiting for container start")
}
return nil
}
// containerStop stops the specified container and waits for it to stop.
func (obj *DockerContainerRes) containerStop(ctx context.Context, id string, timeout *time.Duration) error {
ch, errCh := obj.client.ContainerWait(ctx, id, container.WaitConditionNotRunning)
obj.client.ContainerStop(ctx, id, timeout)
select {
case <-ch:
case err := <-errCh:
return errwrap.Wrapf(err, "error waiting for container to stop")
}
return nil
}
// containerRemove removes the specified container and waits for it to be
// removed.
func (obj *DockerContainerRes) containerRemove(ctx context.Context, id string, opts types.ContainerRemoveOptions) error {
ch, errCh := obj.client.ContainerWait(ctx, id, container.WaitConditionRemoved)
obj.client.ContainerRemove(ctx, id, opts)
select {
case <-ch:
case err := <-errCh:
return errwrap.Wrapf(err, "error waiting for container to be removed")
}
return nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *DockerContainerRes) Cmp(r engine.Res) error {
// we can only compare DockerContainerRes to others of the same resource kind
res, ok := r.(*DockerContainerRes)
if !ok {
return fmt.Errorf("error casting r to *DockerContainerRes")
}
if obj.Name() != res.Name() {
return fmt.Errorf("names differ")
}
if err := util.SortedStrSliceCompare(obj.Cmd, res.Cmd); err != nil {
return errwrap.Wrapf(err, "cmd differs")
}
if err := util.SortedStrSliceCompare(obj.Env, res.Env); err != nil {
return errwrap.Wrapf(err, "env differs")
}
if len(obj.Ports) != len(res.Ports) {
return fmt.Errorf("ports length differs")
}
for k, v := range obj.Ports {
for p, q := range v {
if w, ok := res.Ports[k][p]; !ok || q != w {
return fmt.Errorf("ports differ")
}
}
}
if obj.APIVersion != res.APIVersion {
return fmt.Errorf("apiversions differ")
}
if obj.Force != res.Force {
return fmt.Errorf("forces differ")
}
return nil
}
// DockerUID is the UID struct for DockerContainerRes.
type DockerUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *DockerContainerRes) UIDs() []engine.ResUID {
x := &DockerUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *DockerContainerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes DockerContainerRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*DockerContainerRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to DockerContainerRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = DockerContainerRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -0,0 +1,201 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !nodocker
package resources
import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"testing"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
)
var res *DockerContainerRes
var id string
func TestMain(m *testing.M) {
var setupCode, testCode, cleanupCode int
if err := setup(); err != nil {
log.Printf("error during setup: %s", err)
setupCode = 1
}
if setupCode == 0 {
testCode = m.Run()
}
if err := cleanup(); err != nil {
log.Printf("error during cleanup: %s", err)
cleanupCode = 1
}
os.Exit(setupCode + testCode + cleanupCode)
}
func Test_containerStart(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := res.containerStart(ctx, id, types.ContainerStartOptions{}); err != nil {
t.Errorf("containerStart() error: %s", err)
return
}
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
filters.KeyValuePair{Key: "status", Value: "running"},
),
},
)
if err != nil {
t.Errorf("error listing containers: %s", err)
return
}
if len(l) != 1 {
t.Errorf("failed to start container")
return
}
}
func Test_containerStop(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := res.containerStop(ctx, id, nil); err != nil {
t.Errorf("containerStop() error: %s", err)
return
}
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
),
},
)
if err != nil {
t.Errorf("error listing containers: %s", err)
return
}
if len(l) != 0 {
t.Errorf("failed to stop container")
return
}
}
func Test_containerRemove(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
if err := res.containerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
t.Errorf("containerRemove() error: %s", err)
return
}
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
All: true,
Filters: filters.NewArgs(
filters.KeyValuePair{Key: "id", Value: id},
),
},
)
if err != nil {
t.Errorf("error listing containers: %s", err)
return
}
if len(l) != 0 {
t.Errorf("failed to remove container")
return
}
}
func setup() error {
var err error
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
res = &DockerContainerRes{}
res.Init(res.init)
p, err := res.client.ImagePull(ctx, "alpine", types.ImagePullOptions{})
if err != nil {
return fmt.Errorf("error pulling image: %s", err)
}
if _, err := ioutil.ReadAll(p); err != nil {
return fmt.Errorf("error reading image pull result: %s", err)
}
resp, err := res.client.ContainerCreate(
ctx,
&container.Config{
Image: "alpine",
Cmd: []string{"sleep", "100"},
},
&container.HostConfig{},
nil,
"mgmt-test",
)
if err != nil {
return fmt.Errorf("error creating container: %s", err)
}
id = resp.ID
return nil
}
func cleanup() error {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
l, err := res.client.ContainerList(
ctx,
types.ContainerListOptions{
All: true,
Filters: filters.NewArgs(filters.KeyValuePair{Key: "id", Value: id}),
},
)
if err != nil {
return fmt.Errorf("error listing containers: %s", err)
}
if len(l) > 0 {
if err := res.client.ContainerStop(ctx, id, nil); err != nil {
return fmt.Errorf("error stopping container: %s", err)
}
if err := res.client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{}); err != nil {
return fmt.Errorf("error removing container: %s", err)
}
}
return nil
}

View File

@@ -21,25 +21,31 @@ import (
"bufio" "bufio"
"bytes" "bytes"
"fmt" "fmt"
"log"
"os/exec" "os/exec"
"os/user" "os/user"
"strings" "strings"
"sync" "sync"
"syscall" "syscall"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("exec", func() Res { return &ExecRes{} }) engine.RegisterResource("exec", func() engine.Res { return &ExecRes{} })
} }
// ExecRes is an exec resource for running commands. // ExecRes is an exec resource for running commands.
type ExecRes struct { type ExecRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Edgeable
init *engine.Init
Cmd string `yaml:"cmd"` // the command to run Cmd string `yaml:"cmd"` // the command to run
Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd
Timeout int `yaml:"timeout"` // the cmd timeout in seconds Timeout int `yaml:"timeout"` // the cmd timeout in seconds
@@ -52,15 +58,13 @@ type ExecRes struct {
Output *string // all cmd output, read only, do not set! Output *string // all cmd output, read only, do not set!
Stdout *string // the cmd stdout, read only, do not set! Stdout *string // the cmd stdout, read only, do not set!
Stderr *string // the cmd stderr, read only, do not set! Stderr *string // the cmd stderr, read only, do not set!
wg *sync.WaitGroup
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *ExecRes) Default() Res { func (obj *ExecRes) Default() engine.Res {
return &ExecRes{ return &ExecRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -80,37 +84,27 @@ func (obj *ExecRes) Validate() error {
} }
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *ExecRes) Init() error { func (obj *ExecRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
obj.wg = &sync.WaitGroup{}
return nil
} }
// BufioChanScanner wraps the scanner output in a channel. // Close is run by the engine to clean up after the resource is done.
func (obj *ExecRes) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan error) { func (obj *ExecRes) Close() error {
ch, errch := make(chan string), make(chan error) return nil
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
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
func (obj *ExecRes) Watch() error { func (obj *ExecRes) Watch() error {
var send = false // send event? ioChan := make(chan *bufioOutput)
var exit *error defer obj.wg.Wait()
bufioch, errch := make(chan string), make(chan error)
if obj.WatchCmd != "" { if obj.WatchCmd != "" {
var cmdName string var cmdName string
@@ -157,43 +151,50 @@ func (obj *ExecRes) Watch() error {
return errwrap.Wrapf(err, "error starting Cmd") return errwrap.Wrapf(err, "error starting Cmd")
} }
bufioch, errch = obj.BufioChanScanner(scanner) ioChan = obj.bufioChanScanner(scanner)
} }
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event?
for { for {
select { select {
case text := <-bufioch: case data, ok := <-ioChan:
// each time we get a line of output, we loop! if !ok { // EOF
log.Printf("%s: Watch output: %s", obj, text)
if text != "" {
send = true
obj.StateOK(false) // something made state dirty
}
case err := <-errch:
if err == nil { // EOF
// FIXME: add an "if watch command ends/crashes" // FIXME: add an "if watch command ends/crashes"
// restart or generate error option // restart or generate error option
return fmt.Errorf("reached EOF") return fmt.Errorf("reached EOF")
} }
if err := data.err; err != nil {
// error reading input? // error reading input?
return errwrap.Wrapf(err, "unknown error") return errwrap.Wrapf(err, "unknown error")
}
case event := <-obj.Events(): // each time we get a line of output, we loop!
if exit, send = obj.ReadEvent(event); exit != nil { obj.init.Logf("watch output: %s", data.text)
return *exit // exit if data.text != "" {
send = true
obj.init.Dirty() // dirty
}
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -248,7 +249,7 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
} }
// apply portion // apply portion
log.Printf("%s: Apply", obj) obj.init.Logf("Apply")
var cmdName string var cmdName string
var cmdArgs []string var cmdArgs []string
if obj.Shell == "" { if obj.Shell == "" {
@@ -326,25 +327,23 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
wStatus, ok := pStateSys.(syscall.WaitStatus) wStatus, ok := pStateSys.(syscall.WaitStatus)
if !ok { if !ok {
e := errwrap.Wrapf(err, "error running cmd") return false, errwrap.Wrapf(err, "error running cmd")
return false, e
} }
return false, fmt.Errorf("cmd error, exit status: %d", wStatus.ExitStatus()) return false, fmt.Errorf("cmd error, exit status: %d", wStatus.ExitStatus())
} else if err != nil { } else if err != nil {
e := errwrap.Wrapf(err, "general cmd error") return false, errwrap.Wrapf(err, "general cmd error")
return false, e
} }
// TODO: if we printed the stdout while the command is running, this // 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 // would be nice, but it would require terminal log output that doesn't
// interleave all the parallel parts which would mix it all up... // interleave all the parallel parts which would mix it all up...
if s := out.String(); s == "" { if s := out.String(); s == "" {
log.Printf("%s: Command output is empty!", obj) obj.init.Logf("Command output is empty!")
} else { } else {
log.Printf("%s: Command output is:", obj) obj.init.Logf("Command output is:")
log.Printf(out.String()) obj.init.Logf(out.String())
} }
// The state tracking is for exec resources that can't "detect" their // The state tracking is for exec resources that can't "detect" their
@@ -355,83 +354,21 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
return false, nil // success return false, nil // success
} }
// ExecUID is the UID struct for ExecRes. // Cmp compares two resources and returns an error if they are not equivalent.
type ExecUID struct { func (obj *ExecRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
Cmd string return fmt.Errorf("did not compare")
IfCmd string
// TODO: add more elements here
}
// ExecResAutoEdges holds the state of the auto edge generator.
type ExecResAutoEdges struct {
edges []ResUID
}
// Next returns the next automatic edge.
func (obj *ExecResAutoEdges) Next() []ResUID {
return obj.edges
}
// Test gets results of the earlier Next() call, & returns if we should continue!
func (obj *ExecResAutoEdges) Test(input []bool) bool {
return false // Never keep going
// TODO: We could return false if we find as many edges as the number of different path in cmdFiles()
}
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *ExecRes) AutoEdges() (AutoEdge, error) {
var data []ResUID
for _, x := range obj.cmdFiles() {
var reversed = true
data = append(data, &PkgFileUID{
BaseUID: BaseUID{
Name: obj.GetName(),
Kind: obj.GetKind(),
Reversed: &reversed,
},
path: x, // what matters
})
} }
return &ExecResAutoEdges{ return nil
edges: data,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *ExecRes) UIDs() []ResUID {
x := &ExecUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
Cmd: obj.Cmd,
IfCmd: obj.IfCmd,
// TODO: add more params here
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *ExecRes) GroupCmp(r Res) bool {
_, ok := r.(*ExecRes)
if !ok {
return false
}
return false // not possible atm
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *ExecRes) Compare(r Res) bool { func (obj *ExecRes) Compare(r engine.Res) bool {
// we can only compare ExecRes to others of the same resource kind // we can only compare ExecRes to others of the same resource kind
res, ok := r.(*ExecRes) res, ok := r.(*ExecRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.Cmd != res.Cmd { if obj.Cmd != res.Cmd {
return false return false
@@ -464,6 +401,61 @@ func (obj *ExecRes) Compare(r Res) bool {
return true return true
} }
// ExecUID is the UID struct for ExecRes.
type ExecUID struct {
engine.BaseUID
Cmd string
IfCmd string
// TODO: add more elements here
}
// ExecResAutoEdges holds the state of the auto edge generator.
type ExecResAutoEdges struct {
edges []engine.ResUID
}
// Next returns the next automatic edge.
func (obj *ExecResAutoEdges) Next() []engine.ResUID {
return obj.edges
}
// Test gets results of the earlier Next() call, & returns if we should continue!
func (obj *ExecResAutoEdges) Test(input []bool) bool {
return false // never keep going
// TODO: we could return false if we find as many edges as the number of different path's in cmdFiles()
}
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *ExecRes) AutoEdges() (engine.AutoEdge, error) {
var data []engine.ResUID
for _, x := range obj.cmdFiles() {
var reversed = true
data = append(data, &PkgFileUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
path: x, // what matters
})
}
return &ExecResAutoEdges{
edges: data,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *ExecRes) UIDs() []engine.ResUID {
x := &ExecUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
Cmd: obj.Cmd,
IfCmd: obj.IfCmd,
// TODO: add more params here
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -499,14 +491,14 @@ func (obj *ExecRes) getCredential() (*syscall.Credential, error) {
} }
if obj.Group != "" { if obj.Group != "" {
gid, err = GetGID(obj.Group) gid, err = engineUtil.GetGID(obj.Group)
if err != nil { if err != nil {
return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group) return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group)
} }
} }
if obj.User != "" { if obj.User != "" {
uid, err = GetUID(obj.User) uid, err = engineUtil.GetUID(obj.User)
if err != nil { if err != nil {
return nil, errwrap.Wrapf(err, "error looking up uid for %s", obj.User) return nil, errwrap.Wrapf(err, "error looking up uid for %s", obj.User)
} }
@@ -515,74 +507,6 @@ func (obj *ExecRes) getCredential() (*syscall.Credential, error) {
return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, nil return &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}, nil
} }
// splitWriter mimics what the ssh.CombinedOutput command does, but stores the
// the stdout and stderr separately. This is slightly tricky because we don't
// want the combined output to be interleaved incorrectly. It creates sub writer
// structs which share the same lock and a shared output buffer.
type splitWriter struct {
Stdout *wrapWriter
Stderr *wrapWriter
stdout bytes.Buffer // just the stdout
stderr bytes.Buffer // just the stderr
output bytes.Buffer // combined output
mutex *sync.Mutex
initialized bool // is this initialized?
}
// Init initializes the splitWriter.
func (sw *splitWriter) Init() {
if sw.initialized {
panic("splitWriter is already initialized")
}
sw.mutex = &sync.Mutex{}
sw.Stdout = &wrapWriter{
Mutex: sw.mutex,
Buffer: &sw.stdout,
Output: &sw.output,
}
sw.Stderr = &wrapWriter{
Mutex: sw.mutex,
Buffer: &sw.stderr,
Output: &sw.output,
}
sw.initialized = true
}
// String returns the contents of the combined output buffer.
func (sw *splitWriter) String() string {
if !sw.initialized {
panic("splitWriter is not initialized")
}
return sw.output.String()
}
// wrapWriter is a simple writer which is used internally by splitWriter.
type wrapWriter struct {
Mutex *sync.Mutex
Buffer *bytes.Buffer // stdout or stderr
Output *bytes.Buffer // combined output
Activity bool // did we get any writes?
}
// Write writes to both bytes buffers with a parent lock to mix output safely.
func (w *wrapWriter) Write(p []byte) (int, error) {
// TODO: can we move the lock to only guard around the Output.Write ?
w.Mutex.Lock()
defer w.Mutex.Unlock()
w.Activity = true
i, err := w.Buffer.Write(p) // first write
if err != nil {
return i, err
}
return w.Output.Write(p) // shared write
}
// String returns the contents of the unshared buffer.
func (w *wrapWriter) String() string {
return w.Buffer.String()
}
// cmdFiles returns all the potential files/commands this command might need. // cmdFiles returns all the potential files/commands this command might need.
func (obj *ExecRes) cmdFiles() []string { func (obj *ExecRes) cmdFiles() []string {
var paths []string var paths []string
@@ -603,3 +527,95 @@ func (obj *ExecRes) cmdFiles() []string {
} }
return paths return paths
} }
// bufioOutput is the output struct of the bufioChanScanner channel output.
type bufioOutput struct {
text string
err error
}
// bufioChanScanner wraps the scanner output in a channel.
func (obj *ExecRes) bufioChanScanner(scanner *bufio.Scanner) chan *bufioOutput {
ch := make(chan *bufioOutput)
obj.wg.Add(1)
go func() {
defer obj.wg.Done()
defer close(ch)
for scanner.Scan() {
ch <- &bufioOutput{text: scanner.Text()} // blocks here ?
}
// on EOF, scanner.Err() will be nil
if err := scanner.Err(); err != nil {
ch <- &bufioOutput{err: err} // send any misc errors we encounter
}
}()
return ch
}
// splitWriter mimics what the ssh.CombinedOutput command does, but stores the
// the stdout and stderr separately. This is slightly tricky because we don't
// want the combined output to be interleaved incorrectly. It creates sub writer
// structs which share the same lock and a shared output buffer.
type splitWriter struct {
Stdout *wrapWriter
Stderr *wrapWriter
stdout bytes.Buffer // just the stdout
stderr bytes.Buffer // just the stderr
output bytes.Buffer // combined output
mutex *sync.Mutex
initialized bool // is this initialized?
}
// Init initializes the splitWriter.
func (obj *splitWriter) Init() {
if obj.initialized {
panic("splitWriter is already initialized")
}
obj.mutex = &sync.Mutex{}
obj.Stdout = &wrapWriter{
Mutex: obj.mutex,
Buffer: &obj.stdout,
Output: &obj.output,
}
obj.Stderr = &wrapWriter{
Mutex: obj.mutex,
Buffer: &obj.stderr,
Output: &obj.output,
}
obj.initialized = true
}
// String returns the contents of the combined output buffer.
func (obj *splitWriter) String() string {
if !obj.initialized {
panic("splitWriter is not initialized")
}
return obj.output.String()
}
// wrapWriter is a simple writer which is used internally by splitWriter.
type wrapWriter struct {
Mutex *sync.Mutex
Buffer *bytes.Buffer // stdout or stderr
Output *bytes.Buffer // combined output
Activity bool // did we get any writes?
}
// Write writes to both bytes buffers with a parent lock to mix output safely.
func (obj *wrapWriter) Write(p []byte) (int, error) {
// TODO: can we move the lock to only guard around the Output.Write ?
obj.Mutex.Lock()
defer obj.Mutex.Unlock()
obj.Activity = true
i, err := obj.Buffer.Write(p) // first write
if err != nil {
return i, err
}
return obj.Output.Write(p) // shared write
}
// String returns the contents of the unshared buffer.
func (obj *wrapWriter) String() string {
return obj.Buffer.String()
}

View File

@@ -15,24 +15,36 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources package resources
import ( import (
"testing" "testing"
"github.com/purpleidea/mgmt/engine"
) )
func fakeInit(t *testing.T) *engine.Init {
debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
return &engine.Init{
Running: func() error {
return nil
},
Debug: debug,
Logf: logf,
}
}
func TestExecSendRecv1(t *testing.T) { func TestExecSendRecv1(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world", Cmd: "echo hello world",
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -41,7 +53,7 @@ func TestExecSendRecv1(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine
@@ -73,16 +85,10 @@ func TestExecSendRecv1(t *testing.T) {
func TestExecSendRecv2(t *testing.T) { func TestExecSendRecv2(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world 1>&2", // to stderr Cmd: "echo hello world 1>&2", // to stderr
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -91,7 +97,7 @@ func TestExecSendRecv2(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine
@@ -123,16 +129,10 @@ func TestExecSendRecv2(t *testing.T) {
func TestExecSendRecv3(t *testing.T) { func TestExecSendRecv3(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -141,7 +141,7 @@ func TestExecSendRecv3(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,148 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources
import (
"bytes"
"encoding/base64"
"encoding/gob"
"testing"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autoedge"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/pgraph"
)
func TestFileAutoEdge1(t *testing.T) {
g, err := pgraph.NewGraph("TestGraph")
if err != nil {
t.Errorf("error creating graph: %v", err)
return
}
r1 := &FileRes{
Path: "/tmp/a/b/", // some dir
}
r2 := &FileRes{
Path: "/tmp/a/", // some parent dir
}
r3 := &FileRes{
Path: "/tmp/a/b/c", // some child file
}
g.AddVertex(r1, r2, r3)
if i := g.NumEdges(); i != 0 {
t.Errorf("should have 0 edges instead of: %d", i)
}
debug := testing.Verbose() // set via the -test.v flag to `go test`
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
// run artificially without the entire engine
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
t.Errorf("error running autoedges: %v", err)
}
// two edges should have been added
if i := g.NumEdges(); i != 2 {
t.Errorf("should have 2 edges instead of: %d", i)
}
}
func TestMiscEncodeDecode1(t *testing.T) {
var err error
// encode
var input interface{} = &FileRes{}
b1 := bytes.Buffer{}
e := gob.NewEncoder(&b1)
err = e.Encode(&input) // pass with &
if err != nil {
t.Errorf("Gob failed to Encode: %v", err)
}
str := base64.StdEncoding.EncodeToString(b1.Bytes())
// decode
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Errorf("Base64 failed to Decode: %v", err)
}
b2 := bytes.NewBuffer(bb)
d := gob.NewDecoder(b2)
err = d.Decode(&output) // pass with &
if err != nil {
t.Errorf("Gob failed to Decode: %v", err)
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
}
}
func TestMiscEncodeDecode2(t *testing.T) {
var err error
// encode
input, err := engine.NewNamedResource("file", "file1")
if err != nil {
t.Errorf("Can't create: %v", err)
return
}
b64, err := engineUtil.ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
return
}
output, err := engineUtil.B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
return
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
}
}

View File

@@ -20,26 +20,30 @@ package resources
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os/exec" "os/exec"
"os/user" "os/user"
"strconv" "strconv"
"syscall" "syscall"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("group", func() Res { return &GroupRes{} }) engine.RegisterResource("group", func() engine.Res { return &GroupRes{} })
} }
const groupFile = "/etc/group" const groupFile = "/etc/group"
// GroupRes is a user group resource. // GroupRes is a user group resource.
type GroupRes struct { type GroupRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
init *engine.Init
State string `yaml:"state"` // state: exists, absent State string `yaml:"state"` // state: exists, absent
GID *uint32 `yaml:"gid"` // the group's gid GID *uint32 `yaml:"gid"` // the group's gid
@@ -47,12 +51,8 @@ type GroupRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *GroupRes) Default() Res { func (obj *GroupRes) Default() engine.Res {
return &GroupRes{ return &GroupRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -60,12 +60,19 @@ func (obj *GroupRes) Validate() error {
if obj.State != "exists" && obj.State != "absent" { if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("State must be 'exists' or 'absent'") return fmt.Errorf("State must be 'exists' or 'absent'")
} }
return obj.BaseRes.Validate() return nil
} }
// Init initializes the resource. // Init runs some startup code for this resource.
func (obj *GroupRes) Init() error { func (obj *GroupRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *GroupRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -78,16 +85,14 @@ func (obj *GroupRes) Watch() error {
defer obj.recWatcher.Close() defer obj.recWatcher.Close()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
if obj.debug { if obj.init.Debug {
log.Printf("%s: Watching: %s", obj, groupFile) // attempting to watch... obj.init.Logf("Watching: %s", groupFile) // attempting to watch...
} }
select { select {
@@ -98,34 +103,38 @@ func (obj *GroupRes) Watch() error {
if err := event.Error; err != nil { if err := event.Error; err != nil {
return errwrap.Wrapf(err, "Unknown %s watcher error", obj) return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
} }
if obj.debug { // don't access event.Body if event.Error isn't nil if obj.init.Debug { // don't access event.Body if event.Error isn't nil
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op) obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
//obj.StateOK(false) // dirty // these events don't invalidate state
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for Group resource. // CheckApply method for Group resource.
func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply) obj.init.Logf("CheckApply(%t)", apply)
// check if the group exists // check if the group exists
exists := true exists := true
group, err := user.LookupGroup(obj.GetName()) group, err := user.LookupGroup(obj.Name())
if err != nil { if err != nil {
if _, ok := err.(user.UnknownGroupError); !ok { if _, ok := err.(user.UnknownGroupError); !ok {
return false, errwrap.Wrapf(err, "error looking up group") return false, errwrap.Wrapf(err, "error looking up group")
@@ -148,7 +157,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, errwrap.Wrapf(err, "error looking up GID") return false, errwrap.Wrapf(err, "error looking up GID")
} }
} }
if lookupGID != nil && lookupGID.Name != obj.GetName() { if lookupGID != nil && lookupGID.Name != obj.Name() {
return false, fmt.Errorf("the requested GID belongs to another group") return false, fmt.Errorf("the requested GID belongs to another group")
} }
// get the existing group's GID // get the existing group's GID
@@ -159,7 +168,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
// check if existing group has the wrong GID // check if existing group has the wrong GID
// if it is wrong groupmod will change it to the desired value // if it is wrong groupmod will change it to the desired value
if *obj.GID != uint32(existingGID) { if *obj.GID != uint32(existingGID) {
log.Printf("%s: Inconsistent GID: %s", obj, obj.GetName()) obj.init.Logf("Inconsistent GID: %s", obj.Name())
} }
// if the group exists and has the correct GID, we are done // if the group exists and has the correct GID, we are done
if obj.State == "exists" && *obj.GID == uint32(existingGID) { if obj.State == "exists" && *obj.GID == uint32(existingGID) {
@@ -172,14 +181,14 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
var cmdName string var cmdName string
args := []string{obj.GetName()} args := []string{obj.Name()}
if obj.State == "exists" { if obj.State == "exists" {
if exists { if exists {
log.Printf("%s: Modifying group: %s", obj, obj.GetName()) obj.init.Logf("Modifying group: %s", obj.Name())
cmdName = "groupmod" cmdName = "groupmod"
} else { } else {
log.Printf("%s: Adding group: %s", obj, obj.GetName()) obj.init.Logf("Adding group: %s", obj.Name())
cmdName = "groupadd" cmdName = "groupadd"
} }
if obj.GID != nil { if obj.GID != nil {
@@ -187,7 +196,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
} }
if obj.State == "absent" && exists { if obj.State == "absent" && exists {
log.Printf("%s: Deleting group: %s", obj, obj.GetName()) obj.init.Logf("Deleting group: %s", obj.Name())
cmdName = "groupdel" cmdName = "groupdel"
} }
@@ -220,15 +229,45 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil return false, nil
} }
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *GroupRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *GroupRes) Compare(r engine.Res) bool {
// we can only compare GroupRes to others of the same resource kind
res, ok := r.(*GroupRes)
if !ok {
return false
}
if obj.State != res.State {
return false
}
if (obj.GID == nil) != (res.GID == nil) {
return false
}
if obj.GID != nil && res.GID != nil {
if *obj.GID != *res.GID {
return false
}
}
return true
}
// GroupUID is the UID struct for GroupRes. // GroupUID is the UID struct for GroupRes.
type GroupUID struct { type GroupUID struct {
BaseUID engine.BaseUID
name string name string
gid *uint32 gid *uint32
} }
// IFF aka if and only if they are equivalent, return true. If not, false. // IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *GroupUID) IFF(uid ResUID) bool { func (obj *GroupUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*GroupUID) res, ok := uid.(*GroupUID)
if !ok { if !ok {
return false return false
@@ -248,49 +287,13 @@ func (obj *GroupUID) IFF(uid ResUID) bool {
// UIDs includes all params to make a unique identification of this object. // UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple. // Most resources only return one, although some resources can return multiple.
func (obj *GroupRes) UIDs() []ResUID { func (obj *GroupRes) UIDs() []engine.ResUID {
x := &GroupUID{ x := &GroupUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name, name: obj.Name(),
gid: obj.GID, gid: obj.GID,
} }
return []ResUID{x} return []engine.ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *GroupRes) GroupCmp(r Res) bool {
_, ok := r.(*GroupRes)
if !ok {
return false
}
return false
}
// Compare two resources and return if they are equivalent.
func (obj *GroupRes) Compare(r Res) bool {
// we can only compare GroupRes to others of the same resource kind
res, ok := r.(*GroupRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if (obj.GID == nil) != (res.GID == nil) {
return false
}
if obj.GID != nil && res.GID != nil {
if *obj.GID != *res.GID {
return false
}
}
return true
} }
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.

View File

@@ -20,29 +20,30 @@ package resources
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
"github.com/godbus/dbus" "github.com/godbus/dbus"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
// ErrResourceInsufficientParameters is returned when the configuration of the resource
// is insufficient for the resource to do any useful work.
var ErrResourceInsufficientParameters = errors.New(
"Insufficient parameters for this resource")
func init() { func init() {
RegisterResource("hostname", func() Res { return &HostnameRes{} }) engine.RegisterResource("hostname", func() engine.Res { return &HostnameRes{} })
} }
const ( const (
hostname1Path = "/org/freedesktop/hostname1" hostname1Path = "/org/freedesktop/hostname1"
hostname1Iface = "org.freedesktop.hostname1" hostname1Iface = "org.freedesktop.hostname1"
dbusAddMatch = "org.freedesktop.DBus.AddMatch" dbusPropertiesIface = "org.freedesktop.DBus.Properties"
) )
// ErrResourceInsufficientParameters is returned when the configuration of the
// resource is insufficient for the resource to do any useful work.
var ErrResourceInsufficientParameters = errors.New("insufficient parameters for this resource")
// HostnameRes is a resource that allows setting and watching the hostname. // HostnameRes is a resource that allows setting and watching the hostname.
// //
// StaticHostname is the one configured in /etc/hostname or a similar file. // StaticHostname is the one configured in /etc/hostname or a similar file.
@@ -58,7 +59,10 @@ const (
// Hostname is the fallback value for all 3 fields above, if only Hostname is // Hostname is the fallback value for all 3 fields above, if only Hostname is
// specified, it will set all 3 fields to this value. // specified, it will set all 3 fields to this value.
type HostnameRes struct { type HostnameRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
init *engine.Init
Hostname string `yaml:"hostname"` Hostname string `yaml:"hostname"`
PrettyHostname string `yaml:"pretty_hostname"` PrettyHostname string `yaml:"pretty_hostname"`
StaticHostname string `yaml:"static_hostname"` StaticHostname string `yaml:"static_hostname"`
@@ -68,12 +72,8 @@ type HostnameRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *HostnameRes) Default() Res { func (obj *HostnameRes) Default() engine.Res {
return &HostnameRes{ return &HostnameRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -81,11 +81,13 @@ func (obj *HostnameRes) Validate() error {
if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" { if obj.PrettyHostname == "" && obj.StaticHostname == "" && obj.TransientHostname == "" {
return ErrResourceInsufficientParameters return ErrResourceInsufficientParameters
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *HostnameRes) Init() error { func (obj *HostnameRes) Init(init *engine.Init) error {
obj.init = init // save for later
if obj.PrettyHostname == "" { if obj.PrettyHostname == "" {
obj.PrettyHostname = obj.Hostname obj.PrettyHostname = obj.Hostname
} }
@@ -95,7 +97,12 @@ func (obj *HostnameRes) Init() error {
if obj.TransientHostname == "" { if obj.TransientHostname == "" {
obj.TransientHostname = obj.Hostname obj.TransientHostname = obj.Hostname
} }
return obj.BaseRes.Init() // call base init, b/c we're overriding return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *HostnameRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -106,47 +113,52 @@ func (obj *HostnameRes) Watch() error {
return errwrap.Wrap(err, "Failed to connect to bus") return errwrap.Wrap(err, "Failed to connect to bus")
} }
defer bus.Close() defer bus.Close()
callResult := bus.BusObject().Call( // watch the PropertiesChanged signal on the hostname1 dbus path
"org.freedesktop.DBus.AddMatch", 0, args := fmt.Sprintf(
fmt.Sprintf("type='signal',path='%s',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", hostname1Path)) "type='signal', path='%s', interface='%s', member='PropertiesChanged'",
if callResult.Err != nil { hostname1Path,
return errwrap.Wrap(callResult.Err, "Failed to subscribe to DBus events for hostname1") dbusPropertiesIface,
)
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
return errwrap.Wrap(call.Err, "Failed to subscribe to DBus events for hostname1")
} }
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
signals := make(chan *dbus.Signal, 10) // closed by dbus package signals := make(chan *dbus.Signal, 10) // closed by dbus package
bus.Signal(signals) bus.Signal(signals)
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
for { for {
select { select {
case <-signals: case <-signals:
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, _ := obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
send = true
obj.StateOK(false) // dirty
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
func updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) { func (obj *HostnameRes) updateHostnameProperty(object dbus.BusObject, expectedValue, property, setterName string, apply bool) (checkOK bool, err error) {
propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property) propertyObject, err := object.GetProperty("org.freedesktop.hostname1." + property)
if err != nil { if err != nil {
return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property) return false, errwrap.Wrapf(err, "failed to get org.freedesktop.hostname1.%s", property)
@@ -171,7 +183,7 @@ func updateHostnameProperty(object dbus.BusObject, expectedValue, property, sett
} }
// attempting to apply the changes // attempting to apply the changes
log.Printf("Changing %s: %s => %s", property, propertyValue, expectedValue) obj.init.Logf("Changing %s: %s => %s", property, propertyValue, expectedValue)
if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil { if err := object.Call("org.freedesktop.hostname1."+setterName, 0, expectedValue, false).Err; err != nil {
return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName) return false, errwrap.Wrapf(err, "failed to call org.freedesktop.hostname1.%s", setterName)
} }
@@ -192,21 +204,21 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
checkOK = true checkOK = true
if obj.PrettyHostname != "" { if obj.PrettyHostname != "" {
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply) propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.PrettyHostname, "PrettyHostname", "SetPrettyHostname", apply)
if err != nil { if err != nil {
return false, err return false, err
} }
checkOK = checkOK && propertyCheckOK checkOK = checkOK && propertyCheckOK
} }
if obj.StaticHostname != "" { if obj.StaticHostname != "" {
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply) propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.StaticHostname, "StaticHostname", "SetStaticHostname", apply)
if err != nil { if err != nil {
return false, err return false, err
} }
checkOK = checkOK && propertyCheckOK checkOK = checkOK && propertyCheckOK
} }
if obj.TransientHostname != "" { if obj.TransientHostname != "" {
propertyCheckOK, err := updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply) propertyCheckOK, err := obj.updateHostnameProperty(hostnameObject, obj.TransientHostname, "Hostname", "SetHostname", apply)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -216,46 +228,21 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
return checkOK, nil return checkOK, nil
} }
// HostnameUID is the UID struct for HostnameRes. // Cmp compares two resources and returns an error if they are not equivalent.
type HostnameUID struct { func (obj *HostnameRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
prettyHostname string
staticHostname string
transientHostname string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *HostnameRes) UIDs() []ResUID {
x := &HostnameUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
prettyHostname: obj.PrettyHostname,
staticHostname: obj.StaticHostname,
transientHostname: obj.TransientHostname,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *HostnameRes) GroupCmp(r Res) bool {
return false
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *HostnameRes) Compare(r Res) bool { func (obj *HostnameRes) Compare(r engine.Res) bool {
// we can only compare HostnameRes to others of the same resource kind // we can only compare HostnameRes to others of the same resource kind
res, ok := r.(*HostnameRes) res, ok := r.(*HostnameRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.PrettyHostname != res.PrettyHostname { if obj.PrettyHostname != res.PrettyHostname {
return false return false
@@ -270,6 +257,29 @@ func (obj *HostnameRes) Compare(r Res) bool {
return true return true
} }
// HostnameUID is the UID struct for HostnameRes.
type HostnameUID struct {
engine.BaseUID
name string
prettyHostname string
staticHostname string
transientHostname string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *HostnameRes) UIDs() []engine.ResUID {
x := &HostnameUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
prettyHostname: obj.PrettyHostname,
staticHostname: obj.StaticHostname,
transientHostname: obj.TransientHostname,
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -19,14 +19,16 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"strconv" "strconv"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("kv", func() Res { return &KVRes{} }) engine.RegisterResource("kv", func() engine.Res { return &KVRes{} })
} }
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan. // KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
@@ -47,7 +49,13 @@ const (
// The one exception is that when this resource receives a refresh signal, then // The one exception is that when this resource receives a refresh signal, then
// it will set the value to be the exact one if they are not identical already. // it will set the value to be the exact one if they are not identical already.
type KVRes struct { type KVRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
//traits.Groupable // TODO: it could be useful to group our writes and watches!
traits.Refreshable
traits.Recvable
init *engine.Init
// XXX: shouldn't the name be the key? // XXX: shouldn't the name be the key?
Key string `yaml:"key"` // key to set Key string `yaml:"key"` // key to set
Value *string `yaml:"value"` // value to set (nil to delete) Value *string `yaml:"value"` // value to set (nil to delete)
@@ -57,17 +65,11 @@ type KVRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *KVRes) Default() Res { func (obj *KVRes) Default() engine.Res {
return &KVRes{ return &KVRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
// FIXME: This will catch most issues unless data is passed in after Init with
// the Send/Recv mechanism. Should the engine re-call Validate after Send/Recv?
func (obj *KVRes) Validate() error { func (obj *KVRes) Validate() error {
if obj.Key == "" { if obj.Key == "" {
return fmt.Errorf("key must not be empty") return fmt.Errorf("key must not be empty")
@@ -83,26 +85,32 @@ func (obj *KVRes) Validate() error {
} }
} }
} }
return obj.BaseRes.Validate() return nil
} }
// Init initializes the resource. // Init initializes the resource.
func (obj *KVRes) Init() error { func (obj *KVRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *KVRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
func (obj *KVRes) Watch() error { func (obj *KVRes) Watch() error {
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
ch := obj.Data().World.StrMapWatch(obj.Key) // get possible events! ch := obj.init.World.StrMapWatch(obj.Key) // get possible events!
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
// NOTE: this part is very similar to the file resource code // NOTE: this part is very similar to the file resource code
@@ -113,36 +121,39 @@ func (obj *KVRes) Watch() error {
if err != nil { if err != nil {
return errwrap.Wrapf(err, "unknown %s watcher error", obj) return errwrap.Wrapf(err, "unknown %s watcher error", obj)
} }
if obj.Data().Debug { if obj.init.Debug {
log.Printf("%s: Event!", obj) obj.init.Logf("Event!")
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// lessThanCheck checks for less than validity. // lessThanCheck checks for less than validity.
func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) { func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
v := *obj.Value v := *obj.Value
if value == v { // redundant check for safety if value == v { // redundant check for safety
return true, nil return true, nil
} }
var refresh = obj.Refresh() // do we have a pending reload to apply? var refresh = obj.init.Refresh() // do we have a pending reload to apply?
if !obj.SkipLessThan || refresh { // update lessthan on refresh if !obj.SkipLessThan || refresh { // update lessthan on refresh
return false, nil return false, nil
} }
@@ -175,15 +186,15 @@ func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
// CheckApply method for Password resource. Does nothing, returns happy! // CheckApply method for Password resource. Does nothing, returns happy!
func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply) obj.init.Logf("CheckApply(%t)", apply)
if val, exists := obj.Recv["Value"]; exists && val.Changed { if val, exists := obj.init.Recv()["Value"]; exists && val.Changed {
// if we received on Value, and it changed, wooo, nothing to do. // if we received on Value, and it changed, wooo, nothing to do.
log.Printf("CheckApply: `Value` was updated!") obj.init.Logf("CheckApply: `Value` was updated!")
} }
hostname := obj.Data().Hostname // me hostname := obj.init.Hostname // me
keyMap, err := obj.Data().World.StrMapGet(obj.Key) keyMap, err := obj.init.World.StrMapGet(obj.Key)
if err != nil { if err != nil {
return false, errwrap.Wrapf(err, "check error during StrGet") return false, errwrap.Wrapf(err, "check error during StrGet")
} }
@@ -203,7 +214,7 @@ func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
return true, nil // nothing to delete, we're good! return true, nil // nothing to delete, we're good!
} else if ok && obj.Value == nil { // delete } else if ok && obj.Value == nil { // delete
err := obj.Data().World.StrMapDel(obj.Key) err := obj.init.World.StrMapDel(obj.Key)
return false, errwrap.Wrapf(err, "apply error during StrDel") return false, errwrap.Wrapf(err, "apply error during StrDel")
} }
@@ -211,49 +222,28 @@ func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil return false, nil
} }
if err := obj.Data().World.StrMapSet(obj.Key, *obj.Value); err != nil { if err := obj.init.World.StrMapSet(obj.Key, *obj.Value); err != nil {
return false, errwrap.Wrapf(err, "apply error during StrSet") return false, errwrap.Wrapf(err, "apply error during StrSet")
} }
return false, nil return false, nil
} }
// KVUID is the UID struct for KVRes. // Cmp compares two resources and returns an error if they are not equivalent.
type KVUID struct { func (obj *KVRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *KVRes) UIDs() []ResUID {
x := &KVUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *KVRes) GroupCmp(r Res) bool {
_, ok := r.(*KVRes)
if !ok {
return false
}
return false // TODO: this is doable!
// TODO: it could be useful to group our writes and watches!
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *KVRes) Compare(r Res) bool { func (obj *KVRes) Compare(r engine.Res) bool {
// we can only compare KVRes to others of the same resource kind // we can only compare KVRes to others of the same resource kind
res, ok := r.(*KVRes) res, ok := r.(*KVRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Key != res.Key { if obj.Key != res.Key {
return false return false
@@ -276,6 +266,22 @@ func (obj *KVRes) Compare(r Res) bool {
return true return true
} }
// KVUID is the UID struct for KVRes.
type KVUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *KVRes) UIDs() []engine.ResUID {
x := &KVUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

732
engine/resources/mount.go Normal file
View File

@@ -0,0 +1,732 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
"unsafe"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util"
sdbus "github.com/coreos/go-systemd/dbus"
"github.com/coreos/go-systemd/unit"
systemdUtil "github.com/coreos/go-systemd/util"
fstab "github.com/deniswernert/go-fstab"
"github.com/godbus/dbus"
errwrap "github.com/pkg/errors"
"golang.org/x/sys/unix"
)
func init() {
engine.RegisterResource("mount", func() engine.Res { return &MountRes{} })
}
const (
// procFilesystems is a file that lists all the valid filesystem types.
procFilesystems = "/proc/filesystems"
// procPath is the path to /proc/mounts which contains all active mounts.
procPath = "/proc/mounts"
// fstabPath is the path to the fstab file which defines mounts.
fstabPath = "/etc/fstab"
// fstabUmask is the umask (permissions) used to edit /etc/fstab.
fstabUmask = 0644
// getStatus64 is an ioctl command to get the status of file backed
// loopback devices (i.e. iso file mounts.)
getStatus64 = 0x4C05
// loopFileUmask is the umask (permissions) used to read the loop file.
loopFileUmask = 0660
// devDisk is the path where disks and partitions can be found, organized
// by uuid/label/path.
devDisk = "/dev/disk/"
// diskByUUID is the location of symlinks for devices by UUID.
diskByUUID = devDisk + "by-uuid/"
// diskByLabel is the location of symlinks for devices by label.
diskByLabel = devDisk + "by-label/"
// diskByUUID is the location of symlinks for partitions by UUID.
diskByPartUUID = devDisk + "by-partuuid/"
// diskByLabel is the location of symlinks for partitions by label.
diskByPartLabel = devDisk + "by-partlabel/"
// dbusSystemd1Interface is the base systemd1 path.
dbusSystemd1Path = "/org/freedesktop/systemd1"
// dbusUnitPath is the dbus path where mount unit files are found.
dbusUnitPath = dbusSystemd1Path + "/unit/"
// dbusSystemd1Interface is the base systemd1 interface.
dbusSystemd1Interface = "org.freedesktop.systemd1"
// dbusMountInterface is used as an argument to filter dbus messages.
dbusMountInterface = dbusSystemd1Interface + ".Mount"
// dbusManagerInterface is the systemd manager interface used for
// interfacing with systemd units.
dbusManagerInterface = dbusSystemd1Interface + ".Manager"
// dbusRestartUnit is the dbus method for restarting systemd units.
dbusRestartUnit = dbusManagerInterface + ".RestartUnit"
// restartTimeout is the delay before restartUnit is assumed to have
// failed.
dbusRestartCtxTimeout = 10
// dbusSignalJobRemoved is the name of the dbus signal that produces a
// message when a dbus job is done (or has errored.)
dbusSignalJobRemoved = "JobRemoved"
)
// MountRes is a systemd mount resource that adds/removes entries from
// /etc/fstab, and makes sure the defined device is mounted or unmounted
// accordingly. The mount point is set according to the resource's name.
type MountRes struct {
traits.Base
init *engine.Init
// State must be exists ot absent. If absent, remaining fields are ignored.
State string `yaml:"state"`
Device string `yaml:"device"` // location of the device or image
Type string `yaml:"type"` // the type of filesystem
Options map[string]string `yaml:"options"` // mount options
Freq int `yaml:"freq"` // dump frequency
PassNo int `yaml:"passno"` // verification order
mount *fstab.Mount // struct representing the mount
}
// Default returns some sensible defaults for this resource.
func (obj *MountRes) Default() engine.Res {
return &MountRes{
Options: defaultMntOps(),
}
}
// Validate if the params passed in are valid data.
func (obj *MountRes) Validate() error {
var err error
// validate state
if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("state must be 'exists', or 'absent'")
}
// validate type
fs, err := ioutil.ReadFile(procFilesystems)
if err != nil {
return errwrap.Wrapf(err, "error reading %s", procFilesystems)
}
fsSlice := strings.Fields(string(fs))
for i, x := range fsSlice {
if x == "nodev" {
fsSlice = append(fsSlice[:i], fsSlice[i+1:]...)
}
}
if obj.State != "absent" && !util.StrInList(obj.Type, fsSlice) {
return fmt.Errorf("type must be a valid filesystem type (see /proc/filesystems)")
}
// validate mountpoint
if strings.Contains(obj.Name(), "//") {
return fmt.Errorf("double slashes are not allowed in resource name")
}
if err := unix.Access(obj.Name(), unix.R_OK); err != nil {
return errwrap.Wrapf(err, "error validating mount point: %s", obj.Name())
}
// validate device
device, err := evalSpec(obj.Device) // eval symlink
if err != nil {
return errwrap.Wrapf(err, "error evaluating spec: %s", obj.Device)
}
if err := unix.Access(device, unix.R_OK); err != nil {
return errwrap.Wrapf(err, "error validating device: %s", device)
}
return nil
}
// Init runs some startup code for this resource.
func (obj *MountRes) Init(init *engine.Init) error {
obj.init = init //save for later
obj.mount = &fstab.Mount{
Spec: obj.Device,
File: obj.Name(),
VfsType: obj.Type,
MntOps: obj.Options,
Freq: obj.Freq,
PassNo: obj.PassNo,
}
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *MountRes) Close() error {
return nil
}
// Watch listens for signals from the mount unit associated with the resource.
// It also watch for changes to /etc/fstab, where mounts are defined.
func (obj *MountRes) Watch() error {
// make sure systemd is running
if !systemdUtil.IsRunningSystemd() {
return fmt.Errorf("systemd is not running")
}
// establish a godbus connection
conn, err := util.SystemBusPrivateUsable()
if err != nil {
return errwrap.Wrapf(err, "error establishing dbus connection")
}
defer conn.Close()
// add a dbus rule to watch signals from the mount unit.
args := fmt.Sprintf("type='signal', path='%s', arg0='%s'",
dbusUnitPath+sdbus.PathBusEscape(unit.UnitNamePathEscape((obj.Name()+".mount"))),
dbusMountInterface,
)
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
return errwrap.Wrapf(call.Err, "error creating dbus call")
}
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
ch := make(chan *dbus.Signal)
defer close(ch)
conn.Signal(ch)
defer conn.RemoveSignal(ch)
// watch the fstab file
recWatcher, err := recwatch.NewRecWatcher(fstabPath, false)
if err != nil {
return err
}
// close the recwatcher when we're done
defer recWatcher.Close()
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // bubble up a NACK...
}
var send bool
var done bool
for {
select {
case event, ok := <-recWatcher.Events():
if !ok {
if done {
return nil
}
done = true
continue
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown recwatcher error")
}
if obj.init.Debug {
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
}
obj.init.Dirty()
send = true
case event, ok := <-ch:
if !ok {
if done {
return nil
}
done = true
continue
}
if obj.init.Debug {
obj.init.Logf("event: %+v", event)
}
obj.init.Dirty()
send = true
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}
// fstabCheckApply checks /etc/fstab for entries corresponding to the resource
// definition, and adds or deletes the entry as needed.
func (obj *MountRes) fstabCheckApply(apply bool) (checkOK bool, err error) {
exists, err := fstabEntryExists(fstabPath, obj.mount)
if err != nil {
return false, errwrap.Wrapf(err, "error checking if fstab entry exists")
}
// if everything is as it should be, we're done
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
return true, nil
}
if !apply {
return false, nil
}
obj.init.Logf("fstabCheckApply(%t)", apply)
if obj.State == "exists" {
if err := obj.fstabEntryAdd(fstabPath, obj.mount); err != nil {
return false, errwrap.Wrapf(err, "error adding fstab entry: %+v", obj.mount)
}
return false, nil
}
if err := obj.fstabEntryRemove(fstabPath, obj.mount); err != nil {
return false, errwrap.Wrapf(err, "error removing fstab entry: %+v", obj.mount)
}
return false, nil
}
// mountCheckApply checks if the defined resource is mounted, and mounts or
// unmounts it according to the defined state.
func (obj *MountRes) mountCheckApply(apply bool) (bool, error) {
exists, err := mountExists(procPath, obj.mount)
if err != nil {
return false, errwrap.Wrapf(err, "error checking if mount exists")
}
// if everything is as it should be, we're done
if (exists && obj.State == "exists") || (!exists && obj.State == "absent") {
return true, nil
}
if !apply {
return false, nil
}
obj.init.Logf("mountCheckApply(%t)", apply)
if obj.State == "exists" {
// Reload mounts from /etc/fstab by performing a `daemon-reload` and
// restarting `local-fs.target` and `remote-fs.target` units.
if err := mountReload(); err != nil {
return false, errwrap.Wrapf(err, "error reloading /etc/fstab")
}
return false, nil // we're done
}
// unmount the device
if err := unix.Unmount(obj.Name(), 0); err != nil { // 0 means no flags
return false, errwrap.Wrapf(err, "error unmounting %s", obj.Name())
}
return false, nil
}
// CheckApply is run to check the state and, if apply is true, to apply the
// necessary changes to reach the desired state. This is run before Watch and
// again if Watch finds a change occurring to the state.
func (obj *MountRes) CheckApply(apply bool) (checkOK bool, err error) {
checkOK = true
if c, err := obj.fstabCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
if c, err := obj.mountCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
return checkOK, nil
}
// Cmp compares two resources and return if they are equivalent.
func (obj *MountRes) Cmp(r engine.Res) error {
// we can only compare MountRes to others of the same resource kind
res, ok := r.(*MountRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.State != res.State {
return fmt.Errorf("the State differs")
}
if obj.Type != res.Type {
return fmt.Errorf("the Type differs")
}
if !strMapEq(obj.Options, res.Options) {
return fmt.Errorf("the Options differ")
}
if obj.Freq != res.Freq {
return fmt.Errorf("the Type differs")
}
if obj.PassNo != res.PassNo {
return fmt.Errorf("the PassNo differs")
}
return nil
}
// MountUID is a unique resource identifier.
type MountUID struct {
engine.BaseUID
name string
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *MountUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*MountUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *MountRes) UIDs() []engine.ResUID {
x := &MountUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *MountRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes MountRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*MountRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to MountRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = MountRes(raw) // restore from indirection with type conversion!
return nil
}
// defaultMntOps returns a map that sets the default mount options for fstab
// mounts.
func defaultMntOps() map[string]string {
return map[string]string{"defaults": ""}
}
// strMapEq returns true, if and only if the two provided maps are identical.
func strMapEq(x, y map[string]string) bool {
if len(x) != len(y) {
return false
}
for k, v := range x {
if val, ok := x[k]; !ok || v != val {
return false
}
}
return true
}
// fstabEntryExists checks whether or not a given mount exists in the provided
// fstab file.
func fstabEntryExists(file string, mount *fstab.Mount) (bool, error) {
mounts, err := fstab.ParseFile(file)
if err != nil {
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
}
for _, m := range mounts {
if m.Equals(mount) {
return true, nil
}
}
return false, nil
}
// fstabEntryAdd adds the given mount to the provided fstab file.
func (obj *MountRes) fstabEntryAdd(file string, mount *fstab.Mount) error {
mounts, err := fstab.ParseFile(file)
if err != nil {
return errwrap.Wrapf(err, "error parsing file: %s", file)
}
for _, m := range mounts {
// if the entry exists, we're done
if m.Equals(mount) {
return nil
}
}
// mount does not exist so we need to add it
mounts = append(mounts, mount)
return obj.fstabWrite(file, mounts)
}
// fstabEntryRemove removes the given mount from the provided fstab file.
func (obj *MountRes) fstabEntryRemove(file string, mount *fstab.Mount) error {
mounts, err := fstab.ParseFile(file)
if err != nil {
return errwrap.Wrapf(err, "error parsing file: %s", file)
}
for i, m := range mounts {
// remove any entry with the defined mountpoint
if m.File == mount.File {
mounts = append(mounts[:i], mounts[i+1:]...)
}
}
return obj.fstabWrite(file, mounts)
}
// fstabWrite generates an fstab file with the given mounts, and writes them
// to the provided fstab file.
func (obj *MountRes) fstabWrite(file string, mounts fstab.Mounts) error {
// build the file contents
contents := fmt.Sprintf("# Generated by %s at %d", obj.init.Program, time.Now().UnixNano()) + "\n"
contents = contents + mounts.String() + "\n"
// write the file
if err := ioutil.WriteFile(file, []byte(contents), fstabUmask); err != nil {
return errwrap.Wrapf(err, "error writing fstab file: %s", file)
}
return nil
}
// mountExists returns true, if a given mount exists in the given file
// (typically /proc/mounts.)
func mountExists(file string, mount *fstab.Mount) (bool, error) {
var err error
m := *mount // make a copy so we don't change the definition
// resolve the device's symlink if there is one
if m.Spec, err = evalSpec(mount.Spec); err != nil {
return false, errwrap.Wrapf(err, "error evaluating spec: %s", mount.Spec)
}
// get all mounts
mounts, err := fstab.ParseFile(file)
if err != nil {
return false, errwrap.Wrapf(err, "error parsing file: %s", file)
}
// check for the defined mount
for _, p := range mounts {
found, err := mountCompare(&m, p)
if err != nil {
return false, errwrap.Wrapf(err, "mounts could not be compared: %s and %s", mount.String(), p.String())
}
if found {
return true, nil
}
}
return false, nil
}
// mountCompare compares two mounts. It is assumed that the first comes from
// a resource definition, and the second comes from /proc/mounts. It compares
// the two after resolving the loopback device's file path (if necessary,) and
// ignores freq and passno, as they may differ between the definition and
// /proc/mounts.
func mountCompare(def, proc *fstab.Mount) (bool, error) {
if def.Equals(proc) {
return true, nil
}
if def.File != proc.File {
return false, nil
}
if def.Spec != "" {
procSpec, err := loopFilePath(proc.Spec)
if err != nil {
return false, err
}
if def.Spec != procSpec {
return false, nil
}
}
if !strMapEq(def.MntOps, defaultMntOps()) && !strMapEq(def.MntOps, proc.MntOps) {
return false, nil
}
if def.VfsType != "" && def.VfsType != proc.VfsType {
return false, nil
}
return true, nil
}
// mountReload performs a daemon-reload and restarts fs-local.target and
// fs-remote.target, to let systemd mount any new entries in /etc/fstab.
func mountReload() error {
// establish a godbus connection
conn, err := util.SystemBusPrivateUsable()
if err != nil {
return errwrap.Wrapf(err, "error establishing dbus connection")
}
defer conn.Close()
// systemctl daemon-reload
conn.BusObject().Call("Reload", 0)
// systemctl restart local-fs.target
if err := restartUnit(conn, "local-fs.target"); err != nil {
return errwrap.Wrapf(err, "error restarting unit")
}
// systemctl restart remote-fs.target
if err := restartUnit(conn, "local-fs.target"); err != nil {
return errwrap.Wrapf(err, "error restarting unit")
}
return nil
}
// restartUnit restarts the given dbus unit and waits for it to finish
// starting up. If restartTimeout is exceeded, it will return an error.
func restartUnit(conn *dbus.Conn, unit string) error {
// timeout if we don't get the JobRemoved event
ctx, cancel := context.WithTimeout(context.TODO(), dbusRestartCtxTimeout*time.Second)
defer cancel()
// Add a dbus rule to watch the systemd1 JobRemoved signal used to wait
// until the restart job completes.
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s', arg2='%s'",
dbusSystemd1Path,
dbusManagerInterface,
dbusSignalJobRemoved,
unit,
)
if call := conn.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
return errwrap.Wrapf(call.Err, "error creating dbus call")
}
defer conn.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
// channel for godbus connection
ch := make(chan *dbus.Signal)
defer close(ch)
conn.Signal(ch)
defer conn.RemoveSignal(ch)
// restart the unit
sd1 := conn.Object(dbusSystemd1Interface, dbus.ObjectPath(dbusSystemd1Path))
if call := sd1.Call(dbusRestartUnit, 0, unit, "fail"); call.Err != nil {
return errwrap.Wrapf(call.Err, "error restarting unit: %s", unit)
}
// wait for the job to be removed, indicating completion
select {
case event, ok := <-ch:
if !ok {
return fmt.Errorf("channel closed unexpectedly")
}
if event.Body[3] != "done" {
return fmt.Errorf("unexpected job status: %s", event.Body[3])
}
case <-ctx.Done():
return fmt.Errorf("restarting %s failed due to context timeout", unit)
}
return nil
}
// evalSpec resolves the device from the supplied spec, i.e. it follows the
// symlink, if any, from the provided uuid, label, or path.
func evalSpec(spec string) (string, error) {
var path string
m := &fstab.Mount{}
m.Spec = spec
switch m.SpecType() {
case fstab.UUID:
path = diskByUUID + m.SpecValue()
case fstab.Label:
path = diskByLabel + m.SpecValue()
case fstab.PartUUID:
path = diskByPartUUID + m.SpecValue()
case fstab.PartLabel:
path = diskByPartLabel + m.SpecValue()
case fstab.Path:
path = m.SpecValue()
default:
return "", fmt.Errorf("unexpected spec type: %v", m.SpecType())
}
return filepath.EvalSymlinks(path)
}
// loopFilePath returns the file path of the mounted filesystem image, backing
// the given loopback device.
func loopFilePath(spec string) (string, error) {
// if it's not a loopback device, return the input
if !strings.Contains(spec, "/dev/loop") {
return spec, nil
}
info, err := getLoopInfo(spec)
if err != nil {
return "", errwrap.Wrapf(err, "error getting loop info")
}
// trim the extra null chars off the end of the filename
return string(bytes.Trim(info.FileName[:], "\x00")), nil
}
// loopInfo is a datastructure that holds relevant information about a file
// backed loopback device. Code is based on freddierice/go-losetup.
type loopInfo struct {
Device uint64
INode uint64
RDevice uint64
Offset uint64
SizeLimit uint64
Number uint32
EncryptType uint32
EncryptKeySize uint32
Flags uint32
FileName [64]byte
CryptName [64]byte
EncryptKey [32]byte
Init [2]uint64
}
// getLoopInfo returns a loopInfo struct containing information about the
// provided file backed loopback device.
func getLoopInfo(loop string) (*loopInfo, error) {
// open the loop file
f, err := os.OpenFile(loop, 0, loopFileUmask)
if err != nil {
return nil, fmt.Errorf("error opening %s: %s", loop, err)
}
defer f.Close()
// deserialize the contents
retInfo := &loopInfo{}
_, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), getStatus64, uintptr(unsafe.Pointer(retInfo)))
if errno == unix.ENXIO {
return nil, fmt.Errorf("device not backed by a file")
} else if errno != 0 {
return nil, fmt.Errorf("error getting info about %s (errno: %d)", loop, errno)
}
return retInfo, nil
}

View File

@@ -0,0 +1,343 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources
import (
"io/ioutil"
"os"
"testing"
fstab "github.com/deniswernert/go-fstab"
)
const fstabMock1 = `UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad / ext4 defaults 1 1` + "\n"
const procMock1 = `/tmp/mount0 /mnt/proctest ext4 rw,seclabel,relatime,data=ordered 0 0` + "\n"
var fstabWriteTests = []struct {
in fstab.Mounts
}{
{
fstab.Mounts{
&fstab.Mount{
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
File: "/boot",
VfsType: "ext3",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 2,
},
&fstab.Mount{
Spec: "/dev/mapper/home",
File: "/home",
VfsType: "ext3",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 2,
},
},
},
{
fstab.Mounts{
&fstab.Mount{
Spec: "/dev/cdrom",
File: "/mnt/cdrom",
VfsType: "iso9660",
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
},
},
},
}
func (obj *MountRes) TestFstabWrite(t *testing.T) {
file, err := ioutil.TempFile("", "fstab")
if err != nil {
t.Errorf("error creating temp file: %v", err)
return
}
defer os.Remove(file.Name())
for _, test := range fstabWriteTests {
if err := obj.fstabWrite(file.Name(), test.in); err != nil {
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
return
}
for _, mount := range test.in {
exists, err := fstabEntryExists(file.Name(), mount)
if err != nil {
t.Errorf("error checking if fstab entry %s exists: %v", mount.String(), err)
return
}
if !exists {
t.Errorf("failed to write %s to fstab", mount.String())
}
}
}
}
var fstabEntryAddTests = []struct {
fstabMock []byte
in *fstab.Mount
}{
{
[]byte(fstabMock1),
&fstab.Mount{
Spec: "/dev/sdb1",
File: "/mnt/foo",
VfsType: "ext2",
MntOps: map[string]string{"ro": "", "blocksize": "2048"},
},
},
{
[]byte(fstabMock1),
&fstab.Mount{
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
File: "/",
VfsType: "ext3",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 2,
},
},
}
func (obj *MountRes) TestFstabEntryAdd(t *testing.T) {
file, err := ioutil.TempFile("", "fstab")
if err != nil {
t.Errorf("error creating temp file: %v", err)
return
}
defer os.Remove(file.Name())
for _, test := range fstabEntryAddTests {
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
return
}
err := obj.fstabEntryAdd(file.Name(), test.in)
if err != nil {
t.Errorf("error adding fstab entry: %s to file: %s: %v", test.in.String(), file.Name(), err)
return
}
exists, err := fstabEntryExists(file.Name(), test.in)
if err != nil {
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
return
}
if !exists {
t.Errorf("fstab failed to add entry: %s to fstab", test.in.String())
}
}
}
var fstabEntryRemoveTests = []struct {
fstabMock []byte
in *fstab.Mount
}{
{
[]byte(fstabMock1),
&fstab.Mount{
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
File: "/",
VfsType: "ext4",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 1,
},
},
}
func (obj *MountRes) TestFstabEntryRemove(t *testing.T) {
file, err := ioutil.TempFile("", "fstab")
if err != nil {
t.Errorf("error creating temp file: %v", err)
return
}
defer os.Remove(file.Name())
for _, test := range fstabEntryRemoveTests {
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
return
}
err := obj.fstabEntryRemove(file.Name(), test.in)
if err != nil {
t.Errorf("error removing fstab entry: %s from file: %s: %v", test.in.String(), file.Name(), err)
return
}
exists, err := fstabEntryExists(file.Name(), test.in)
if err != nil {
t.Errorf("error checking if %s exists: %v", test.in.String(), err)
return
}
if exists {
t.Errorf("fstab failed to remove entry: %s from fstab", test.in.String())
}
}
}
var mountCompareTests = []struct {
dIn *fstab.Mount
pIn *fstab.Mount
out bool
}{
{
&fstab.Mount{
Spec: "/dev/foo",
File: "/mnt/foo",
VfsType: "ext3",
MntOps: map[string]string{"defaults": ""},
},
&fstab.Mount{
Spec: "/dev/foo",
File: "/mnt/foo",
VfsType: "ext3",
MntOps: map[string]string{"foo": "bar", "baz": ""},
},
true,
},
{
&fstab.Mount{
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
File: "/mnt/foo",
VfsType: "ext3",
},
&fstab.Mount{
Spec: "UUID=00112233-4455-6677-8899-aabbccddeeff",
File: "/mnt/bar",
VfsType: "ext3",
},
false,
},
}
var fstabEntryExistsTests = []struct {
fstabMock []byte
in *fstab.Mount
out bool
}{
{
[]byte(fstabMock1),
&fstab.Mount{
Spec: "UUID=ef5726f2-615c-4350-b0ab-f106e5fc90ad",
File: "/",
VfsType: "ext4",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 1,
},
true,
},
{
[]byte(fstabMock1),
&fstab.Mount{
Spec: "/dev/mapper/root",
File: "/home",
VfsType: "ext4",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 1,
},
false,
},
}
func TestFstabEntryExists(t *testing.T) {
file, err := ioutil.TempFile("", "fstab")
if err != nil {
t.Errorf("error creating temp file: %v", err)
return
}
defer os.Remove(file.Name())
for _, test := range fstabEntryExistsTests {
if err := ioutil.WriteFile(file.Name(), test.fstabMock, 0644); err != nil {
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
return
}
result, err := fstabEntryExists(file.Name(), test.in)
if err != nil {
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
return
}
if result != test.out {
t.Errorf("fstabEntryExists test wanted: %t, got: %t", test.out, result)
}
}
}
func TestMountCompare(t *testing.T) {
for _, test := range mountCompareTests {
result, err := mountCompare(test.dIn, test.pIn)
if err != nil {
t.Errorf("error comparing mounts: %s and %s: %v", test.dIn.String(), test.pIn.String(), err)
return
}
if result != test.out {
t.Errorf("mountCompare test wanted: %t, got: %t", test.out, result)
}
}
}
var mountExistsTests = []struct {
procMock []byte
in *fstab.Mount
out bool
}{
{
[]byte(procMock1),
&fstab.Mount{
Spec: "/tmp/mount0",
File: "/mnt/proctest",
VfsType: "ext4",
MntOps: map[string]string{"defaults": ""},
Freq: 1,
PassNo: 1,
},
true,
},
}
func TestMountExists(t *testing.T) {
file, err := ioutil.TempFile("", "proc")
if err != nil {
t.Errorf("error creating temp file: %v", err)
return
}
defer os.Remove(file.Name())
for _, test := range mountExistsTests {
if err := ioutil.WriteFile(file.Name(), test.procMock, 0664); err != nil {
t.Errorf("error writing proc file: %s: %v", file.Name(), err)
return
}
if err := ioutil.WriteFile(test.in.Spec, []byte{}, 0664); err != nil {
t.Errorf("error writing fstab file: %s: %v", file.Name(), err)
return
}
result, err := mountExists(file.Name(), test.in)
if err != nil {
t.Errorf("error checking if fstab entry %s exists: %v", test.in.String(), err)
return
}
if result != test.out {
t.Errorf("mountExistsTests test wanted: %t, got: %t", test.out, result)
}
}
}

View File

@@ -19,20 +19,26 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"regexp" "regexp"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/coreos/go-systemd/journal" "github.com/coreos/go-systemd/journal"
) )
func init() { func init() {
RegisterResource("msg", func() Res { return &MsgRes{} }) engine.RegisterResource("msg", func() engine.Res { return &MsgRes{} })
} }
// MsgRes is a resource that writes messages to logs. // MsgRes is a resource that writes messages to logs.
type MsgRes struct { type MsgRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Refreshable
init *engine.Init
Body string `yaml:"body"` Body string `yaml:"body"`
Priority string `yaml:"priority"` Priority string `yaml:"priority"`
Fields map[string]string `yaml:"fields"` Fields map[string]string `yaml:"fields"`
@@ -43,19 +49,9 @@ type MsgRes struct {
syslogStateOK bool syslogStateOK bool
} }
// MsgUID is a unique representation for a MsgRes object.
type MsgUID struct {
BaseUID
body string
}
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *MsgRes) Default() Res { func (obj *MsgRes) Default() engine.Res {
return &MsgRes{ return &MsgRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate the params that are passed to MsgRes. // Validate the params that are passed to MsgRes.
@@ -81,15 +77,52 @@ func (obj *MsgRes) Validate() error {
default: default:
return fmt.Errorf("invalid Priority '%s'", obj.Priority) return fmt.Errorf("invalid Priority '%s'", obj.Priority)
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *MsgRes) Init() error { func (obj *MsgRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overrriding obj.init = init // save for later
return nil
} }
// isAllStateOK derives a compound state from all internal cache flags that apply to this resource. // Close is run by the engine to clean up after the resource is done.
func (obj *MsgRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *MsgRes) Watch() error {
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // exit if requested
}
var send = false // send event?
for {
select {
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}
// isAllStateOK derives a compound state from all internal cache flags that
// apply to this resource.
func (obj *MsgRes) isAllStateOK() bool { func (obj *MsgRes) isAllStateOK() bool {
if obj.Journal && !obj.journalStateOK { if obj.Journal && !obj.journalStateOK {
return false return false
@@ -102,7 +135,10 @@ func (obj *MsgRes) isAllStateOK() bool {
// updateStateOK sets the global state so it can be read by the engine. // updateStateOK sets the global state so it can be read by the engine.
func (obj *MsgRes) updateStateOK() { func (obj *MsgRes) updateStateOK() {
obj.StateOK(obj.isAllStateOK()) // XXX: this resource doesn't entirely make sense to me at the moment.
if !obj.isAllStateOK() {
obj.init.Dirty()
}
} }
// JournalPriority converts a string description to a numeric priority. // JournalPriority converts a string description to a numeric priority.
@@ -128,42 +164,15 @@ func (obj *MsgRes) journalPriority() journal.Priority {
return journal.PriNotice return journal.PriNotice
} }
// Watch is the primary listener for this resource and it outputs events. // CheckApply method for Msg resource. Every check leads to an apply, meaning
func (obj *MsgRes) Watch() error { // that the message is flushed to the journal.
// notify engine that we're running
if err := obj.Running(); err != nil {
return err // bubble up a NACK...
}
var send = false // send event?
var exit *error
for {
select {
case event := <-obj.Events():
// we avoid sending events on unpause
if exit, send = obj.ReadEvent(event); exit != nil {
return *exit // exit
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.Event()
}
}
}
// CheckApply method for Msg resource.
// Every check leads to an apply, meaning that the message is flushed to the journal.
func (obj *MsgRes) CheckApply(apply bool) (bool, error) { func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
// isStateOK() done by engine, so we updateStateOK() to pass in value // isStateOK() done by engine, so we updateStateOK() to pass in value
//if obj.isAllStateOK() { //if obj.isAllStateOK() {
// return true, nil // return true, nil
//} //}
if obj.Refresh() { // if we were notified... if obj.init.Refresh() { // if we were notified...
// invalidate cached state... // invalidate cached state...
obj.logStateOK = false obj.logStateOK = false
if obj.Journal { if obj.Journal {
@@ -176,7 +185,7 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
} }
if !obj.logStateOK { if !obj.logStateOK {
log.Printf("%s: Body: %s", obj, obj.Body) obj.init.Logf("Body: %s", obj.Body)
obj.logStateOK = true obj.logStateOK = true
obj.updateStateOK() obj.updateStateOK()
} }
@@ -199,29 +208,21 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
return false, nil return false, nil
} }
// UIDs includes all params to make a unique identification of this object. // Cmp compares two resources and returns an error if they are not equivalent.
// Most resources only return one, although some resources can return multiple. func (obj *MsgRes) Cmp(r engine.Res) error {
func (obj *MsgRes) UIDs() []ResUID { if !obj.Compare(r) {
x := &MsgUID{ return fmt.Errorf("did not compare")
BaseUID: BaseUID{
Name: obj.GetName(),
Kind: obj.GetKind(),
},
body: obj.Body,
} }
return []ResUID{x} return nil
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *MsgRes) Compare(r Res) bool { func (obj *MsgRes) Compare(r engine.Res) bool {
// we can only compare MsgRes to others of the same resource kind // we can only compare MsgRes to others of the same resource kind
res, ok := r.(*MsgRes) res, ok := r.(*MsgRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) {
return false
}
if obj.Body != res.Body { if obj.Body != res.Body {
return false return false
@@ -241,6 +242,23 @@ func (obj *MsgRes) Compare(r Res) bool {
return true return true
} }
// MsgUID is a unique representation for a MsgRes object.
type MsgUID struct {
engine.BaseUID
body string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *MsgRes) UIDs() []engine.ResUID {
x := &MsgUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
body: obj.Body,
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources package resources
import ( import (
@@ -23,15 +25,9 @@ import (
func TestMsgValidate1(t *testing.T) { func TestMsgValidate1(t *testing.T) {
r1 := &MsgRes{ r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "Debug", Priority: "Debug",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -39,15 +35,9 @@ func TestMsgValidate1(t *testing.T) {
func TestMsgValidate2(t *testing.T) { func TestMsgValidate2(t *testing.T) {
r1 := &MsgRes{ r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "UnrealPriority", Priority: "UnrealPriority",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err == nil { if err := r1.Validate(); err == nil {
t.Errorf("validation error is nil") t.Errorf("validation error is nil")
} }

888
engine/resources/net.go Normal file
View File

@@ -0,0 +1,888 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !darwin
package resources
import (
"bytes"
"fmt"
"io/ioutil"
"net"
"os"
"path"
"strings"
"sync"
"syscall"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util"
multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
// XXX: Do NOT use subscribe methods from this lib, as they are racey and
// do not clean up spawned goroutines. Should be replaced when a suitable
// alternative is available.
"github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
)
func init() {
engine.RegisterResource("net", func() engine.Res { return &NetRes{} })
}
const (
// IfacePrefix is the prefix used to identify unit files for managed links.
IfacePrefix = "mgmt-"
// networkdUnitFileDir is the location of networkd unit files which define
// the systemd network connections.
networkdUnitFileDir = "/etc/systemd/network/"
// networkdUnitFileExt is the file extension for networkd unit files.
networkdUnitFileExt = ".network"
// networkdUnitFileUmask sets the permissions on the systemd unit file.
networkdUnitFileUmask = 0644
// ifaceUp is the up (on) interface state.
ifaceUp = "up"
// ifaceDown is the down (off) interface state.
ifaceDown = "down"
// Netlink multicast groups to watch for events. For all groups see:
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
rtmGrps = rtmGrpLink | rtmGrpIPv4IfAddr | rtmGrpIPv6IfAddr | rtmGrpIPv4IfRoute
rtmGrpLink = 0x1 // interface create/delete/up/down
rtmGrpIPv4IfAddr = 0x10 // add/delete IPv4 addresses
rtmGrpIPv6IfAddr = 0x100 // add/delete IPv6 addresses
rtmGrpIPv4IfRoute = 0x40 // add delete routes
// IP routing protocols for used for netlink route messages. For all
// protocols see:
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
rtProtoKernel = 2 // kernel
rtProtoStatic = 4 // static
socketFile = "pipe.sock" // path in vardir to store our socket file
)
// NetRes is a network interface resource based on netlink. It manages the
// state of a network link. Configuration is also stored in a networkd
// configuration file, so the network is available upon reboot.
type NetRes struct {
traits.Base // add the base methods without re-implementation
init *engine.Init
State string `yaml:"state"` // up, down, or empty
Addrs []string `yaml:"addrs"` // list of addresses in cidr format
Gateway string `yaml:"gateway"` // gateway address
iface *iface // a struct containing the net.Interface and netlink.Link
unitFilePath string // the interface unit file path
socketFile string // path for storing the pipe socket file
}
// nlChanStruct defines the channel used to send netlink messages and errors
// to the event processing loop in Watch.
type nlChanStruct struct {
msg []syscall.NetlinkMessage
err error
}
// Default returns some sensible defaults for this resource.
func (obj *NetRes) Default() engine.Res {
return &NetRes{}
}
// Validate if the params passed in are valid data.
func (obj *NetRes) Validate() error {
// validate state
if obj.State != ifaceUp && obj.State != ifaceDown && obj.State != "" {
return fmt.Errorf("state must be up, down or empty")
}
// validate network address input
if (obj.Addrs == nil) != (obj.Gateway == "") {
return fmt.Errorf("addrs and gateway must both be set or both be empty")
}
if obj.Addrs != nil {
for _, addr := range obj.Addrs {
if _, _, err := net.ParseCIDR(addr); err != nil {
return errwrap.Wrapf(err, "error parsing address: %s", addr)
}
}
}
if obj.Gateway != "" {
if g := net.ParseIP(obj.Gateway); g == nil {
return fmt.Errorf("error parsing gateway: %s", obj.Gateway)
}
}
// validate the interface name
_, err := net.InterfaceByName(obj.Name())
if err != nil {
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
}
return nil
}
// Init runs some startup code for this resource.
func (obj *NetRes) Init(init *engine.Init) error {
obj.init = init // save for later
var err error
// tmp directory for pipe socket
dir, err := obj.init.VarDir("")
if err != nil {
return errwrap.Wrapf(err, "could not get VarDir in Init()")
}
obj.socketFile = path.Join(dir, socketFile) // return a unique file
// store the network interface in the struct
obj.iface = &iface{}
if obj.iface.iface, err = net.InterfaceByName(obj.Name()); err != nil {
return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
}
// store the netlink link to use as interface input in netlink functions
if obj.iface.link, err = netlink.LinkByName(obj.Name()); err != nil {
return errwrap.Wrapf(err, "error finding link: %s", obj.Name())
}
// build the path to the networkd configuration file
obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.Name() + networkdUnitFileExt
return nil
}
// Close cleans up when we're done.
func (obj *NetRes) Close() error {
var errList error
if obj.socketFile == "/" {
return fmt.Errorf("socket file should not be the root path")
}
if obj.socketFile != "" { // safety
if err := os.Remove(obj.socketFile); err != nil {
errList = multierr.Append(errList, err)
}
}
return errList
}
// Watch listens for events from the specified interface via a netlink socket.
// TODO: currently gets events from ALL interfaces, would be nice to reject
// events from other interfaces.
func (obj *NetRes) Watch() error {
// waitgroup for netlink receive goroutine
wg := &sync.WaitGroup{}
defer wg.Wait()
// create a netlink socket for receiving network interface events
conn, err := newSocketSet(rtmGrps, obj.socketFile)
if err != nil {
return errwrap.Wrapf(err, "error creating socket set")
}
defer conn.shutdown() // close the netlink socket and unblock conn.receive()
// watch the systemd-networkd configuration file
recWatcher, err := recwatch.NewRecWatcher(obj.unitFilePath, false)
if err != nil {
return err
}
// close the recwatcher when we're done
defer recWatcher.Close()
// channel for netlink messages
nlChan := make(chan *nlChanStruct) // closed from goroutine
// channel to unblock selects in goroutine
closeChan := make(chan struct{})
defer close(closeChan)
wg.Add(1)
go func() {
defer wg.Done()
defer conn.close() // close the pipe when we're done with it
defer close(nlChan)
for {
// receive messages from the socket set
msgs, err := conn.receive()
if err != nil {
select {
case nlChan <- &nlChanStruct{
err: errwrap.Wrapf(err, "error receiving messages"),
}:
case <-closeChan:
return
}
}
select {
case nlChan <- &nlChanStruct{
msg: msgs,
}:
case <-closeChan:
return
}
}
}()
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // exit if requested
}
var send = false // send event?
var done bool
for {
select {
case s, ok := <-nlChan:
if !ok {
if done {
return nil
}
done = true
continue
}
if err := s.err; err != nil {
return errwrap.Wrapf(s.err, "unknown netlink error")
}
if obj.init.Debug {
obj.init.Logf("Event: %+v", s.msg)
}
send = true
obj.init.Dirty() // dirty
case event, ok := <-recWatcher.Events():
if !ok {
if done {
return nil
}
done = true
continue
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown recwatcher error")
}
if obj.init.Debug {
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
}
send = true
obj.init.Dirty() // dirty
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}
// ifaceCheckApply checks the state of the network device and brings it up or
// down as necessary.
func (obj *NetRes) ifaceCheckApply(apply bool) (bool, error) {
// check the interface state
state, err := obj.iface.state()
if err != nil {
return false, errwrap.Wrapf(err, "error checking %s state", obj.Name())
}
// if the state is correct or unspecified, we're done
if obj.State == state || obj.State == "" {
return true, nil
}
// end of state checking
if !apply {
return false, nil
}
obj.init.Logf("ifaceCheckApply(%t)", apply)
// ip link set up/down
if err := obj.iface.linkUpDown(obj.State); err != nil {
return false, errwrap.Wrapf(err, "error setting %s up or down", obj.Name())
}
return false, nil
}
// addrCheckApply checks if the interface has the correct addresses and then
// adds/deletes addresses as necessary.
func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
// get the link's addresses
ifaceAddrs, err := obj.iface.getAddrs()
if err != nil {
return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.Name())
}
// if state is not defined
if obj.Addrs == nil {
// send addrs
obj.Addrs = ifaceAddrs
return true, nil
}
// check if all addrs have a kernel route needed for first hop
kernelOK, err := obj.iface.kernelCheck(obj.Addrs)
if err != nil {
return false, errwrap.Wrapf(err, "error checking kernel routes")
}
// if the kernel routes are intact and the addrs match, we're done
err = util.SortedStrSliceCompare(obj.Addrs, ifaceAddrs)
if err == nil && kernelOK {
return true, nil
}
// end of state checking
if !apply {
return false, nil
}
obj.init.Logf("addrCheckApply(%t)", apply)
// check each address and delete the ones that aren't in the definition
if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil {
return false, errwrap.Wrapf(err, "error checking or deleting addresses")
}
// check each address and add the ones that are defined but do not exist
if err := obj.iface.addrApplyAdd(obj.Addrs); err != nil {
return false, errwrap.Wrapf(err, "error checking or adding addresses")
}
// make sure all the addrs have the appropriate kernel routes
if err := obj.iface.kernelApply(obj.Addrs); err != nil {
return false, errwrap.Wrapf(err, "error adding kernel routes")
}
return false, nil
}
// gatewayCheckApply checks if the interface has the correct default gateway
// and adds/deletes routes as necessary.
func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
// get all routes from the interface
routes, err := netlink.RouteList(obj.iface.link, netlink.FAMILY_V4)
if err != nil {
return false, errwrap.Wrapf(err, "error getting default routes")
}
// add default routes to a slice
defRoutes := []netlink.Route{}
for _, route := range routes {
if route.Dst == nil { // route is default
defRoutes = append(defRoutes, route)
}
}
// if the gateway is already set, we're done
if len(defRoutes) == 1 && defRoutes[0].Gw.String() == obj.Gateway {
return true, nil
}
// if no gateway was defined
if obj.Gateway == "" {
// send the gateway if there is one
if len(defRoutes) == 1 {
obj.Gateway = defRoutes[0].Gw.String()
}
return true, nil
}
// end of state checking
if !apply {
return false, nil
}
obj.init.Logf("gatewayCheckApply(%t)", apply)
// delete all but one default route
for i := 1; i < len(defRoutes); i++ {
if err := netlink.RouteDel(&defRoutes[i]); err != nil {
return false, errwrap.Wrapf(err, "error deleting route: %+v", defRoutes[i])
}
}
// add or change the default route
if err := netlink.RouteReplace(&netlink.Route{
LinkIndex: obj.iface.iface.Index,
Gw: net.ParseIP(obj.Gateway),
Protocol: rtProtoStatic,
}); err != nil {
return false, errwrap.Wrapf(err, "error replacing default route")
}
return false, nil
}
// fileCheckApply checks and maintains the systemd-networkd unit file contents.
func (obj *NetRes) fileCheckApply(apply bool) (bool, error) {
// check if the unit file exists
_, err := os.Stat(obj.unitFilePath)
if err != nil && !os.IsNotExist(err) {
return false, errwrap.Wrapf(err, "error checking file")
}
// build the unit file contents from the definition
contents := obj.unitFileContents()
// check the file contents
if err == nil {
unitFile, err := ioutil.ReadFile(obj.unitFilePath)
if err != nil {
return false, errwrap.Wrapf(err, "error reading file")
}
// return if the file is good
if bytes.Equal(unitFile, contents) {
return true, nil
}
}
if !apply {
return false, nil
}
obj.init.Logf("fileCheckApply(%t)", apply)
// write the file
if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil {
return false, errwrap.Wrapf(err, "error writing configuration file")
}
return false, nil
}
// CheckApply is run to check the state and, if apply is true, to apply the
// necessary changes to reach the desired state. This is run before Watch and
// again if Watch finds a change occurring to the state.
func (obj *NetRes) CheckApply(apply bool) (checkOK bool, err error) {
checkOK = true
// check the network device
if c, err := obj.ifaceCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
// if the interface is supposed to be down, we're done
if obj.State == ifaceDown {
return checkOK, nil
}
// check the addresses
if c, err := obj.addrCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
// check the gateway
if c, err := obj.gatewayCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
// if the state is unspecified, we're done
if obj.State == "" {
return checkOK, nil
}
// check the networkd unit file
if c, err := obj.fileCheckApply(apply); err != nil {
return false, err
} else if !c {
checkOK = false
}
return checkOK, nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NetRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *NetRes) Compare(r engine.Res) bool {
// we can only compare NetRes to others of the same resource kind
res, ok := r.(*NetRes)
if !ok {
return false
}
if obj.State != res.State {
return false
}
if (obj.Addrs == nil) != (res.Addrs == nil) {
return false
}
if err := util.SortedStrSliceCompare(obj.Addrs, res.Addrs); err != nil {
return false
}
if obj.Gateway != res.Gateway {
return false
}
return true
}
// NetUID is a unique resource identifier.
type NetUID struct {
// NOTE: There is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
engine.BaseUID
name string // the network interface name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *NetUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*NetUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *NetRes) UIDs() []engine.ResUID {
x := &NetUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NetRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*NetRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to NetRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = NetRes(raw) // restore from indirection with type conversion!
return nil
}
// unitFileContents builds the unit file contents from the definition.
func (obj *NetRes) unitFileContents() []byte {
// build the unit file contents
u := []string{"[Match]"}
u = append(u, fmt.Sprintf("Name=%s", obj.Name()))
u = append(u, "[Network]")
for _, addr := range obj.Addrs {
u = append(u, fmt.Sprintf("Address=%s", addr))
}
if obj.Gateway != "" {
u = append(u, fmt.Sprintf("Gateway=%s", obj.Gateway))
}
c := strings.Join(u, "\n")
return []byte(c)
}
// iface wraps net.Interface to add additional methods.
type iface struct {
iface *net.Interface
link netlink.Link
}
// state reports the state of the interface as up or down.
func (obj *iface) state() (string, error) {
var err error
if obj.iface, err = net.InterfaceByName(obj.iface.Name); err != nil {
return "", errwrap.Wrapf(err, "error updating interface")
}
// if the interface's "up" flag is 0, it's down
if obj.iface.Flags&net.FlagUp == 0 {
return ifaceDown, nil
}
// otherwise it's up
return ifaceUp, nil
}
// linkUpDown brings the interface up or down, depending on input value.
func (obj *iface) linkUpDown(state string) error {
if state != ifaceUp && state != ifaceDown {
return fmt.Errorf("state must be up or down")
}
if state == ifaceUp {
return netlink.LinkSetUp(obj.link)
}
return netlink.LinkSetDown(obj.link)
}
// getAddrs returns a list of strings containing all of the interface's
// IP addresses in CIDR format.
func (obj *iface) getAddrs() ([]string, error) {
var ifaceAddrs []string
a, err := obj.iface.Addrs()
if err != nil {
return nil, errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
}
// we're only interested in the strings (not the network)
for _, addr := range a {
ifaceAddrs = append(ifaceAddrs, addr.String())
}
return ifaceAddrs, nil
}
// kernelCheck checks if all addresses in the list have a corresponding kernel
// route, without which the network would be unreachable.
func (obj *iface) kernelCheck(addrs []string) (bool, error) {
var routeOK bool
// get a list of all the routes associated with the interface
routes, err := netlink.RouteList(obj.link, netlink.FAMILY_V4)
if err != nil {
return false, errwrap.Wrapf(err, "error getting routes")
}
// check each route against each addr
for _, addr := range addrs {
routeOK = false
ip, ipNet, err := net.ParseCIDR(addr)
if err != nil {
return false, errwrap.Wrapf(err, "error parsing addr: %s", addr)
}
for _, r := range routes {
// if src, dst and protocol are correct, the kernel route exists
if r.Src.Equal(ip) && r.Dst.String() == ipNet.String() && r.Protocol == rtProtoKernel {
routeOK = true
break
}
}
// if any addr is missing a kernel route return early
if !routeOK {
break
}
}
return routeOK, nil
}
// kernelApply adds or replaces each address' kernel route as necessary.
func (obj *iface) kernelApply(addrs []string) error {
// for each addr, add or replace the corresponding kernel route
for _, addr := range addrs {
ip, ipNet, err := net.ParseCIDR(addr)
if err != nil {
return errwrap.Wrapf(err, "error parsing addr: %s", addr)
}
// kernel route needed for the network to be reachable from a given ip
if err := netlink.RouteReplace(&netlink.Route{
LinkIndex: obj.iface.Index,
Dst: ipNet,
Src: ip,
Protocol: rtProtoKernel,
Scope: netlink.SCOPE_LINK,
}); err != nil {
return errwrap.Wrapf(err, "error replacing first hop route")
}
}
return nil
}
// addrApplyDelete, checks the interface's addresses and deletes any that are not
// in the list/definition.
func (obj *iface) addrApplyDelete(objAddrs []string) error {
ifaceAddrs, err := obj.getAddrs()
if err != nil {
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
}
for _, ifaceAddr := range ifaceAddrs {
addrOK := false
for _, objAddr := range objAddrs {
if ifaceAddr == objAddr {
addrOK = true
}
}
if addrOK {
continue
}
addr, err := netlink.ParseAddr(ifaceAddr)
if err != nil {
return errwrap.Wrapf(err, "error parsing netlink address: %s", ifaceAddr)
}
if err := netlink.AddrDel(obj.link, addr); err != nil {
return errwrap.Wrapf(err, "error deleting addr: %s from %s", ifaceAddr, obj.iface.Name)
}
}
return nil
}
// addrApplyAdd checks if the interface has each address in the supplied list,
// and if it doesn't, it adds them.
func (obj *iface) addrApplyAdd(objAddrs []string) error {
ifaceAddrs, err := obj.getAddrs()
if err != nil {
return errwrap.Wrapf(err, "error getting addrs from interface: %s", obj.iface.Name)
}
for _, objAddr := range objAddrs {
addrOK := false
for _, ifaceAddr := range ifaceAddrs {
if ifaceAddr == objAddr {
addrOK = true
}
}
if addrOK {
continue
}
addr, err := netlink.ParseAddr(objAddr)
if err != nil {
return errwrap.Wrapf(err, "error parsing cidr address: %s", objAddr)
}
if err := netlink.AddrAdd(obj.link, addr); err != nil {
return errwrap.Wrapf(err, "error adding addr: %s to %s", objAddr, obj.iface.Name)
}
}
return nil
}
// socketSet is used to receive events from a socket and shut it down cleanly
// when asked. It contains a socket for events and a pipe socket to unblock
// receive on shutdown.
type socketSet struct {
fdEvents int
fdPipe int
pipeFile string
}
// newSocketSet returns a socketSet, initialized with the given parameters.
func newSocketSet(groups uint32, file string) (*socketSet, error) {
// make a netlink socket file descriptor
fdEvents, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW, unix.NETLINK_ROUTE)
if err != nil {
return nil, errwrap.Wrapf(err, "error creating netlink socket")
}
// bind to the socket and add add the netlink groups we need to get events
if err := unix.Bind(fdEvents, &unix.SockaddrNetlink{
Family: unix.AF_NETLINK,
Groups: groups,
}); err != nil {
return nil, errwrap.Wrapf(err, "error binding netlink socket")
}
// create a pipe socket to unblock unix.Select when we close
fdPipe, err := unix.Socket(unix.AF_UNIX, unix.SOCK_RAW, unix.PROT_NONE)
if err != nil {
return nil, errwrap.Wrapf(err, "error creating pipe socket")
}
// bind the pipe to a file
if err = unix.Bind(fdPipe, &unix.SockaddrUnix{
Name: file,
}); err != nil {
return nil, errwrap.Wrapf(err, "error binding pipe socket")
}
return &socketSet{
fdEvents: fdEvents,
fdPipe: fdPipe,
pipeFile: file,
}, nil
}
// shutdown closes the event file descriptor and unblocks receive by sending
// a message to the pipe file descriptor. It must be called before close, and
// should only be called once.
func (obj *socketSet) shutdown() error {
// close the event socket so no more events are produced
if err := unix.Close(obj.fdEvents); err != nil {
return err
}
// send a message to the pipe to unblock select
return unix.Sendto(obj.fdPipe, nil, 0, &unix.SockaddrUnix{
Name: path.Join(obj.pipeFile),
})
}
// close closes the pipe file descriptor. It must only be called after
// shutdown has closed fdEvents, and unblocked receive. It should only be
// called once.
func (obj *socketSet) close() error {
return unix.Close(obj.fdPipe)
}
// receive waits for bytes from fdEvents and parses them into a slice of
// netlink messages. It will block until an event is produced, or shutdown
// is called.
func (obj *socketSet) receive() ([]syscall.NetlinkMessage, error) {
// Select will return when any fd in fdSet (fdEvents and fdPipe) is ready
// to read.
_, err := unix.Select(obj.nfd(), obj.fdSet(), nil, nil, nil)
if err != nil {
// if a system interrupt is caught
if err == unix.EINTR { // signal interrupt
return nil, nil
}
return nil, errwrap.Wrapf(err, "error selecting on fd")
}
// receive the message from the netlink socket into b
b := make([]byte, os.Getpagesize())
n, _, err := unix.Recvfrom(obj.fdEvents, b, unix.MSG_DONTWAIT) // non-blocking receive
if err != nil {
// if fdEvents is closed
if err == unix.EBADF { // bad file descriptor
return nil, nil
}
return nil, errwrap.Wrapf(err, "error receiving messages")
}
// if we didn't get enough bytes for a header, something went wrong
if n < unix.NLMSG_HDRLEN {
return nil, fmt.Errorf("received short header")
}
b = b[:n] // truncate b to message length
// use syscall to parse, as func does not exist in x/sys/unix
return syscall.ParseNetlinkMessage(b)
}
// nfd returns one more than the highest fd value in the struct, for use as as
// the nfds parameter in select. It represents the file descriptor set maximum
// size. See man select for more info.
func (obj *socketSet) nfd() int {
if obj.fdEvents > obj.fdPipe {
return obj.fdEvents + 1
}
return obj.fdPipe + 1
}
// fdSet returns a bitmask representation of the integer values of fdEvents
// and fdPipe. See man select for more info.
func (obj *socketSet) fdSet() *unix.FdSet {
fdSet := &unix.FdSet{}
fdSet.Bits[obj.fdEvents/64] |= 1 << uint(obj.fdEvents)
fdSet.Bits[obj.fdPipe/64] |= 1 << uint(obj.fdPipe) // fd = 3 becomes 100 if we add 5, we get 10100
return fdSet
}

View File

@@ -19,117 +19,127 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
) )
func init() { func init() {
RegisterResource("noop", func() Res { return &NoopRes{} }) engine.RegisterResource("noop", func() engine.Res { return &NoopRes{} })
} }
// NoopRes is a no-op resource that does nothing. // NoopRes is a no-op resource that does nothing.
type NoopRes struct { type NoopRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Groupable
traits.Refreshable
init *engine.Init
Comment string `lang:"comment" yaml:"comment"` // extra field for example purposes Comment string `lang:"comment" yaml:"comment"` // extra field for example purposes
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *NoopRes) Default() Res { func (obj *NoopRes) Default() engine.Res {
return &NoopRes{ return &NoopRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
func (obj *NoopRes) Validate() error { func (obj *NoopRes) Validate() error {
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *NoopRes) Init() error { func (obj *NoopRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *NoopRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
func (obj *NoopRes) Watch() error { func (obj *NoopRes) Watch() error {
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for Noop resource. Does nothing, returns happy! // CheckApply method for Noop resource. Does nothing, returns happy!
func (obj *NoopRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *NoopRes) CheckApply(apply bool) (checkOK bool, err error) {
if obj.Refresh() { if obj.init.Refresh() {
log.Printf("%s: Received a notification!", obj) obj.init.Logf("received a notification!")
} }
return true, nil // state is always okay return true, nil // state is always okay
} }
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NoopRes) Cmp(r engine.Res) error {
// we can only compare NoopRes to others of the same resource kind
res, ok := r.(*NoopRes)
if !ok {
return fmt.Errorf("not a %s", obj.Kind())
}
if obj.Comment != res.Comment {
return fmt.Errorf("the Comment differs")
}
return nil
}
// NoopUID is the UID struct for NoopRes. // NoopUID is the UID struct for NoopRes.
type NoopUID struct { type NoopUID struct {
BaseUID engine.BaseUID
name string name string
} }
// UIDs includes all params to make a unique identification of this object. // UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple. // Most resources only return one, although some resources can return multiple.
func (obj *NoopRes) UIDs() []ResUID { func (obj *NoopRes) UIDs() []engine.ResUID {
x := &NoopUID{ x := &NoopUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name, name: obj.Name(),
} }
return []ResUID{x} return []engine.ResUID{x}
} }
// GroupCmp returns whether two resources can be grouped together or not. // GroupCmp returns whether two resources can be grouped together or not.
func (obj *NoopRes) GroupCmp(r Res) bool { func (obj *NoopRes) GroupCmp(r engine.GroupableRes) error {
_, ok := r.(*NoopRes) _, ok := r.(*NoopRes)
if !ok { if !ok {
// NOTE: technically we could group a noop into any other // NOTE: technically we could group a noop into any other
// resource, if that resource knew how to handle it, although, // resource, if that resource knew how to handle it, although,
// since the mechanics of inter-kind resource grouping are // since the mechanics of inter-kind resource grouping are
// tricky, avoid doing this until there's a good reason. // tricky, avoid doing this until there's a good reason.
return false return fmt.Errorf("resource is not the same kind")
} }
return true // noop resources can always be grouped together! return nil // noop resources can always be grouped together!
}
// Compare two resources and return if they are equivalent.
func (obj *NoopRes) Compare(r Res) bool {
// we can only compare NoopRes to others of the same resource kind
res, ok := r.(*NoopRes)
if !ok {
return false
}
// calling base Compare is probably unneeded for the noop res, but do it
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
return true
} }
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.

View File

@@ -0,0 +1,104 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources
import (
"reflect"
"testing"
"github.com/purpleidea/mgmt/engine"
)
func TestCmp1(t *testing.T) {
r1, err := engine.NewResource("noop")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
r2, err := engine.NewResource("noop")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
r3, err := engine.NewResource("file")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
if err := r1.Cmp(r2); err != nil {
t.Errorf("the two resources do not match: %+v", err)
}
if err := r2.Cmp(r1); err != nil {
t.Errorf("the two resources do not match: %+v", err)
}
if r1.Cmp(r3) == nil {
t.Errorf("the two resources should not match")
}
if r3.Cmp(r1) == nil {
t.Errorf("the two resources should not match")
}
}
func TestSort0(t *testing.T) {
rs := []engine.Res{}
s := engine.Sort(rs)
if !reflect.DeepEqual(s, []engine.Res{}) {
t.Errorf("sort failed!")
if s == nil {
t.Logf("output is nil!")
} else {
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
}
}
func TestSort1(t *testing.T) {
r1, _ := engine.NewNamedResource("noop", "noop1")
r2, _ := engine.NewNamedResource("noop", "noop2")
r3, _ := engine.NewNamedResource("noop", "noop3")
r4, _ := engine.NewNamedResource("noop", "noop4")
r5, _ := engine.NewNamedResource("noop", "noop5")
r6, _ := engine.NewNamedResource("noop", "noop6")
rs := []engine.Res{r3, r2, r6, r1, r5, r4}
s := engine.Sort(rs)
if !reflect.DeepEqual(s, []engine.Res{r1, r2, r3, r4, r5, r6}) {
t.Errorf("sort failed!")
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
if !reflect.DeepEqual(rs, []engine.Res{r3, r2, r6, r1, r5, r4}) {
t.Errorf("sort modified input!")
str := "Got:"
for _, r := range rs {
str += " " + r.String()
}
t.Errorf(str)
}
}

View File

@@ -20,10 +20,12 @@ package resources
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"strconv" "strconv"
"unicode" "unicode"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
systemdDbus "github.com/coreos/go-systemd/dbus" systemdDbus "github.com/coreos/go-systemd/dbus"
@@ -36,19 +38,23 @@ import (
const ( const (
running = "running" running = "running"
stopped = "stopped" stopped = "stopped"
dbusInterface = "org.freedesktop.machine1.Manager" dbusMachine1Iface = "org.freedesktop.machine1.Manager"
machineNew = "org.freedesktop.machine1.Manager.MachineNew" machineNew = dbusMachine1Iface + ".MachineNew"
machineRemoved = "org.freedesktop.machine1.Manager.MachineRemoved" machineRemoved = dbusMachine1Iface + ".MachineRemoved"
nspawnServiceTmpl = "systemd-nspawn@%s" nspawnServiceTmpl = "systemd-nspawn@%s"
) )
func init() { func init() {
RegisterResource("nspawn", func() Res { return &NspawnRes{} }) engine.RegisterResource("nspawn", func() engine.Res { return &NspawnRes{} })
} }
// NspawnRes is an nspawn container resource. // NspawnRes is an nspawn container resource.
type NspawnRes struct { type NspawnRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
//traits.Groupable // TODO: this would be quite useful for this resource
init *engine.Init
State string `yaml:"state"` State string `yaml:"state"`
// We're using the svc resource to start and stop the machine because // We're using the svc resource to start and stop the machine because
// that's what machinectl does. We're not using svc.Watch because then we // that's what machinectl does. We're not using svc.Watch because then we
@@ -59,11 +65,8 @@ type NspawnRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *NspawnRes) Default() Res { func (obj *NspawnRes) Default() engine.Res {
return &NspawnRes{ return &NspawnRes{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
State: running, State: running,
} }
} }
@@ -71,7 +74,7 @@ func (obj *NspawnRes) Default() Res {
// makeComposite creates a pointer to a SvcRes. The pointer is used to // makeComposite creates a pointer to a SvcRes. The pointer is used to
// validate and initialize the nested svc. // validate and initialize the nested svc.
func (obj *NspawnRes) makeComposite() (*SvcRes, error) { func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
res, err := NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.GetName())) res, err := engine.NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.Name()))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -80,6 +83,274 @@ func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
return svc, nil return svc, nil
} }
// Validate if the params passed in are valid data.
func (obj *NspawnRes) Validate() error {
if len(obj.Name()) > 64 {
return fmt.Errorf("name must be 64 characters or less")
}
// check if systemd version is higher than 231 to allow non-alphanumeric
// machine names, as previous versions would error in such cases
ver, err := systemdVersion()
if err != nil {
return err
}
if ver < 231 {
for _, char := range obj.Name() {
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
}
}
}
if obj.State != running && obj.State != stopped {
return fmt.Errorf("invalid state: %s", obj.State)
}
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in validate")
}
if err := svc.Validate(); err != nil { // composite resource
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
}
return nil
}
// Init runs some startup code for this resource.
func (obj *NspawnRes) Init(init *engine.Init) error {
obj.init = init // save for later
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in init")
}
obj.svc = svc
// TODO: we could build a new init that adds a prefix to the logger...
if err := obj.svc.Init(init); err != nil {
return err
}
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *NspawnRes) Close() error {
if obj.svc != nil {
return obj.svc.Close()
}
return nil
}
// Watch for state changes and sends a message to the bus if there is a change.
func (obj *NspawnRes) Watch() error {
// this resource depends on systemd to ensure that it's running
if !systemdUtil.IsRunningSystemd() {
return fmt.Errorf("systemd is not running")
}
// create a private message bus
bus, err := util.SystemBusPrivateUsable()
if err != nil {
return errwrap.Wrapf(err, "failed to connect to bus")
}
defer bus.Close()
// add a match rule to match messages going through the message bus
args := fmt.Sprintf("type='signal',interface='%s',eavesdrop='true'", dbusMachine1Iface)
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
return err
}
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
busChan := make(chan *dbus.Signal)
defer close(busChan)
bus.Signal(busChan)
defer bus.RemoveSignal(busChan) // not needed here, but nice for symmetry
// notify engine that we're running
if err := obj.init.Running(); err != nil {
return err // exit if requested
}
var send = false // send event?
for {
select {
case event := <-busChan:
// process org.freedesktop.machine1 events for this resource's name
if event.Body[0] == obj.Name() {
obj.init.Logf("Event received: %v", event.Name)
if event.Name == machineNew {
obj.init.Logf("Machine started")
} else if event.Name == machineRemoved {
obj.init.Logf("Machine stopped")
} else {
return fmt.Errorf("unknown event: %s", event.Name)
}
send = true
obj.init.Dirty() // dirty
}
case event, ok := <-obj.init.Events:
if !ok {
return nil
}
if err := obj.init.Read(event); err != nil {
return err
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
if err := obj.init.Event(); err != nil {
return err // exit if requested
}
}
}
}
// CheckApply is run to check the state and, if apply is true, to apply the
// necessary changes to reach the desired state. This is run before Watch and
// again if Watch finds a change occurring to the state.
func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
// this resource depends on systemd to ensure that it's running
if !systemdUtil.IsRunningSystemd() {
return false, errors.New("systemd is not running")
}
// connect to org.freedesktop.machine1.Manager
conn, err := machined.New()
if err != nil {
return false, errwrap.Wrapf(err, "failed to connect to dbus")
}
// compare the current state with the desired state and perform the
// appropriate action
var exists = true
properties, err := conn.DescribeMachine(obj.Name())
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name !=
"org.freedesktop.machine1.NoSuchMachine" {
return false, err
}
exists = false
// if we could not successfully get the properties because
// there's no such machine the machine is stopped
// error if we need the image ignore if we don't
if _, err = conn.GetImage(obj.Name()); err != nil && obj.State != stopped {
return false, fmt.Errorf(
"no machine nor image named '%s'",
obj.Name())
}
}
if obj.init.Debug {
obj.init.Logf("properties: %v", properties)
}
// if the machine doesn't exist and is supposed to
// be stopped or the state matches we're done
if !exists && obj.State == stopped || properties["State"] == obj.State {
if obj.init.Debug {
obj.init.Logf("CheckApply() in valid state")
}
return true, nil
}
// end of state checking. if we're here, checkOK is false
if !apply {
return false, nil
}
obj.init.Logf("CheckApply() applying '%s' state", obj.State)
// use the embedded svc to apply the correct state
if _, err := obj.svc.CheckApply(apply); err != nil {
return false, errwrap.Wrapf(err, "nested svc failed")
}
return false, nil
}
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NspawnRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *NspawnRes) Compare(r engine.Res) bool {
// we can only compare NspawnRes to others of the same resource kind
res, ok := r.(*NspawnRes)
if !ok {
return false
}
if obj.State != res.State {
return false
}
// TODO: why is res.svc ever nil?
if (obj.svc == nil) != (res.svc == nil) { // xor
return false
}
if obj.svc != nil && res.svc != nil {
if !obj.svc.Compare(res.svc) {
return false
}
}
return true
}
// NspawnUID is a unique resource identifier.
type NspawnUID struct {
// NOTE: There is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
engine.BaseUID
name string // the machine name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *NspawnUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*NspawnUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *NspawnRes) UIDs() []engine.ResUID {
x := &NspawnUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(), // svc name
}
return append([]engine.ResUID{x}, obj.svc.UIDs()...)
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NspawnRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*NspawnRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to NspawnRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = NspawnRes(raw) // restore from indirection with type conversion!
return nil
}
// systemdVersion uses dbus to check which version of systemd is installed. // systemdVersion uses dbus to check which version of systemd is installed.
func systemdVersion() (uint16, error) { func systemdVersion() (uint16, error) {
// check if systemd is running // check if systemd is running
@@ -108,266 +379,3 @@ func systemdVersion() (uint16, error) {
} }
return uint16(ver), nil return uint16(ver), nil
} }
// Validate if the params passed in are valid data.
func (obj *NspawnRes) Validate() error {
if len(obj.GetName()) > 64 {
return fmt.Errorf("name must be 64 characters or less")
}
// check if systemd version is higher than 231 to allow non-alphanumeric
// machine names, as previous versions would error in such cases
ver, err := systemdVersion()
if err != nil {
return err
}
if ver < 231 {
for _, char := range obj.GetName() {
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
}
}
}
if obj.State != running && obj.State != stopped {
return fmt.Errorf("invalid state: %s", obj.State)
}
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in validate")
}
if err := svc.Validate(); err != nil { // composite resource
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
}
return obj.BaseRes.Validate()
}
// Init runs some startup code for this resource.
func (obj *NspawnRes) Init() error {
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in init")
}
obj.svc = svc
if err := obj.svc.Init(); err != nil {
return err
}
return obj.BaseRes.Init()
}
// Watch for state changes and sends a message to the bus if there is a change.
func (obj *NspawnRes) Watch() error {
// this resource depends on systemd to ensure that it's running
if !systemdUtil.IsRunningSystemd() {
return fmt.Errorf("systemd is not running")
}
// create a private message bus
bus, err := util.SystemBusPrivateUsable()
if err != nil {
return errwrap.Wrapf(err, "failed to connect to bus")
}
// add a match rule to match messages going through the message bus
call := bus.BusObject().Call("org.freedesktop.DBus.AddMatch", 0,
fmt.Sprintf("type='signal',interface='%s',eavesdrop='true'",
dbusInterface))
// <-call.Done
if err := call.Err; err != nil {
return err
}
// TODO: verify that implementation doesn't deadlock if there are unread
// messages left in the channel
busChan := make(chan *dbus.Signal, 10)
bus.Signal(busChan)
// notify engine that we're running
if err := obj.Running(); err != nil {
return err // bubble up a NACK...
}
var send = false
var exit *error
defer close(busChan)
defer bus.Close()
defer bus.RemoveSignal(busChan)
for {
select {
case event := <-busChan:
// process org.freedesktop.machine1 events for this resource's name
if event.Body[0] == obj.GetName() {
log.Printf("%s: Event received: %v", obj, event.Name)
if event.Name == machineNew {
log.Printf("%s: Machine started", obj)
} else if event.Name == machineRemoved {
log.Printf("%s: Machine stopped", obj)
} else {
return fmt.Errorf("unknown event: %s", event.Name)
}
send = true
obj.StateOK(false) // dirty
}
case event := <-obj.Events():
if exit, send = obj.ReadEvent(event); exit != nil {
return *exit // exit
}
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.Event()
}
}
}
// CheckApply is run to check the state and, if apply is true, to apply the
// necessary changes to reach the desired state. This is run before Watch and
// again if Watch finds a change occurring to the state.
func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
// this resource depends on systemd to ensure that it's running
if !systemdUtil.IsRunningSystemd() {
return false, errors.New("systemd is not running")
}
// connect to org.freedesktop.machine1.Manager
conn, err := machined.New()
if err != nil {
return false, errwrap.Wrapf(err, "failed to connect to dbus")
}
// compare the current state with the desired state and perform the
// appropriate action
var exists = true
properties, err := conn.DescribeMachine(obj.GetName())
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name !=
"org.freedesktop.machine1.NoSuchMachine" {
return false, err
}
exists = false
// if we could not successfully get the properties because
// there's no such machine the machine is stopped
// error if we need the image ignore if we don't
if _, err = conn.GetImage(obj.GetName()); err != nil && obj.State != stopped {
return false, fmt.Errorf(
"no machine nor image named '%s'",
obj.GetName())
}
}
if obj.debug {
log.Printf("%s: properties: %v", obj, properties)
}
// if the machine doesn't exist and is supposed to
// be stopped or the state matches we're done
if !exists && obj.State == stopped || properties["State"] == obj.State {
if obj.debug {
log.Printf("%s: CheckApply() in valid state", obj)
}
return true, nil
}
// end of state checking. if we're here, checkOK is false
if !apply {
return false, nil
}
log.Printf("%s: CheckApply() applying '%s' state", obj, obj.State)
// use the embedded svc to apply the correct state
if _, err := obj.svc.CheckApply(apply); err != nil {
return false, errwrap.Wrapf(err, "nested svc failed")
}
return false, nil
}
// NspawnUID is a unique resource identifier.
type NspawnUID struct {
// NOTE: There is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
BaseUID
name string // the machine name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *NspawnUID) IFF(uid ResUID) bool {
res, ok := uid.(*NspawnUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *NspawnRes) UIDs() []ResUID {
x := &NspawnUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name, // svc name
}
return append([]ResUID{x}, obj.svc.UIDs()...)
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *NspawnRes) GroupCmp(r Res) bool {
_, ok := r.(*NspawnRes)
if !ok {
return false
}
// TODO: this would be quite useful for this resource!
return false
}
// Compare two resources and return if they are equivalent.
func (obj *NspawnRes) Compare(r Res) bool {
// we can only compare NspawnRes to others of the same resource kind
res, ok := r.(*NspawnRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) {
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
// TODO: why is res.svc ever nil?
if (obj.svc == nil) != (res.svc == nil) { // xor
return false
}
if obj.svc != nil && res.svc != nil {
if !obj.svc.Compare(res.svc) {
return false
}
}
return true
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *NspawnRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes NspawnRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*NspawnRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to NspawnRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = NspawnRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -22,18 +22,19 @@ package packagekit
import ( import (
"fmt" "fmt"
"log"
"runtime" "runtime"
"strings" "strings"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
"github.com/godbus/dbus" "github.com/godbus/dbus"
multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors"
) )
// global tweaks of verbosity and code path // global tweaks of verbosity and code path
const ( const (
Debug = false
Paranoid = false // enable if you see any ghosts Paranoid = false // enable if you see any ghosts
) )
@@ -47,7 +48,6 @@ const (
PkPath = "/org/freedesktop/PackageKit" PkPath = "/org/freedesktop/PackageKit"
PkIface = "org.freedesktop.PackageKit" PkIface = "org.freedesktop.PackageKit"
PkIfaceTransaction = PkIface + ".Transaction" PkIfaceTransaction = PkIface + ".Transaction"
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
) )
var ( var (
@@ -57,6 +57,7 @@ var (
// TODO: add more values // TODO: add more values
// noarch // noarch
"noarch": "ANY", // special value "ANY" (noarch as seen in Fedora) "noarch": "ANY", // special value "ANY" (noarch as seen in Fedora)
"any": "ANY", // special value "ANY" ('any' as seen in ArchLinux)
"all": "ANY", // special value "ANY" ('all' as seen in Debian) "all": "ANY", // special value "ANY" ('all' as seen in Debian)
// fedora // fedora
"x86_64": "amd64", "x86_64": "amd64",
@@ -149,6 +150,9 @@ const ( //typedef enum
// Conn is a wrapper struct so we can pass bus connection around in the struct. // Conn is a wrapper struct so we can pass bus connection around in the struct.
type Conn struct { type Conn struct {
conn *dbus.Conn conn *dbus.Conn
Debug bool
Logf func(format string, v ...interface{})
} }
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values. // PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
@@ -173,57 +177,74 @@ func NewBus() *Conn {
} }
// GetBus gets the dbus connection object. // GetBus gets the dbus connection object.
func (bus *Conn) GetBus() *dbus.Conn { func (obj *Conn) GetBus() *dbus.Conn {
return bus.conn return obj.conn
} }
// Close closes the dbus connection object. // Close closes the dbus connection object.
func (bus *Conn) Close() error { func (obj *Conn) Close() error {
return bus.conn.Close() return obj.conn.Close()
} }
// internal helper to add signal matches to the bus, should only be called once // internal helper to add signal matches to the bus, should only be called once
func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error { func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
if Debug { if obj.Debug {
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals) obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
} }
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal> // eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
var call *dbus.Call bus := obj.GetBus().BusObject()
var argsList []string
// cleanup function should be called when done or when AddMatch errors
removeSignals := func() error {
var errList error
for i := len(argsList) - 1; i >= 0; i-- { // last in first out
if call := bus.Call(engineUtil.DBusRemoveMatch, 0, argsList[i]); call.Err != nil {
errList = multierr.Append(errList, call.Err)
}
}
return errList
}
// TODO: if we make this call many times, we seem to receive signals // TODO: if we make this call many times, we seem to receive signals
// that many times... Maybe this should be an object singleton? // that many times... Maybe this should be an object singleton?
obj := bus.GetBus().BusObject() var call *dbus.Call
pathStr := fmt.Sprintf("%s", path) pathStr := fmt.Sprintf("%s", path)
if len(signals) == 0 { if len(signals) == 0 {
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"'") args := fmt.Sprintf("type='signal', path='%s', interface='%s'", pathStr, iface)
argsList = append(argsList, args)
call = bus.Call(engineUtil.DBusAddMatch, 0, args)
} else { } else {
for _, signal := range signals { for _, signal := range signals {
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'") args := fmt.Sprintf("type='signal', path='%s', interface='%s', member'%s'", pathStr, iface, signal)
if call.Err != nil { argsList = append(argsList, args)
break if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
break // fail if any one fails
} }
} }
} }
if call.Err != nil { if call.Err != nil {
return call.Err defer removeSignals() // ignore the error
return nil, call.Err
} }
// The caller has to make sure that ch is sufficiently buffered; if a // The caller has to make sure that ch is sufficiently buffered; if a
// message arrives when a write to c is not possible, it is discarded! // message arrives when a write to c is not possible, it is discarded!
// This can be disastrous if we're waiting for a "Finished" signal! // This can be disastrous if we're waiting for a "Finished" signal!
bus.GetBus().Signal(ch) obj.GetBus().Signal(ch)
return nil return removeSignals, nil
} }
// WatchChanges gets a signal anytime an event happens. // WatchChanges gets a signal anytime an event happens.
func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) { func (obj *Conn) WatchChanges() (chan *dbus.Signal, error) {
ch := make(chan *dbus.Signal, PkBufferSize) ch := make(chan *dbus.Signal, PkBufferSize)
// NOTE: the TransactionListChanged signal fires much more frequently, // NOTE: the TransactionListChanged signal fires much more frequently,
// but with much less specificity. If we're missing events, report the // but with much less specificity. If we're missing events, report the
// issue upstream! The UpdatesChanged signal is what hughsie suggested // issue upstream! The UpdatesChanged signal is what hughsie suggested
var signal = "UpdatesChanged" var signal = "UpdatesChanged"
err := bus.matchSignal(ch, PkPath, PkIface, []string{signal}) removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal})
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer removeSignals() // ignore the error
if Paranoid { // TODO: this filtering might not be necessary anymore... if Paranoid { // TODO: this filtering might not be necessary anymore...
// try to handle the filtering inside this function! // try to handle the filtering inside this function!
rch := make(chan *dbus.Signal) rch := make(chan *dbus.Signal)
@@ -236,13 +257,13 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
// zero value immediately": if i get nil here, // zero value immediately": if i get nil here,
// it means the channel was closed by someone!! // it means the channel was closed by someone!!
if event == nil { // shared bus issue? if event == nil { // shared bus issue?
log.Println("PackageKit: Hrm, channel was closed!") obj.Logf("Hrm, channel was closed!")
break loop // TODO: continue? break loop // TODO: continue?
} }
// i think this was caused by using the shared // i think this was caused by using the shared
// bus, but we might as well leave it in for now // bus, but we might as well leave it in for now
if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) { if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) {
log.Printf("PackageKit: Woops: Event: %+v", event) obj.Logf("Woops: Event: %+v", event)
continue continue
} }
rch <- event // forward... rch <- event // forward...
@@ -256,41 +277,45 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
} }
// CreateTransaction creates and returns a transaction path. // CreateTransaction creates and returns a transaction path.
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) { func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) {
if Debug { if obj.Debug {
log.Println("PackageKit: CreateTransaction()") obj.Logf("CreateTransaction()")
} }
var interfacePath dbus.ObjectPath var interfacePath dbus.ObjectPath
obj := bus.GetBus().Object(PkIface, PkPath) bus := obj.GetBus().Object(PkIface, PkPath)
call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath) call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
if call != nil { if call != nil {
return "", call return "", call
} }
if Debug { if obj.Debug {
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath) obj.Logf("CreateTransaction(): %v", interfacePath)
} }
return interfacePath, nil return interfacePath, nil
} }
// ResolvePackages runs the PackageKit Resolve method and returns the result. // ResolvePackages runs the PackageKit Resolve method and returns the result.
func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) { func (obj *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
packageIDs := []string{} packageIDs := []string{}
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() // emits Destroy on close interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
// add signal matches for Package and Finished which will always be last // add signal matches for Package and Finished which will always be last
var signals = []string{"Package", "Finished", "Error", "Destroy"} var signals = []string{"Package", "Finished", "Error", "Destroy"}
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if Debug { if err != nil {
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath) return nil, err
} }
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path defer removeSignals()
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages) if obj.Debug {
if Debug { obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath)
log.Println("PackageKit: ResolvePackages(): Call: Success!") }
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
if obj.Debug {
obj.Logf("ResolvePackages(): Call: Success!")
} }
if call.Err != nil { if call.Err != nil {
return []string{}, call.Err return []string{}, call.Err
@@ -300,11 +325,11 @@ loop:
// FIXME: add a timeout option to error in case signals are dropped! // FIXME: add a timeout option to error in case signals are dropped!
select { select {
case signal := <-ch: case signal := <-ch:
if Debug { if obj.Debug {
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal) obj.Logf("ResolvePackages(): Signal: %+v", signal)
} }
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -337,10 +362,10 @@ loop:
} }
// IsInstalledList queries a list of packages to see if they are installed. // IsInstalledList queries a list of packages to see if they are installed.
func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) { func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
var filter uint64 // initializes at the "zero" value of 0 var filter uint64 // initializes at the "zero" value of 0
filter += PkFilterEnumArch // always search in our arch filter += PkFilterEnumArch // always search in our arch
packageIDs, e := bus.ResolvePackages(packages, filter) packageIDs, e := obj.ResolvePackages(packages, filter)
if e != nil { if e != nil {
return nil, fmt.Errorf("ResolvePackages error: %v", e) return nil, fmt.Errorf("ResolvePackages error: %v", e)
} }
@@ -375,8 +400,8 @@ func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
// IsInstalled returns if a package is installed. // IsInstalled returns if a package is installed.
// TODO: this could be optimized by making the resolve call directly // TODO: this could be optimized by making the resolve call directly
func (bus *Conn) IsInstalled(pkg string) (bool, error) { func (obj *Conn) IsInstalled(pkg string) (bool, error) {
p, e := bus.IsInstalledList([]string{pkg}) p, e := obj.IsInstalledList([]string{pkg})
if len(p) != 1 { if len(p) != 1 {
return false, e return false, e
} }
@@ -384,23 +409,27 @@ func (bus *Conn) IsInstalled(pkg string) (bool, error) {
} }
// InstallPackages installs a list of packages by packageID. // InstallPackages installs a list of packages by packageID.
func (bus *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error { func (obj *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() // emits Destroy on close interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil { if err != nil {
return err return err
} }
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("RefreshCache"), 0, false) call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
call = obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs) call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
@@ -411,7 +440,7 @@ loop:
select { select {
case signal := <-ch: case signal := <-ch:
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -431,30 +460,34 @@ loop:
} }
case <-util.TimeAfterOrBlock(timeout): case <-util.TimeAfterOrBlock(timeout):
if finished { if finished {
log.Println("PackageKit: Timeout: InstallPackages: Waiting for 'Destroy'") obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
return nil // got tired of waiting for Destroy return nil // got tired of waiting for Destroy
} }
return fmt.Errorf("PackageKit: Timeout: InstallPackages: %v", strings.Join(packageIDs, ", ")) return fmt.Errorf("PackageKit: Timeout: InstallPackages: %s", strings.Join(packageIDs, ", "))
} }
} }
} }
// RemovePackages removes a list of packages by packageID. // RemovePackages removes a list of packages by packageID.
func (bus *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error { func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
var allowDeps = true // TODO: configurable var allowDeps = true // TODO: configurable
var autoremove = false // unsupported on GNU/Linux var autoremove = false // unsupported on GNU/Linux
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() // emits Destroy on close interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil { if err != nil {
return err return err
} }
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove) call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
@@ -464,7 +497,7 @@ loop:
select { select {
case signal := <-ch: case signal := <-ch:
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -488,18 +521,22 @@ loop:
} }
// UpdatePackages updates a list of packages to versions that are specified. // UpdatePackages updates a list of packages to versions that are specified.
func (bus *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error { func (obj *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() interfacePath, err := obj.CreateTransaction()
if err != nil { if err != nil {
return err return err
} }
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs) call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
if call.Err != nil { if call.Err != nil {
return call.Err return call.Err
} }
@@ -509,7 +546,7 @@ loop:
select { select {
case signal := <-ch: case signal := <-ch:
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -531,20 +568,24 @@ loop:
} }
// GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs. // GetFilesByPackageID gets the list of files that are contained inside a list of packageIDs.
func (bus *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) { func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23 // NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a // https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() interfacePath, err := obj.CreateTransaction()
if err != nil { if err != nil {
return return
} }
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ? var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return
}
defer removeSignals()
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs) call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
if call.Err != nil { if call.Err != nil {
err = call.Err err = call.Err
return return
@@ -557,7 +598,7 @@ loop:
case signal := <-ch: case signal := <-ch:
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -596,22 +637,26 @@ loop:
} }
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter. // GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
func (bus *Conn) GetUpdates(filter uint64) ([]string, error) { func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
if Debug { if obj.Debug {
log.Println("PackageKit: GetUpdates()") obj.Logf("GetUpdates()")
} }
packageIDs := []string{} packageIDs := []string{}
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :( ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() interfacePath, err := obj.CreateTransaction()
if err != nil { if err != nil {
return nil, err return nil, err
} }
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ? var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals) removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return nil, err
}
defer removeSignals()
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("GetUpdates"), 0, filter) call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
if call.Err != nil { if call.Err != nil {
return nil, call.Err return nil, call.Err
} }
@@ -621,7 +666,7 @@ loop:
select { select {
case signal := <-ch: case signal := <-ch:
if signal.Path != interfacePath { if signal.Path != interfacePath {
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path) obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop continue loop
} }
@@ -660,7 +705,7 @@ loop:
// outside mgmt. The packageMap input has the package names as keys and // outside mgmt. The packageMap input has the package names as keys and
// requested states as values. These states can be: installed, uninstalled, // requested states as values. These states can be: installed, uninstalled,
// newest or a requested version str. // newest or a requested version str.
func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) { func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
count := 0 count := 0
packages := make([]string, len(packageMap)) packages := make([]string, len(packageMap))
for k := range packageMap { // lol, golang has no hash.keys() function! for k := range packageMap { // lol, golang has no hash.keys() function!
@@ -672,10 +717,10 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
filter += PkFilterEnumArch // always search in our arch filter += PkFilterEnumArch // always search in our arch
} }
if Debug { if obj.Debug {
log.Printf("PackageKit: PackagesToPackageIDs(): %v", strings.Join(packages, ", ")) obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
} }
resolved, e := bus.ResolvePackages(packages, filter) resolved, e := obj.ResolvePackages(packages, filter)
if e != nil { if e != nil {
return nil, fmt.Errorf("Resolve error: %v", e) return nil, fmt.Errorf("Resolve error: %v", e)
} }
@@ -692,13 +737,16 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
for _, packageID := range resolved { for _, packageID := range resolved {
index = -1 index = -1
//log.Printf("* %v", packageID) //obj.Logf("* %v", packageID)
// format is: name;version;arch;data // format is: name;version;arch;data
s := strings.Split(packageID, ";") s := strings.Split(packageID, ";")
//if len(s) != 4 { continue } // this would be a bug! //if len(s) != 4 { continue } // this would be a bug!
pkg, ver, arch, data := s[0], s[1], s[2], s[3] pkg, ver, arch, data := s[0], s[1], s[2], s[3]
// we might need to allow some of this, eg: i386 .deb on amd64 // we might need to allow some of this, eg: i386 .deb on amd64
if !IsMyArch(arch) { b, err := IsMyArch(arch)
if err != nil {
return nil, errwrap.Wrapf(err, "arch error")
} else if !b {
continue continue
} }
@@ -748,12 +796,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
// to be done, and if so, anything that needs updating isn't newest! // to be done, and if so, anything that needs updating isn't newest!
// if something isn't installed, we can't verify it with this method // if something isn't installed, we can't verify it with this method
// FIXME: https://github.com/hughsie/PackageKit/issues/116 // FIXME: https://github.com/hughsie/PackageKit/issues/116
updates, e := bus.GetUpdates(filter) updates, e := obj.GetUpdates(filter)
if e != nil { if e != nil {
return nil, fmt.Errorf("Updates error: %v", e) return nil, fmt.Errorf("Updates error: %v", e)
} }
for _, packageID := range updates { for _, packageID := range updates {
//log.Printf("* %v", packageID) //obj.Logf("* %v", packageID)
// format is: name;version;arch;data // format is: name;version;arch;data
s := strings.Split(packageID, ";") s := strings.Split(packageID, ";")
//if len(s) != 4 { continue } // this would be a bug! //if len(s) != 4 { continue } // this would be a bug!
@@ -792,13 +840,13 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
} }
// we _could_ do a second resolve and then parse like this... // we _could_ do a second resolve and then parse like this...
//resolved, e := bus.ResolvePackages(..., filter+PkFilterEnumNewest) //resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest)
// but that's basically what recursion here could do too! // but that's basically what recursion here could do too!
if len(checkPackages) > 0 { if len(checkPackages) > 0 {
if Debug { if obj.Debug {
log.Printf("PackageKit: PackagesToPackageIDs(): Recurse: %v", strings.Join(checkPackages, ", ")) obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
} }
recursion, e = bus.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest) recursion, e = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
if e != nil { if e != nil {
return nil, fmt.Errorf("Recursion error: %v", e) return nil, fmt.Errorf("Recursion error: %v", e)
} }
@@ -834,12 +882,12 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) { func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
result := []string{} result := []string{}
for _, k := range packages { for _, k := range packages {
obj, ok := m[k] // lookup single package p, ok := m[k] // lookup single package
// package doesn't exist, this is an error! // package doesn't exist, this is an error!
if !ok || !obj.Found || obj.PackageID == "" { if !ok || !p.Found || p.PackageID == "" {
return nil, fmt.Errorf("can't find package named '%s'", k) return nil, fmt.Errorf("can't find package named '%s'", k)
} }
result = append(result, obj.PackageID) result = append(result, p.PackageID)
} }
return result, nil return result, nil
} }
@@ -849,18 +897,18 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
result = make(map[string]bool) result = make(map[string]bool)
pkgs := []string{} // bad pkgs that don't have a bool state pkgs := []string{} // bad pkgs that don't have a bool state
for _, k := range packages { for _, k := range packages {
obj, ok := m[k] // lookup single package p, ok := m[k] // lookup single package
// package doesn't exist, this is an error! // package doesn't exist, this is an error!
if !ok || !obj.Found { if !ok || !p.Found {
return nil, fmt.Errorf("can't find package named '%s'", k) return nil, fmt.Errorf("can't find package named '%s'", k)
} }
var b bool var b bool
if state == "installed" { if state == "installed" {
b = obj.Installed b = p.Installed
} else if state == "uninstalled" { } else if state == "uninstalled" {
b = !obj.Installed b = !p.Installed
} else if state == "newest" { } else if state == "newest" {
b = obj.Newest b = p.Newest
} else { } else {
// we can't filter "version" state in this function // we can't filter "version" state in this function
pkgs = append(pkgs, k) pkgs = append(pkgs, k)
@@ -869,7 +917,7 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
result[k] = b // save result[k] = b // save
} }
if len(pkgs) > 0 { if len(pkgs) > 0 {
err = fmt.Errorf("can't filter non-boolean state on: %v", strings.Join(pkgs, ",")) err = fmt.Errorf("can't filter non-boolean state on: %s", strings.Join(pkgs, ","))
} }
return result, err return result, err
} }
@@ -878,19 +926,19 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) { func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
result = []string{} result = []string{}
for _, k := range packages { for _, k := range packages {
obj, ok := m[k] // lookup single package p, ok := m[k] // lookup single package
// package doesn't exist, this is an error! // package doesn't exist, this is an error!
if !ok || !obj.Found { if !ok || !p.Found {
return nil, fmt.Errorf("can't find package named '%s'", k) return nil, fmt.Errorf("can't find package named '%s'", k)
} }
b := false b := false
if state == "installed" && obj.Installed { if state == "installed" && p.Installed {
b = true b = true
} else if state == "uninstalled" && !obj.Installed { } else if state == "uninstalled" && !p.Installed {
b = true b = true
} else if state == "newest" && obj.Newest { } else if state == "newest" && p.Newest {
b = true b = true
} else if state == obj.Version { } else if state == p.Version {
b = true b = true
} }
if b { if b {
@@ -917,14 +965,14 @@ func FmtTransactionMethod(method string) string {
} }
// IsMyArch determines if a PackageKit architecture matches the current os arch. // IsMyArch determines if a PackageKit architecture matches the current os arch.
func IsMyArch(arch string) bool { func IsMyArch(arch string) (bool, error) {
goarch, ok := PkArchMap[arch] goarch, ok := PkArchMap[arch]
if !ok { if !ok {
// if you get this error, please update the PkArchMap const // if you get this error, please update the PkArchMap const
log.Fatalf("PackageKit: Arch '%v', not found!", arch) return false, fmt.Errorf("arch '%s', not found", arch)
} }
if goarch == "ANY" { // special value that corresponds to noarch if goarch == "ANY" { // special value that corresponds to noarch
return true return true, nil
} }
return goarch == runtime.GOARCH return goarch == runtime.GOARCH, nil
} }

View File

@@ -21,19 +21,20 @@ import (
"crypto/rand" "crypto/rand"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"math/big" "math/big"
"os" "os"
"path" "path"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("password", func() Res { return &PasswordRes{} }) engine.RegisterResource("password", func() engine.Res { return &PasswordRes{} })
} }
const ( const (
@@ -43,43 +44,54 @@ const (
// PasswordRes is a no-op resource that returns a random password string. // PasswordRes is a no-op resource that returns a random password string.
type PasswordRes struct { type PasswordRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
// TODO: it could be useful to group our tokens into a single write, and
// as a result, we save inotify watches too!
//traits.Groupable // TODO: this is doable, but probably not very useful
traits.Refreshable
traits.Sendable
init *engine.Init
// FIXME: is uint16 too big? // FIXME: is uint16 too big?
Length uint16 `yaml:"length"` // number of characters to return Length uint16 `yaml:"length"` // number of characters to return
Saved bool // this caches the password in the clear locally Saved bool // this caches the password in the clear locally
CheckRecovery bool // recovery from integrity checks by re-generating CheckRecovery bool // recovery from integrity checks by re-generating
Password *string // the generated password, read only, do not set!
path string // the path to local storage path string // the path to local storage
recWatcher *recwatch.RecWatcher recWatcher *recwatch.RecWatcher
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *PasswordRes) Default() Res { func (obj *PasswordRes) Default() engine.Res {
return &PasswordRes{ return &PasswordRes{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
Length: 64, // safe default Length: 64, // safe default
} }
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
func (obj *PasswordRes) Validate() error { func (obj *PasswordRes) Validate() error {
return obj.BaseRes.Validate() return nil
} }
// Init generates a new password for this resource if one was not provided. It // Init runs some startup code for this resource. It generates a new password
// will save this into a local file. It will load it back in from previous runs. // for this resource if one was not provided. It will save this into a local
func (obj *PasswordRes) Init() error { // file. It will load it back in from previous runs.
func (obj *PasswordRes) Init(init *engine.Init) error {
obj.init = init // save for later
dir, err := obj.VarDir("") dir, err := obj.init.VarDir("")
if err != nil { if err != nil {
return errwrap.Wrapf(err, "could not get VarDir in Init()") return errwrap.Wrapf(err, "could not get VarDir in Init()")
} }
obj.path = path.Join(dir, "password") // return a unique file obj.path = path.Join(dir, "password") // return a unique file
return obj.BaseRes.Init() // call base init, b/c we're overriding return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *PasswordRes) Close() error {
return nil
} }
func (obj *PasswordRes) read() (string, error) { func (obj *PasswordRes) read() (string, error) {
@@ -171,12 +183,11 @@ func (obj *PasswordRes) Watch() error {
defer obj.recWatcher.Close() defer obj.recWatcher.Close()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
// NOTE: this part is very similar to the file resource code // NOTE: this part is very similar to the file resource code
@@ -188,27 +199,30 @@ func (obj *PasswordRes) Watch() error {
return errwrap.Wrapf(err, "unknown %s watcher error", obj) return errwrap.Wrapf(err, "unknown %s watcher error", obj)
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for Password resource. Does nothing, returns happy! // CheckApply method for Password resource. Does nothing, returns happy!
func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
var refresh = obj.Refresh() // do we have a pending reload to apply?
var exists = true // does the file (aka the token) exist? var exists = true // does the file (aka the token) exist?
var generate bool // do we need to generate a new password? var generate bool // do we need to generate a new password?
var write bool // do we need to write out to disk? var write bool // do we need to write out to disk?
@@ -226,7 +240,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
if !obj.CheckRecovery { if !obj.CheckRecovery {
return false, errwrap.Wrapf(err, "check failed") return false, errwrap.Wrapf(err, "check failed")
} }
log.Printf("%s: Integrity check failed", obj) obj.init.Logf("integrity check failed")
generate = true // okay to build a new one generate = true // okay to build a new one
write = true // make sure to write over the old one write = true // make sure to write over the old one
} }
@@ -240,9 +254,9 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
// stored password isn't consistent with memory // stored password isn't consistent with memory
if p := obj.Password; obj.Saved && (p != nil && *p != password) { //if p := obj.Password; obj.Saved && (p != nil && *p != password) {
write = true // write = true
} //}
if !refresh && exists && !generate && !write { // nothing to do, done! if !refresh && exists && !generate && !write { // nothing to do, done!
return true, nil return true, nil
@@ -260,13 +274,18 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
// generate the actual password // generate the actual password
var err error var err error
log.Printf("%s: Generating new password...", obj) obj.init.Logf("generating new password...")
if password, err = obj.generate(); err != nil { // generate one! if password, err = obj.generate(); err != nil { // generate one!
return false, errwrap.Wrapf(err, "could not generate password") return false, errwrap.Wrapf(err, "could not generate password")
} }
} }
obj.Password = &password // save in memory // send
if err := obj.init.Send(&PasswordSends{
Password: &password,
}); err != nil {
return false, err
}
var output string // the string to write out var output string // the string to write out
@@ -277,7 +296,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
output = password output = password
} }
// write either an empty token, or the password // write either an empty token, or the password
log.Printf("%s: Writing password token...", obj) obj.init.Logf("writing password token...")
if _, err := obj.write(output); err != nil { if _, err := obj.write(output); err != nil {
return false, errwrap.Wrapf(err, "can't write to file") return false, errwrap.Wrapf(err, "can't write to file")
} }
@@ -286,46 +305,21 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil return false, nil
} }
// PasswordUID is the UID struct for PasswordRes. // Cmp compares two resources and returns an error if they are not equivalent.
type PasswordUID struct { func (obj *PasswordRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *PasswordRes) UIDs() []ResUID {
x := &PasswordUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *PasswordRes) GroupCmp(r Res) bool {
_, ok := r.(*PasswordRes)
if !ok {
return false
}
return false // TODO: this is doable, but probably not very useful
// TODO: it could be useful to group our tokens into a single write, and
// as a result, we save inotify watches too!
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *PasswordRes) Compare(r Res) bool { func (obj *PasswordRes) Compare(r engine.Res) bool {
// we can only compare PasswordRes to others of the same resource kind // we can only compare PasswordRes to others of the same resource kind
res, ok := r.(*PasswordRes) res, ok := r.(*PasswordRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.Length != res.Length { if obj.Length != res.Length {
return false return false
@@ -342,6 +336,37 @@ func (obj *PasswordRes) Compare(r Res) bool {
return true return true
} }
// PasswordUID is the UID struct for PasswordRes.
type PasswordUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *PasswordRes) UIDs() []engine.ResUID {
x := &PasswordUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// PasswordSends is the struct of data which is sent after a successful Apply.
type PasswordSends struct {
// Password is the generated password being sent.
Password *string
// Hashing is the algorithm used for this password. Empty is plain text.
Hashing string // TODO: implement me
}
// Sends represents the default struct of values we can send using Send/Recv.
func (obj *PasswordRes) Sends() interface{} {
return &PasswordSends{
Password: nil,
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -19,23 +19,29 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"path" "path"
"strings" "strings"
"github.com/purpleidea/mgmt/resources/packagekit" "github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources/packagekit"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("pkg", func() Res { return &PkgRes{} }) engine.RegisterResource("pkg", func() engine.Res { return &PkgRes{} })
} }
// PkgRes is a package resource for packagekit. // PkgRes is a package resource for packagekit.
type PkgRes struct { type PkgRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Edgeable
traits.Groupable
init *engine.Init
State string `yaml:"state"` // state: installed, uninstalled, newest, <version> State string `yaml:"state"` // state: installed, uninstalled, newest, <version>
AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed? AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed?
AllowNonFree bool `yaml:"allownonfree"` // allow nonfree packages to be found? AllowNonFree bool `yaml:"allownonfree"` // allow nonfree packages to be found?
@@ -45,11 +51,8 @@ type PkgRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *PkgRes) Default() Res { func (obj *PkgRes) Default() engine.Res {
return &PkgRes{ return &PkgRes{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
State: "installed", // i think this is preferable to "latest" State: "installed", // i think this is preferable to "latest"
} }
} }
@@ -60,14 +63,12 @@ func (obj *PkgRes) Validate() error {
return fmt.Errorf("state cannot be empty") return fmt.Errorf("state cannot be empty")
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *PkgRes) Init() error { func (obj *PkgRes) Init(init *engine.Init) error {
if err := obj.BaseRes.Init(); err != nil { // call base init, b/c we're overriding obj.init = init // save for later
return err
}
if obj.fileList == nil { if obj.fileList == nil {
if err := obj.populateFileList(); err != nil { if err := obj.populateFileList(); err != nil {
@@ -78,6 +79,11 @@ func (obj *PkgRes) Init() error {
return nil return nil
} }
// Close is run by the engine to clean up after the resource is done.
func (obj *PkgRes) Close() error {
return nil
}
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
// It uses the PackageKit UpdatesChanged signal to watch for changes. // It uses the PackageKit UpdatesChanged signal to watch for changes.
// TODO: https://github.com/hughsie/PackageKit/issues/109 // TODO: https://github.com/hughsie/PackageKit/issues/109
@@ -88,6 +94,10 @@ func (obj *PkgRes) Watch() error {
return fmt.Errorf("can't connect to PackageKit bus") return fmt.Errorf("can't connect to PackageKit bus")
} }
defer bus.Close() defer bus.Close()
bus.Debug = obj.init.Debug
bus.Logf = func(format string, v ...interface{}) {
obj.init.Logf("packagekit: "+format, v...)
}
ch, err := bus.WatchChanges() ch, err := bus.WatchChanges()
if err != nil { if err != nil {
@@ -95,23 +105,21 @@ func (obj *PkgRes) Watch() error {
} }
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
if obj.debug { if obj.init.Debug {
log.Printf("%s: Watching...", obj.fmtNames(obj.getNames())) obj.init.Logf("%s: Watching...", obj.fmtNames(obj.getNames()))
} }
select { select {
case event := <-ch: case event := <-ch:
// FIXME: ask packagekit for info on what packages changed // FIXME: ask packagekit for info on what packages changed
if obj.debug { if obj.init.Debug {
log.Printf("%s: Event: %v", obj.fmtNames(obj.getNames()), event.Name) obj.init.Logf("Event(%s): %s", event.Name, obj.fmtNames(obj.getNames()))
} }
// since the chan is buffered, remove any supplemental // since the chan is buffered, remove any supplemental
@@ -121,20 +129,20 @@ func (obj *PkgRes) Watch() error {
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if err := obj.init.Read(event); err != nil {
return *exit // exit return err
} }
//obj.StateOK(false) // these events don't invalidate state
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -142,22 +150,22 @@ func (obj *PkgRes) Watch() error {
// get list of names when grouped or not // get list of names when grouped or not
func (obj *PkgRes) getNames() []string { func (obj *PkgRes) getNames() []string {
if g := obj.GetGroup(); len(g) > 0 { // grouped elements if g := obj.GetGroup(); len(g) > 0 { // grouped elements
names := []string{obj.GetName()} names := []string{obj.Name()}
for _, x := range g { for _, x := range g {
pkg, ok := x.(*PkgRes) // convert from Res pkg, ok := x.(*PkgRes) // convert from Res
if ok { if ok {
names = append(names, pkg.Name) names = append(names, pkg.Name())
} }
} }
return names return names
} }
return []string{obj.GetName()} return []string{obj.Name()}
} }
// pretty print for header values // pretty print for header values
func (obj *PkgRes) fmtNames(names []string) string { func (obj *PkgRes) fmtNames(names []string) string {
if len(obj.GetGroup()) > 0 { // grouped elements if len(obj.GetGroup()) > 0 { // grouped elements
return fmt.Sprintf("%s[autogroup:(%s)]", obj.GetKind(), strings.Join(names, ",")) return fmt.Sprintf("%s[autogroup:(%s)]", obj.Kind(), strings.Join(names, ","))
} }
return obj.String() return obj.String()
} }
@@ -168,9 +176,9 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
for _, x := range g { for _, x := range g {
pkg, ok := x.(*PkgRes) // convert from Res pkg, ok := x.(*PkgRes) // convert from Res
if !ok { if !ok {
log.Fatalf("grouped member %v is not a %s", x, obj.GetKind()) panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
} }
result[pkg.Name] = pkg.State result[pkg.Name()] = pkg.State
} }
} }
return result return result
@@ -178,7 +186,7 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) { func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) {
packageMap := obj.groupMappingHelper() // get the grouped values packageMap := obj.groupMappingHelper() // get the grouped values
packageMap[obj.Name] = obj.State // key is pkg name, value is pkg state packageMap[obj.Name()] = obj.State // key is pkg name, value is pkg state
var filter uint64 // initializes at the "zero" value of 0 var filter uint64 // initializes at the "zero" value of 0
filter += packagekit.PkFilterEnumArch // always search in our arch (optional!) filter += packagekit.PkFilterEnumArch // always search in our arch (optional!)
// we're requesting latest version, or to narrow down install choices! // we're requesting latest version, or to narrow down install choices!
@@ -210,16 +218,22 @@ func (obj *PkgRes) populateFileList() error {
return fmt.Errorf("can't connect to PackageKit bus") return fmt.Errorf("can't connect to PackageKit bus")
} }
defer bus.Close() defer bus.Close()
if obj.init != nil {
bus.Debug = obj.init.Debug
bus.Logf = func(format string, v ...interface{}) {
obj.init.Logf("packagekit: "+format, v...)
}
}
result, err := obj.pkgMappingHelper(bus) result, err := obj.pkgMappingHelper(bus)
if err != nil { if err != nil {
return errwrap.Wrapf(err, "the pkgMappingHelper failed") return errwrap.Wrapf(err, "the pkgMappingHelper failed")
} }
data, ok := result[obj.Name] // lookup single package (init does just one) data, ok := result[obj.Name()] // lookup single package (init does just one)
// package doesn't exist, this is an error! // package doesn't exist, this is an error!
if !ok || !data.Found { if !ok || !data.Found {
return fmt.Errorf("can't find package named '%s'", obj.Name) return fmt.Errorf("can't find package named '%s'", obj.Name())
} }
packageIDs := []string{data.PackageID} // just one for now packageIDs := []string{data.PackageID} // just one for now
@@ -237,13 +251,17 @@ func (obj *PkgRes) populateFileList() error {
// CheckApply checks the resource state and applies the resource if the bool // CheckApply checks the resource state and applies the resource if the bool
// input is true. It returns error info and if the state check passed or not. // input is true. It returns error info and if the state check passed or not.
func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: Check", obj.fmtNames(obj.getNames())) obj.init.Logf("Check: %s", obj.fmtNames(obj.getNames()))
bus := packagekit.NewBus() bus := packagekit.NewBus()
if bus == nil { if bus == nil {
return false, fmt.Errorf("can't connect to PackageKit bus") return false, fmt.Errorf("can't connect to PackageKit bus")
} }
defer bus.Close() defer bus.Close()
bus.Debug = obj.init.Debug
bus.Logf = func(format string, v ...interface{}) {
obj.init.Logf("packagekit: "+format, v...)
}
result, err := obj.pkgMappingHelper(bus) result, err := obj.pkgMappingHelper(bus)
if err != nil { if err != nil {
@@ -251,7 +269,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
packageMap := obj.groupMappingHelper() // map[string]string packageMap := obj.groupMappingHelper() // map[string]string
packageList := []string{obj.Name} packageList := []string{obj.Name()}
packageList = append(packageList, util.StrMapKeys(packageMap)...) packageList = append(packageList, util.StrMapKeys(packageMap)...)
//stateList := []string{obj.State} //stateList := []string{obj.State}
//stateList = append(stateList, util.StrMapValues(packageMap)...) //stateList = append(stateList, util.StrMapValues(packageMap)...)
@@ -262,7 +280,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
if err != nil { if err != nil {
return false, errwrap.Wrapf(err, "the FilterState method failed") return false, errwrap.Wrapf(err, "the FilterState method failed")
} }
data, _ := result[obj.Name] // if above didn't error, we won't either! data, _ := result[obj.Name()] // if above didn't error, we won't either!
validState := util.BoolMapTrue(util.BoolMapValues(states)) validState := util.BoolMapTrue(util.BoolMapValues(states))
// obj.State == "installed" || "uninstalled" || "newest" || "4.2-1.fc23" // obj.State == "installed" || "uninstalled" || "newest" || "4.2-1.fc23"
@@ -287,7 +305,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
// apply portion // apply portion
log.Printf("%s: Apply", obj.fmtNames(obj.getNames())) obj.init.Logf("Apply: %s", obj.fmtNames(obj.getNames()))
readyPackages, err := packagekit.FilterPackageState(result, packageList, obj.State) readyPackages, err := packagekit.FilterPackageState(result, packageList, obj.State)
if err != nil { if err != nil {
return false, err // fail return false, err // fail
@@ -301,7 +319,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted
} }
// apply correct state! // apply correct state!
log.Printf("%s: Set: %v...", obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())), obj.State) obj.init.Logf("Set(%s): %s...", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
switch obj.State { switch obj.State {
case "uninstalled": // run remove case "uninstalled": // run remove
// NOTE: packageID is different than when installed, because now // NOTE: packageID is different than when installed, because now
@@ -319,25 +337,61 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
if err != nil { if err != nil {
return false, err // fail return false, err // fail
} }
log.Printf("%s: Set: %v success!", obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())), obj.State) obj.init.Logf("Set(%s) success: %s", obj.State, obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())))
return false, nil // success return false, nil // success
} }
// Cmp compares two resources and returns an error if they are not equivalent.
func (obj *PkgRes) Cmp(r engine.Res) error {
if !obj.Compare(r) {
return fmt.Errorf("did not compare")
}
return nil
}
// Compare two resources and return if they are equivalent.
func (obj *PkgRes) Compare(r engine.Res) bool {
// we can only compare PkgRes to others of the same resource kind
res, ok := r.(*PkgRes)
if !ok {
return false
}
// if obj.Name != res.Name {
// return false
// }
if obj.State != res.State {
return false
}
if obj.AllowUntrusted != res.AllowUntrusted {
return false
}
if obj.AllowNonFree != res.AllowNonFree {
return false
}
if obj.AllowUnsupported != res.AllowUnsupported {
return false
}
return true
}
// PkgUID is the main UID struct for PkgRes. // PkgUID is the main UID struct for PkgRes.
type PkgUID struct { type PkgUID struct {
BaseUID engine.BaseUID
name string // pkg name name string // pkg name
state string // pkg state or "version" state string // pkg state or "version"
} }
// PkgFileUID is the UID struct for PkgRes files. // PkgFileUID is the UID struct for PkgRes files.
type PkgFileUID struct { type PkgFileUID struct {
BaseUID engine.BaseUID
path string // path of the file path string // path of the file
} }
// IFF aka if and only if they are equivalent, return true. If not, false. // IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *PkgUID) IFF(uid ResUID) bool { func (obj *PkgUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*PkgUID) res, ok := uid.(*PkgUID)
if !ok { if !ok {
return false return false
@@ -349,16 +403,16 @@ func (obj *PkgUID) IFF(uid ResUID) bool {
// PkgResAutoEdges holds the state of the auto edge generator. // PkgResAutoEdges holds the state of the auto edge generator.
type PkgResAutoEdges struct { type PkgResAutoEdges struct {
fileList []string fileList []string
svcUIDs []ResUID svcUIDs []engine.ResUID
testIsNext bool // safety testIsNext bool // safety
name string // saved data from PkgRes obj name string // saved data from PkgRes obj
kind string kind string
} }
// Next returns the next automatic edge. // Next returns the next automatic edge.
func (obj *PkgResAutoEdges) Next() []ResUID { func (obj *PkgResAutoEdges) Next() []engine.ResUID {
if obj.testIsNext { if obj.testIsNext {
log.Fatal("expecting a call to Test()") panic("expecting a call to Test()")
} }
obj.testIsNext = true // set after all the errors paths are past obj.testIsNext = true // set after all the errors paths are past
@@ -367,12 +421,12 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
return x return x
} }
var result []ResUID var result []engine.ResUID
// return UID's for whatever is in obj.fileList // return UID's for whatever is in obj.fileList
for _, x := range obj.fileList { for _, x := range obj.fileList {
var reversed = false // cheat by passing a pointer var reversed = false // cheat by passing a pointer
result = append(result, &FileUID{ result = append(result, &FileUID{
BaseUID: BaseUID{ BaseUID: engine.BaseUID{
Name: obj.name, Name: obj.name,
Kind: obj.kind, Kind: obj.kind,
Reversed: &reversed, Reversed: &reversed,
@@ -386,22 +440,22 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
// Test gets results of the earlier Next() call, & returns if we should continue! // Test gets results of the earlier Next() call, & returns if we should continue!
func (obj *PkgResAutoEdges) Test(input []bool) bool { func (obj *PkgResAutoEdges) Test(input []bool) bool {
if !obj.testIsNext { if !obj.testIsNext {
log.Fatal("expecting a call to Next()") panic("expecting a call to Next()")
} }
// ack the svcUID's... // ack the svcUID's...
if x := obj.svcUIDs; len(x) > 0 { if x := obj.svcUIDs; len(x) > 0 {
if y := len(x); y != len(input) { if y := len(x); y != len(input) {
log.Fatalf("expecting %d value(s)", y) panic(fmt.Sprintf("expecting %d value(s)", y))
} }
obj.svcUIDs = []ResUID{} // empty obj.svcUIDs = []engine.ResUID{} // empty
obj.testIsNext = false obj.testIsNext = false
return true return true
} }
count := len(obj.fileList) count := len(obj.fileList)
if count != len(input) { if count != len(input) {
log.Fatalf("expecting %d value(s)", count) panic(fmt.Sprintf("expecting %d value(s)", count))
} }
obj.testIsNext = false // set after all the errors paths are past obj.testIsNext = false // set after all the errors paths are past
@@ -436,7 +490,7 @@ func (obj *PkgResAutoEdges) Test(input []bool) bool {
// AutoEdges produces an object which generates a minimal pkg file optimization // AutoEdges produces an object which generates a minimal pkg file optimization
// sequence of edges. // sequence of edges.
func (obj *PkgRes) AutoEdges() (AutoEdge, error) { func (obj *PkgRes) AutoEdges() (engine.AutoEdge, error) {
// in contrast with the FileRes AutoEdges() function which contains // in contrast with the FileRes AutoEdges() function which contains
// more of the mechanics, most of the AutoEdge mechanics for the PkgRes // more of the mechanics, most of the AutoEdge mechanics for the PkgRes
// are contained in the Test() method! This design is completely okay! // are contained in the Test() method! This design is completely okay!
@@ -448,13 +502,13 @@ func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
} }
// add matches for any svc resources found in pkg definition! // add matches for any svc resources found in pkg definition!
var svcUIDs []ResUID var svcUIDs []engine.ResUID
for _, x := range ReturnSvcInFileList(obj.fileList) { for _, x := range ReturnSvcInFileList(obj.fileList) {
var reversed = false var reversed = false
svcUIDs = append(svcUIDs, &SvcUID{ svcUIDs = append(svcUIDs, &SvcUID{
BaseUID: BaseUID{ BaseUID: engine.BaseUID{
Name: obj.GetName(), Name: obj.Name(),
Kind: obj.GetKind(), Kind: obj.Kind(),
Reversed: &reversed, Reversed: &reversed,
}, },
name: x, // the svc name itself in the SvcUID object! name: x, // the svc name itself in the SvcUID object!
@@ -465,24 +519,24 @@ func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
fileList: util.RemoveCommonFilePrefixes(obj.fileList), // clean start! fileList: util.RemoveCommonFilePrefixes(obj.fileList), // clean start!
svcUIDs: svcUIDs, svcUIDs: svcUIDs,
testIsNext: false, // start with Next() call testIsNext: false, // start with Next() call
name: obj.GetName(), // save data for PkgResAutoEdges obj name: obj.Name(), // save data for PkgResAutoEdges obj
kind: obj.GetKind(), kind: obj.Kind(),
}, nil }, nil
} }
// UIDs includes all params to make a unique identification of this object. // UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple. // Most resources only return one, although some resources can return multiple.
func (obj *PkgRes) UIDs() []ResUID { func (obj *PkgRes) UIDs() []engine.ResUID {
x := &PkgUID{ x := &PkgUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name, name: obj.Name(),
state: obj.State, state: obj.State,
} }
result := []ResUID{x} result := []engine.ResUID{x}
for _, y := range obj.fileList { for _, y := range obj.fileList {
y := &PkgFileUID{ y := &PkgFileUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()}, BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
path: y, path: y,
} }
result = append(result, y) result = append(result, y)
@@ -491,55 +545,24 @@ func (obj *PkgRes) UIDs() []ResUID {
} }
// GroupCmp returns whether two resources can be grouped together or not. // GroupCmp returns whether two resources can be grouped together or not.
// can these two resources be merged ? // Can these two resources be merged, aka, does this resource support doing so?
// (aka does this resource support doing so?) // Will resource allow itself to be grouped _into_ this obj?
// will resource allow itself to be grouped _into_ this obj? func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
func (obj *PkgRes) GroupCmp(r Res) bool {
res, ok := r.(*PkgRes) res, ok := r.(*PkgRes)
if !ok { if !ok {
return false return fmt.Errorf("resource is not the same kind")
} }
objStateIsVersion := (obj.State != "installed" && obj.State != "uninstalled" && obj.State != "newest") // must be a ver. string objStateIsVersion := (obj.State != "installed" && obj.State != "uninstalled" && obj.State != "newest") // must be a ver. string
resStateIsVersion := (res.State != "installed" && res.State != "uninstalled" && res.State != "newest") // must be a ver. string resStateIsVersion := (res.State != "installed" && res.State != "uninstalled" && res.State != "newest") // must be a ver. string
if objStateIsVersion || resStateIsVersion { if objStateIsVersion || resStateIsVersion {
// can't merge specific version checks atm // can't merge specific version checks atm
return false return fmt.Errorf("resource uses a version string")
} }
// FIXME: keep it simple for now, only merge same states // FIXME: keep it simple for now, only merge same states
if obj.State != res.State { if obj.State != res.State {
return false return fmt.Errorf("resource is of a different state")
} }
return true return nil
}
// Compare two resources and return if they are equivalent.
func (obj *PkgRes) Compare(r Res) bool {
// we can only compare PkgRes to others of the same resource kind
res, ok := r.(*PkgRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if obj.AllowUntrusted != res.AllowUntrusted {
return false
}
if obj.AllowNonFree != res.AllowNonFree {
return false
}
if obj.AllowUnsupported != res.AllowUnsupported {
return false
}
return true
} }
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources package resources
import ( import (

View File

@@ -19,130 +19,119 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
) )
func init() { func init() {
RegisterResource("print", func() Res { return &PrintRes{} }) engine.RegisterResource("print", func() engine.Res { return &PrintRes{} })
} }
// PrintRes is a resource that is useful for printing a message to the screen. // PrintRes is a resource that is useful for printing a message to the screen.
// It will also display a message when it receives a notification. It supports // It will also display a message when it receives a notification. It supports
// automatic grouping. // automatic grouping.
type PrintRes struct { type PrintRes struct {
BaseRes `lang:"" yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Groupable
traits.Recvable
traits.Refreshable
init *engine.Init
Msg string `lang:"msg" yaml:"msg"` // the message to display Msg string `lang:"msg" yaml:"msg"` // the message to display
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *PrintRes) Default() Res { func (obj *PrintRes) Default() engine.Res {
return &PrintRes{ return &PrintRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
func (obj *PrintRes) Validate() error { func (obj *PrintRes) Validate() error {
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *PrintRes) Init() error { func (obj *PrintRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *PrintRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
func (obj *PrintRes) Watch() error { func (obj *PrintRes) Watch() error {
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for Print resource. Does nothing, returns happy! // CheckApply method for Print resource. Does nothing, returns happy!
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply: %t", obj, apply) obj.init.Logf("CheckApply: %t", apply)
if val, exists := obj.Recv["Msg"]; exists && val.Changed { if val, exists := obj.init.Recv()["Msg"]; exists && val.Changed {
// if we received on Msg, and it changed, log message // if we received on Msg, and it changed, log message
log.Printf("CheckApply: Received `Msg` of: %s", obj.Msg) obj.init.Logf("CheckApply: Received `Msg` of: %s", obj.Msg)
} }
if obj.Refresh() { if obj.init.Refresh() {
log.Printf("%s: Received a notification!", obj) obj.init.Logf("Received a notification!")
} }
log.Printf("%s: Msg: %s", obj, obj.Msg) obj.init.Logf("Msg: %s", obj.Msg)
if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements if g := obj.GetGroup(); len(g) > 0 { // add any grouped elements
for _, x := range g { for _, x := range g {
print, ok := x.(*PrintRes) // convert from Res print, ok := x.(*PrintRes) // convert from Res
if !ok { if !ok {
log.Fatalf("grouped member %v is not a %s", x, obj.GetKind()) panic(fmt.Sprintf("grouped member %v is not a %s", x, obj.Kind()))
} }
log.Printf("%s: Msg: %s", print, print.Msg) obj.init.Logf("%s: Msg: %s", print, print.Msg)
} }
} }
return true, nil // state is always okay return true, nil // state is always okay
} }
// PrintUID is the UID struct for PrintRes. // Cmp compares two resources and returns an error if they are not equivalent.
type PrintUID struct { func (obj *PrintRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *PrintRes) UIDs() []ResUID {
x := &PrintUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *PrintRes) GroupCmp(r Res) bool {
_, ok := r.(*PrintRes)
if !ok {
return false
}
return true // grouped together if we were asked to
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *PrintRes) Compare(r Res) bool { func (obj *PrintRes) Compare(r engine.Res) bool {
// we can only compare PrintRes to others of the same resource kind // we can only compare PrintRes to others of the same resource kind
res, ok := r.(*PrintRes) res, ok := r.(*PrintRes)
if !ok { if !ok {
return false return false
} }
// calling base Compare is probably unneeded for the print res, but do it
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.Msg != res.Msg { if obj.Msg != res.Msg {
return false return false
@@ -150,6 +139,31 @@ func (obj *PrintRes) Compare(r Res) bool {
return true return true
} }
// PrintUID is the UID struct for PrintRes.
type PrintUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *PrintRes) UIDs() []engine.ResUID {
x := &PrintUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *PrintRes) GroupCmp(r engine.GroupableRes) error {
_, ok := r.(*PrintRes)
if !ok {
return fmt.Errorf("resource is not the same kind")
}
return nil // grouped together if we were asked to
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -21,8 +21,9 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
systemd "github.com/coreos/go-systemd/dbus" // change namespace systemd "github.com/coreos/go-systemd/dbus" // change namespace
@@ -32,24 +33,26 @@ import (
) )
func init() { func init() {
RegisterResource("svc", func() Res { return &SvcRes{} }) engine.RegisterResource("svc", func() engine.Res { return &SvcRes{} })
} }
// SvcRes is a service resource for systemd units. // SvcRes is a service resource for systemd units.
type SvcRes struct { type SvcRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Edgeable
traits.Groupable
traits.Refreshable
init *engine.Init
State string `yaml:"state"` // state: running, stopped, undefined State string `yaml:"state"` // state: running, stopped, undefined
Startup string `yaml:"startup"` // enabled, disabled, undefined Startup string `yaml:"startup"` // enabled, disabled, undefined
Session bool `yaml:"session"` // user session (true) or system? Session bool `yaml:"session"` // user session (true) or system?
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *SvcRes) Default() Res { func (obj *SvcRes) Default() engine.Res {
return &SvcRes{ return &SvcRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate checks if the resource data structure was populated correctly. // Validate checks if the resource data structure was populated correctly.
@@ -60,12 +63,19 @@ func (obj *SvcRes) Validate() error {
if obj.Startup != "enabled" && obj.Startup != "disabled" && obj.Startup != "" { if obj.Startup != "enabled" && obj.Startup != "disabled" && obj.Startup != "" {
return fmt.Errorf("startup must be either `enabled` or `disabled` or undefined") return fmt.Errorf("startup must be either `enabled` or `disabled` or undefined")
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *SvcRes) Init() error { func (obj *SvcRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *SvcRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -101,13 +111,12 @@ func (obj *SvcRes) Watch() error {
bus.Signal(buschan) bus.Signal(buschan)
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var svc = fmt.Sprintf("%s.service", obj.Name) // systemd name var svc = fmt.Sprintf("%s.service", obj.Name()) // systemd name
var send = false // send event? var send = false // send event?
var exit *error
var invalid = false // does the svc exist or not? var invalid = false // does the svc 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
@@ -124,25 +133,25 @@ func (obj *SvcRes) Watch() error {
// firstly, does svc even exist or not? // firstly, does svc even exist or not?
loadstate, err := conn.GetUnitProperty(svc, "LoadState") loadstate, err := conn.GetUnitProperty(svc, "LoadState")
if err != nil { if err != nil {
log.Printf("Failed to get property: %v", err) obj.init.Logf("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 svc: %s", svc) obj.init.Logf("failed to find svc")
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
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
} }
if invalid { if invalid {
log.Printf("Waiting for: %s", svc) // waiting for svc to appear... obj.init.Logf("waiting fo service") // waiting for svc to appear...
if activeSet { if activeSet {
activeSet = false activeSet = false
set.Remove(svc) // no return value should ever occur set.Remove(svc) // no return value should ever occur
@@ -151,11 +160,11 @@ func (obj *SvcRes) Watch() error {
select { select {
case <-buschan: // XXX: wait for new units event to unstick case <-buschan: // XXX: wait for new units event to unstick
// loop so that we can see the changed invalid signal // loop so that we can see the changed invalid signal
log.Printf("Svc[%s]->DaemonReload()", svc) obj.init.Logf("daemon reload")
case event := <-obj.Events(): case event := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if err := obj.init.Read(event); err != nil {
return *exit // exit return err
} }
} }
} else { } else {
@@ -164,47 +173,53 @@ func (obj *SvcRes) Watch() error {
set.Add(svc) // no return value should ever occur set.Add(svc) // no return value should ever occur
} }
log.Printf("Watching: %s", svc) // attempting to watch... obj.init.Logf("watching...") // attempting to watch...
select { select {
case event := <-subChannel: case event := <-subChannel:
log.Printf("Svc event: %+v", event) obj.init.Logf("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[svc] != nil { if event[svc] != nil {
// event[svc].ActiveState is not nil // event[svc].ActiveState is not nil
switch event[svc].ActiveState { switch event[svc].ActiveState {
case "active": case "active":
log.Printf("Svc[%s]->Started", svc) obj.init.Logf("started")
case "inactive": case "inactive":
log.Printf("Svc[%s]->Stopped", svc) obj.init.Logf("stopped")
case "reloading": case "reloading":
log.Printf("Svc[%s]->Reloading", svc) obj.init.Logf("reloading")
case "failed": case "failed":
log.Printf("Svc[%s]->Failed", svc) obj.init.Logf("failed")
case "activating":
obj.init.Logf("activating")
case "deactivating":
obj.init.Logf("deactivating")
default: default:
log.Fatalf("Unknown svc state: %s", event[svc].ActiveState) return fmt.Errorf("unknown svc state: %s", event[svc].ActiveState)
} }
} else { } else {
// svc stopped (and ActiveState is nil...) // svc stopped (and ActiveState is nil...)
log.Printf("Svc[%s]->Stopped", svc) obj.init.Logf("stopped")
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case err := <-subErrors: case err := <-subErrors:
return errwrap.Wrapf(err, "unknown %s error", obj) return errwrap.Wrapf(err, "unknown %s error", obj)
case event := <-obj.Events(): case event := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if err := obj.init.Read(event); err != nil {
return *exit // exit return err
} }
} }
} }
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -228,7 +243,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
defer conn.Close() defer conn.Close()
var svc = fmt.Sprintf("%s.service", obj.Name) // systemd name var svc = fmt.Sprintf("%s.service", obj.Name()) // systemd name
loadstate, err := conn.GetUnitProperty(svc, "LoadState") loadstate, err := conn.GetUnitProperty(svc, "LoadState")
if err != nil { if err != nil {
@@ -252,7 +267,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
var running = (activestate.Value == dbus.MakeVariant("active")) var running = (activestate.Value == dbus.MakeVariant("active"))
var stateOK = ((obj.State == "") || (obj.State == "running" && running) || (obj.State == "stopped" && !running)) var stateOK = ((obj.State == "") || (obj.State == "running" && running) || (obj.State == "stopped" && !running))
var startupOK = true // XXX: DETECT AND SET var startupOK = true // XXX: DETECT AND SET
var refresh = obj.Refresh() // do we have a pending reload to apply? var refresh = obj.init.Refresh() // do we have a pending reload to apply?
if stateOK && startupOK && !refresh { if stateOK && startupOK && !refresh {
return true, nil // we are in the correct state return true, nil // we are in the correct state
@@ -264,7 +279,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
// apply portion // apply portion
log.Printf("%s: Apply", obj) obj.init.Logf("Apply")
var files = []string{svc} // the svc represented in a list var files = []string{svc} // the svc represented in a list
if obj.Startup == "enabled" { if obj.Startup == "enabled" {
_, _, err = conn.EnableUnitFiles(files, false, true) _, _, err = conn.EnableUnitFiles(files, false, true)
@@ -286,7 +301,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, errwrap.Wrapf(err, "failed to start unit") return false, errwrap.Wrapf(err, "failed to start unit")
} }
if refresh { if refresh {
log.Printf("%s: Skipping reload, due to pending start", obj) obj.init.Logf("Skipping reload, due to pending start")
} }
refresh = false // we did a start, so a reload is not needed refresh = false // we did a start, so a reload is not needed
} else if obj.State == "stopped" { } else if obj.State == "stopped" {
@@ -295,7 +310,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, errwrap.Wrapf(err, "failed to stop unit") return false, errwrap.Wrapf(err, "failed to stop unit")
} }
if refresh { if refresh {
log.Printf("%s: Skipping reload, due to pending stop", obj) obj.init.Logf("Skipping reload, due to pending stop")
} }
refresh = false // we did a stop, so a reload is not needed refresh = false // we did a stop, so a reload is not needed
} }
@@ -310,7 +325,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
if refresh { // we need to reload the service if refresh { // we need to reload the service
// XXX: run a svc reload here! // XXX: run a svc reload here!
log.Printf("%s: Reloading...", obj) obj.init.Logf("Reloading...")
} }
// XXX: also set enabled on boot // XXX: also set enabled on boot
@@ -318,124 +333,21 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil // success return false, nil // success
} }
// SvcUID is the UID struct for SvcRes. // Cmp compares two resources and returns an error if they are not equivalent.
type SvcUID struct { func (obj *SvcRes) Cmp(r engine.Res) error {
// NOTE: there is also a name variable in the BaseUID struct, this is if !obj.Compare(r) {
// information about where this UID came from, and is unrelated to the return fmt.Errorf("did not compare")
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
BaseUID
name string // the svc name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *SvcUID) IFF(uid ResUID) bool {
res, ok := uid.(*SvcUID)
if !ok {
return false
} }
return obj.name == res.name
}
// SvcResAutoEdges holds the state of the auto edge generator.
type SvcResAutoEdges struct {
data []ResUID
pointer int
found bool
}
// Next returns the next automatic edge.
func (obj *SvcResAutoEdges) Next() []ResUID {
if obj.found {
log.Fatal("shouldn't be called anymore!")
}
if len(obj.data) == 0 { // check length for rare scenarios
return nil return nil
}
value := obj.data[obj.pointer]
obj.pointer++
return []ResUID{value} // we return one, even though api supports N
}
// Test gets results of the earlier Next() call, & returns if we should continue!
func (obj *SvcResAutoEdges) Test(input []bool) bool {
// if there aren't any more remaining
if len(obj.data) <= obj.pointer {
return false
}
if obj.found { // already found, done!
return false
}
if len(input) != 1 { // in case we get given bad data
log.Fatal("expecting a single value")
}
if input[0] { // if a match is found, we're done!
obj.found = true // no more to find!
return false
}
return true // keep going
}
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *SvcRes) AutoEdges() (AutoEdge, error) {
var data []ResUID
svcFiles := []string{
fmt.Sprintf("/etc/systemd/system/%s.service", obj.Name), // takes precedence
fmt.Sprintf("/usr/lib/systemd/system/%s.service", obj.Name), // pkg default
}
for _, x := range svcFiles {
var reversed = true
data = append(data, &FileUID{
BaseUID: BaseUID{
Name: obj.GetName(),
Kind: obj.GetKind(),
Reversed: &reversed,
},
path: x, // what matters
})
}
return &FileResAutoEdges{
data: data,
pointer: 0,
found: false,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *SvcRes) UIDs() []ResUID {
x := &SvcUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name, // svc name
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *SvcRes) GroupCmp(r Res) bool {
_, ok := r.(*SvcRes)
if !ok {
return false
}
// TODO: depending on if the systemd service api allows batching, we
// might be able to build this, although not sure how useful it is...
// it might just eliminate parallelism be bunching up the graph
return false // not possible atm
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *SvcRes) Compare(r Res) bool { func (obj *SvcRes) Compare(r engine.Res) bool {
// we can only compare SvcRes to others of the same resource kind // we can only compare SvcRes to others of the same resource kind
res, ok := r.(*SvcRes) res, ok := r.(*SvcRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State { if obj.State != res.State {
return false return false
@@ -450,6 +362,111 @@ func (obj *SvcRes) Compare(r Res) bool {
return true return true
} }
// SvcUID is the UID struct for SvcRes.
type SvcUID struct {
// NOTE: there is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
engine.BaseUID
name string // the svc name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *SvcUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*SvcUID)
if !ok {
return false
}
return obj.name == res.name
}
// SvcResAutoEdges holds the state of the auto edge generator.
type SvcResAutoEdges struct {
data []engine.ResUID
pointer int
found bool
}
// Next returns the next automatic edge.
func (obj *SvcResAutoEdges) Next() []engine.ResUID {
if obj.found {
panic("shouldn't be called anymore!")
}
if len(obj.data) == 0 { // check length for rare scenarios
return nil
}
value := obj.data[obj.pointer]
obj.pointer++
return []engine.ResUID{value} // we return one, even though api supports N
}
// Test gets results of the earlier Next() call, & returns if we should continue!
func (obj *SvcResAutoEdges) Test(input []bool) bool {
// if there aren't any more remaining
if len(obj.data) <= obj.pointer {
return false
}
if obj.found { // already found, done!
return false
}
if len(input) != 1 { // in case we get given bad data
panic("expecting a single value")
}
if input[0] { // if a match is found, we're done!
obj.found = true // no more to find!
return false
}
return true // keep going
}
// AutoEdges returns the AutoEdge interface. In this case the systemd units.
func (obj *SvcRes) AutoEdges() (engine.AutoEdge, error) {
var data []engine.ResUID
svcFiles := []string{
fmt.Sprintf("/etc/systemd/system/%s.service", obj.Name()), // takes precedence
fmt.Sprintf("/usr/lib/systemd/system/%s.service", obj.Name()), // pkg default
}
for _, x := range svcFiles {
var reversed = true
data = append(data, &FileUID{
BaseUID: engine.BaseUID{
Name: obj.Name(),
Kind: obj.Kind(),
Reversed: &reversed,
},
path: x, // what matters
})
}
return &FileResAutoEdges{
data: data,
pointer: 0,
found: false,
}, nil
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *SvcRes) UIDs() []engine.ResUID {
x := &SvcUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(), // svc name
}
return []engine.ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
//func (obj *SvcRes) GroupCmp(r engine.GroupableRes) error {
// _, ok := r.(*SvcRes)
// if !ok {
// return fmt.Errorf("resource is not the same kind")
// }
// // TODO: depending on if the systemd service api allows batching, we
// // might be able to build this, although not sure how useful it is...
// // it might just eliminate parallelism by bunching up the graph
// return fmt.Errorf("not possible at the moment")
//}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *SvcRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *SvcRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -19,17 +19,25 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"reflect" "reflect"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
) )
func init() { func init() {
RegisterResource("test", func() Res { return &TestRes{} }) engine.RegisterResource("test", func() engine.Res { return &TestRes{} })
} }
// TestRes is a resource that is mostly harmless and is used for internal tests. // TestRes is a resource that is mostly harmless and is used for internal tests.
type TestRes struct { type TestRes struct {
BaseRes `lang:"" yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Groupable
traits.Refreshable
traits.Sendable
traits.Recvable
init *engine.Init
Bool bool `lang:"bool" yaml:"bool"` Bool bool `lang:"bool" yaml:"bool"`
Str string `lang:"str" yaml:"str"` // can't name it String because of String() Str string `lang:"str" yaml:"str"` // can't name it String because of String()
@@ -80,6 +88,7 @@ type TestRes struct {
ValidateError string `lang:"validateerror" yaml:"validate_error"` // set to cause a validate error ValidateError string `lang:"validateerror" yaml:"validate_error"` // set to cause a validate error
AlwaysGroup bool `lang:"alwaysgroup" yaml:"always_group"` // set to true to cause auto grouping AlwaysGroup bool `lang:"alwaysgroup" yaml:"always_group"` // set to true to cause auto grouping
CompareFail bool `lang:"comparefail" yaml:"compare_fail"` // will compare fail? CompareFail bool `lang:"comparefail" yaml:"compare_fail"` // will compare fail?
SendValue string `lang:"sendvalue" yaml:"send_value"` // what value should we send?
// TODO: add more fun properties! // TODO: add more fun properties!
@@ -87,12 +96,8 @@ type TestRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *TestRes) Default() Res { func (obj *TestRes) Default() engine.Res {
return &TestRes{ return &TestRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -103,128 +108,129 @@ func (obj *TestRes) Validate() error {
if s := obj.ValidateError; s != "" { if s := obj.ValidateError; s != "" {
return fmt.Errorf("the validate error param was set to: %s", s) return fmt.Errorf("the validate error param was set to: %s", s)
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *TestRes) Init() error { func (obj *TestRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *TestRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
func (obj *TestRes) Watch() error { func (obj *TestRes) Watch() error {
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
select { select {
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
// we avoid sending events on unpause if !ok {
if exit, send = obj.ReadEvent(event); exit != nil { return nil
return *exit // exit }
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for Test resource. Does nothing, returns happy! // CheckApply method for Test resource. Does nothing, returns happy!
func (obj *TestRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *TestRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply: %t", obj, apply) for key, val := range obj.init.Recv() {
if obj.Refresh() { obj.init.Logf("CheckApply: Received `%s`, changed: %t", key, val.Changed)
log.Printf("%s: Received a notification!", obj)
} }
log.Printf("%s: Bool: %v", obj, obj.Bool) if obj.init.Refresh() {
log.Printf("%s: Str: %v", obj, obj.Str) obj.init.Logf("Received a notification!")
}
log.Printf("%s: Int: %v", obj, obj.Int) obj.init.Logf("%s: Bool: %v", obj, obj.Bool)
log.Printf("%s: Int8: %v", obj, obj.Int8) obj.init.Logf("%s: Str: %v", obj, obj.Str)
log.Printf("%s: Int16: %v", obj, obj.Int16)
log.Printf("%s: Int32: %v", obj, obj.Int32)
log.Printf("%s: Int64: %v", obj, obj.Int64)
log.Printf("%s: Uint: %v", obj, obj.Uint) obj.init.Logf("%s: Int: %v", obj, obj.Int)
log.Printf("%s: Uint8: %v", obj, obj.Uint) obj.init.Logf("%s: Int8: %v", obj, obj.Int8)
log.Printf("%s: Uint16: %v", obj, obj.Uint) obj.init.Logf("%s: Int16: %v", obj, obj.Int16)
log.Printf("%s: Uint32: %v", obj, obj.Uint) obj.init.Logf("%s: Int32: %v", obj, obj.Int32)
log.Printf("%s: Uint64: %v", obj, obj.Uint) obj.init.Logf("%s: Int64: %v", obj, obj.Int64)
//log.Printf("%s: Uintptr: %v", obj, obj.Uintptr) obj.init.Logf("%s: Uint: %v", obj, obj.Uint)
log.Printf("%s: Byte: %v", obj, obj.Byte) obj.init.Logf("%s: Uint8: %v", obj, obj.Uint)
log.Printf("%s: Rune: %v", obj, obj.Rune) obj.init.Logf("%s: Uint16: %v", obj, obj.Uint)
obj.init.Logf("%s: Uint32: %v", obj, obj.Uint)
obj.init.Logf("%s: Uint64: %v", obj, obj.Uint)
log.Printf("%s: Float32: %v", obj, obj.Float32) //obj.init.Logf("%s: Uintptr: %v", obj, obj.Uintptr)
log.Printf("%s: Float64: %v", obj, obj.Float64) obj.init.Logf("%s: Byte: %v", obj, obj.Byte)
log.Printf("%s: Complex64: %v", obj, obj.Complex64) obj.init.Logf("%s: Rune: %v", obj, obj.Rune)
log.Printf("%s: Complex128: %v", obj, obj.Complex128)
log.Printf("%s: BoolPtr: %v", obj, obj.BoolPtr) obj.init.Logf("%s: Float32: %v", obj, obj.Float32)
log.Printf("%s: StringPtr: %v", obj, obj.StringPtr) obj.init.Logf("%s: Float64: %v", obj, obj.Float64)
log.Printf("%s: Int64Ptr: %v", obj, obj.Int64Ptr) obj.init.Logf("%s: Complex64: %v", obj, obj.Complex64)
log.Printf("%s: Int8Ptr: %v", obj, obj.Int8Ptr) obj.init.Logf("%s: Complex128: %v", obj, obj.Complex128)
log.Printf("%s: Uint8Ptr: %v", obj, obj.Uint8Ptr)
log.Printf("%s: Int8PtrPtrPtr: %v", obj, obj.Int8PtrPtrPtr) obj.init.Logf("%s: BoolPtr: %v", obj, obj.BoolPtr)
obj.init.Logf("%s: StringPtr: %v", obj, obj.StringPtr)
obj.init.Logf("%s: Int64Ptr: %v", obj, obj.Int64Ptr)
obj.init.Logf("%s: Int8Ptr: %v", obj, obj.Int8Ptr)
obj.init.Logf("%s: Uint8Ptr: %v", obj, obj.Uint8Ptr)
log.Printf("%s: SliceString: %v", obj, obj.SliceString) obj.init.Logf("%s: Int8PtrPtrPtr: %v", obj, obj.Int8PtrPtrPtr)
log.Printf("%s: MapIntFloat: %v", obj, obj.MapIntFloat)
log.Printf("%s: MixedStruct: %v", obj, obj.MixedStruct)
log.Printf("%s: Interface: %v", obj, obj.Interface)
log.Printf("%s: AnotherStr: %v", obj, obj.AnotherStr) obj.init.Logf("%s: SliceString: %v", obj, obj.SliceString)
obj.init.Logf("%s: MapIntFloat: %v", obj, obj.MapIntFloat)
obj.init.Logf("%s: MixedStruct: %v", obj, obj.MixedStruct)
obj.init.Logf("%s: Interface: %v", obj, obj.Interface)
obj.init.Logf("%s: AnotherStr: %v", obj, obj.AnotherStr)
// send
hello := obj.SendValue
if err := obj.init.Send(&TestSends{
Hello: &hello,
Answer: 42,
}); err != nil {
return false, err
}
return true, nil // state is always okay return true, nil // state is always okay
} }
// TestUID is the UID struct for TestRes. // Cmp compares two resources and returns an error if they are not equivalent.
type TestUID struct { func (obj *TestRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *TestRes) UIDs() []ResUID {
x := &TestUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *TestRes) GroupCmp(r Res) bool {
_, ok := r.(*TestRes)
if !ok {
return false
}
return obj.AlwaysGroup // grouped together if we were asked to
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *TestRes) Compare(r Res) bool { func (obj *TestRes) Compare(r engine.Res) bool {
// we can only compare TestRes to others of the same resource kind // we can only compare TestRes to others of the same resource kind
res, ok := r.(*TestRes) res, ok := r.(*TestRes)
if !ok { if !ok {
return false return false
} }
// calling base Compare is probably unneeded for the test res, but do it //if obj.Name != res.Name {
if !obj.BaseRes.Compare(res) { // call base Compare // return false
return false //}
}
if obj.Name != res.Name {
return false
}
if obj.CompareFail || res.CompareFail { if obj.CompareFail || res.CompareFail {
return false return false
@@ -368,6 +374,9 @@ func (obj *TestRes) Compare(r Res) bool {
if obj.AlwaysGroup != res.AlwaysGroup { if obj.AlwaysGroup != res.AlwaysGroup {
return false return false
} }
if obj.SendValue != res.SendValue {
return false
}
if obj.Comment != res.Comment { if obj.Comment != res.Comment {
return false return false
@@ -376,6 +385,50 @@ func (obj *TestRes) Compare(r Res) bool {
return true return true
} }
// TestUID is the UID struct for TestRes.
type TestUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *TestRes) UIDs() []engine.ResUID {
x := &TestUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *TestRes) GroupCmp(r engine.GroupableRes) error {
_, ok := r.(*TestRes)
if !ok {
return fmt.Errorf("resource is not the same kind")
}
if !obj.AlwaysGroup { // grouped together if we were asked to
return fmt.Errorf("the AlwaysGroup param is false")
}
return nil
}
// TestSends is the struct of data which is sent after a successful Apply.
type TestSends struct {
// Hello is some value being sent.
Hello *string
Answer int // some other value being sent
}
// Sends represents the default struct of values we can send using Send/Recv.
func (obj *TestRes) Sends() interface{} {
return &TestSends{
Hello: nil,
Answer: -1,
}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *TestRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *TestRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -0,0 +1,177 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package resources
import (
"reflect"
"testing"
engineUtil "github.com/purpleidea/mgmt/engine/util"
)
func TestStructTagToFieldName0(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool `lang:"alpha" yaml:"nope"`
Beta string `yaml:"beta"`
Gamma string
Delta int `lang:"surprise"`
}
mapping, err := engineUtil.StructTagToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"alpha": "Alpha",
"surprise": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName0(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := engineUtil.LowerStructFieldNameToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"testres": "TestRes", // hide by specifying `lang:""` on it
"alpha": "Alpha",
//"skipme": "skipMe",
"beta": "Beta",
"iamacamel": "IAmACamel",
//"pass": "pass",
"gamma": "Gamma",
"delta": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName1(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
// these two should collide
DoubleWord bool
Doubleword string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := engineUtil.LowerStructFieldNameToFieldName(&TestStruct{})
if err == nil {
t.Errorf("expected failure, but passed with: %+v", mapping)
return
}
}
func TestLowerStructFieldNameToFieldName2(t *testing.T) {
mapping, err := engineUtil.LowerStructFieldNameToFieldName(&TestRes{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"base": "Base", // all resources have this trait
"groupable": "Groupable", // the TestRes has this trait
"refreshable": "Refreshable", // the TestRes has this trait
"sendable": "Sendable",
"recvable": "Recvable",
"bool": "Bool",
"str": "Str",
"int": "Int",
"int8": "Int8",
"int16": "Int16",
"int32": "Int32",
"int64": "Int64",
"uint": "Uint",
"uint8": "Uint8",
"uint16": "Uint16",
"uint32": "Uint32",
"uint64": "Uint64",
"byte": "Byte",
"rune": "Rune",
"float32": "Float32",
"float64": "Float64",
"complex64": "Complex64",
"complex128": "Complex128",
"boolptr": "BoolPtr",
"stringptr": "StringPtr",
"int64ptr": "Int64Ptr",
"int8ptr": "Int8Ptr",
"uint8ptr": "Uint8Ptr",
"int8ptrptrptr": "Int8PtrPtrPtr",
"slicestring": "SliceString",
"mapintfloat": "MapIntFloat",
"mixedstruct": "MixedStruct",
"interface": "Interface",
"anotherstr": "AnotherStr",
"validatebool": "ValidateBool",
"validateerror": "ValidateError",
"alwaysgroup": "AlwaysGroup",
"comparefail": "CompareFail",
"sendvalue": "SendValue",
"comment": "Comment",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}

View File

@@ -19,46 +19,49 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
) )
func init() { func init() {
RegisterResource("timer", func() Res { return &TimerRes{} }) engine.RegisterResource("timer", func() engine.Res { return &TimerRes{} })
} }
// TimerRes is a timer resource for time based events. It outputs an event every // TimerRes is a timer resource for time based events. It outputs an event every
// interval seconds. // interval seconds.
type TimerRes struct { type TimerRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Refreshable
init *engine.Init
Interval uint32 `yaml:"interval"` // interval between runs in seconds Interval uint32 `yaml:"interval"` // interval between runs in seconds
ticker *time.Ticker ticker *time.Ticker
} }
// TimerUID is the UID struct for TimerRes.
type TimerUID struct {
BaseUID
name string
}
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *TimerRes) Default() Res { func (obj *TimerRes) Default() engine.Res {
return &TimerRes{ return &TimerRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate the params that are passed to TimerRes. // Validate the params that are passed to TimerRes.
func (obj *TimerRes) Validate() error { func (obj *TimerRes) Validate() error {
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *TimerRes) Init() error { func (obj *TimerRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overrriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *TimerRes) Close() error {
return nil
} }
// newTicker creates a new ticker // newTicker creates a new ticker
@@ -73,27 +76,31 @@ func (obj *TimerRes) Watch() error {
defer obj.ticker.Stop() defer obj.ticker.Stop()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false var send = false // send event?
for { for {
select { select {
case <-obj.ticker.C: // received the timer event case <-obj.ticker.C: // received the timer event
send = true send = true
log.Printf("%s: received tick", obj) obj.init.Logf("received tick")
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, _ := obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
} }
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -102,7 +109,7 @@ func (obj *TimerRes) Watch() error {
func (obj *TimerRes) CheckApply(apply bool) (bool, error) { func (obj *TimerRes) CheckApply(apply bool) (bool, error) {
// because there are no checks to run, this resource has a less // because there are no checks to run, this resource has a less
// traditional pattern than what is seen in most resources... // traditional pattern than what is seen in most resources...
if !obj.Refresh() { // this works for apply || !apply if !obj.init.Refresh() { // this works for apply || !apply
return true, nil // state is always okay if no refresh to do return true, nil // state is always okay if no refresh to do
} else if !apply { // we had a refresh to do } else if !apply { // we had a refresh to do
return false, nil // therefore state is wrong return false, nil // therefore state is wrong
@@ -114,32 +121,21 @@ func (obj *TimerRes) CheckApply(apply bool) (bool, error) {
return false, nil return false, nil
} }
// UIDs includes all params to make a unique identification of this object. // Cmp compares two resources and returns an error if they are not equivalent.
// Most resources only return one, although some resources can return multiple. func (obj *TimerRes) Cmp(r engine.Res) error {
func (obj *TimerRes) UIDs() []ResUID { if !obj.Compare(r) {
x := &TimerUID{ return fmt.Errorf("did not compare")
BaseUID: BaseUID{
Name: obj.GetName(),
Kind: obj.GetKind(),
},
name: obj.Name,
} }
return []ResUID{x} return nil
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *TimerRes) Compare(r Res) bool { func (obj *TimerRes) Compare(r engine.Res) bool {
// we can only compare TimerRes to others of the same resource kind // we can only compare TimerRes to others of the same resource kind
res, ok := r.(*TimerRes) res, ok := r.(*TimerRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) {
return false
}
if obj.Name != res.Name {
return false
}
if obj.Interval != res.Interval { if obj.Interval != res.Interval {
return false return false
@@ -148,6 +144,23 @@ func (obj *TimerRes) Compare(r Res) bool {
return true return true
} }
// TimerUID is the UID struct for TimerRes.
type TimerUID struct {
engine.BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *TimerRes) UIDs() []engine.ResUID {
x := &TimerUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *TimerRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *TimerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -20,7 +20,6 @@ package resources
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"os/exec" "os/exec"
"os/user" "os/user"
"sort" "sort"
@@ -28,20 +27,26 @@ import (
"strings" "strings"
"syscall" "syscall"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("user", func() Res { return &UserRes{} }) engine.RegisterResource("user", func() engine.Res { return &UserRes{} })
} }
const passwdFile = "/etc/passwd" const passwdFile = "/etc/passwd"
// UserRes is a user account resource. // UserRes is a user account resource.
type UserRes struct { type UserRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Edgeable
init *engine.Init
State string `yaml:"state"` // state: exists, absent State string `yaml:"state"` // state: exists, absent
UID *uint32 `yaml:"uid"` // uid must be unique unless AllowDuplicateUID is true UID *uint32 `yaml:"uid"` // uid must be unique unless AllowDuplicateUID is true
GID *uint32 `yaml:"gid"` // gid of the user's primary group GID *uint32 `yaml:"gid"` // gid of the user's primary group
@@ -54,12 +59,8 @@ type UserRes struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *UserRes) Default() Res { func (obj *UserRes) Default() engine.Res {
return &UserRes{ return &UserRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -94,12 +95,19 @@ func (obj *UserRes) Validate() error {
} }
} }
} }
return obj.BaseRes.Validate() return nil
} }
// Init initializes the resource. // Init runs some startup code for this resource.
func (obj *UserRes) Init() error { func (obj *UserRes) Init(init *engine.Init) error {
return obj.BaseRes.Init() // call base init, b/c we're overriding obj.init = init // save for later
return nil
}
// Close is run by the engine to clean up after the resource is done.
func (obj *UserRes) Close() error {
return nil
} }
// Watch is the primary listener for this resource and it outputs events. // Watch is the primary listener for this resource and it outputs events.
@@ -112,16 +120,14 @@ func (obj *UserRes) Watch() error {
defer obj.recWatcher.Close() defer obj.recWatcher.Close()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false // send event? var send = false // send event?
var exit *error
for { for {
if obj.debug { if obj.init.Debug {
log.Printf("%s: Watching: %s", obj, passwdFile) // attempting to watch... obj.init.Logf("Watching: %s", passwdFile) // attempting to watch...
} }
select { select {
@@ -132,33 +138,37 @@ func (obj *UserRes) Watch() error {
if err := event.Error; err != nil { if err := event.Error; err != nil {
return errwrap.Wrapf(err, "Unknown %s watcher error", obj) return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
} }
if obj.debug { // don't access event.Body if event.Error isn't nil if obj.init.Debug { // don't access event.Body if event.Error isn't nil
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op) obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
//obj.StateOK(false) // dirty // these events don't invalidate state
} }
// 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
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
// CheckApply method for User resource. // CheckApply method for User resource.
func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) { func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply) obj.init.Logf("CheckApply(%t)", apply)
var exists = true var exists = true
usr, err := user.Lookup(obj.GetName()) usr, err := user.Lookup(obj.Name())
if err != nil { if err != nil {
if _, ok := err.(user.UnknownUserError); !ok { if _, ok := err.(user.UnknownUserError); !ok {
return false, errwrap.Wrapf(err, "error looking up user") return false, errwrap.Wrapf(err, "error looking up user")
@@ -172,7 +182,7 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
if _, ok := err.(user.UnknownUserIdError); !ok { if _, ok := err.(user.UnknownUserIdError); !ok {
return false, errwrap.Wrapf(err, "error looking up UID") return false, errwrap.Wrapf(err, "error looking up UID")
} }
} else if existingUID.Username != obj.GetName() { } else if existingUID.Username != obj.Name() {
return false, fmt.Errorf("the requested UID is already taken") return false, fmt.Errorf("the requested UID is already taken")
} }
} }
@@ -213,10 +223,10 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
if obj.State == "exists" { if obj.State == "exists" {
if exists { if exists {
cmdName = "usermod" cmdName = "usermod"
log.Printf("%s: Modifying user: %s", obj, obj.GetName()) obj.init.Logf("Modifying user: %s", obj.Name())
} else { } else {
cmdName = "useradd" cmdName = "useradd"
log.Printf("%s: Adding user: %s", obj, obj.GetName()) obj.init.Logf("Adding user: %s", obj.Name())
} }
if obj.AllowDuplicateUID { if obj.AllowDuplicateUID {
args = append(args, "--non-unique") args = append(args, "--non-unique")
@@ -239,10 +249,10 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
} }
if obj.State == "absent" { if obj.State == "absent" {
cmdName = "userdel" cmdName = "userdel"
log.Printf("%s: Deleting user: %s", obj, obj.GetName()) obj.init.Logf("Deleting user: %s", obj.Name())
} }
args = append(args, obj.GetName()) args = append(args, obj.Name())
cmd := exec.Command(cmdName, args...) cmd := exec.Command(cmdName, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{ cmd.SysProcAttr = &syscall.SysProcAttr{
@@ -273,113 +283,22 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil return false, nil
} }
// UserUID is the UID struct for UserRes. // Cmp compares two resources and returns an error if they are not equivalent.
type UserUID struct { func (obj *UserRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
name string return fmt.Errorf("did not compare")
}
// UserResAutoEdges holds the state of the auto edge generator.
type UserResAutoEdges struct {
UIDs []ResUID
pointer int
}
// AutoEdges returns edges from the user resource to each group found in
// its definition. The groups can be in any of the three applicable fields
// (GID, Group and Groups.) If the user exists, reversed ensures the edge
// goes from group to user, and if the user is absent the edge goes from
// user to group. This ensures that we don't add users to groups that
// don't exist or delete groups before we delete their members.
func (obj *UserRes) AutoEdges() (AutoEdge, error) {
var result []ResUID
var reversed bool
if obj.State == "exists" {
reversed = true
} }
if obj.GID != nil {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
gid: obj.GID,
})
}
if obj.Group != nil {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
name: *obj.Group,
})
}
for _, group := range obj.Groups {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
name: group,
})
}
return &UserResAutoEdges{
UIDs: result,
pointer: 0,
}, nil
}
// Next returns the next automatic edge.
func (obj *UserResAutoEdges) Next() []ResUID {
if len(obj.UIDs) == 0 {
return nil return nil
}
value := obj.UIDs[obj.pointer]
obj.pointer++
return []ResUID{value}
}
// Test gets results of the earlier Next() call, & returns if we should continue.
func (obj *UserResAutoEdges) Test(input []bool) bool {
if len(obj.UIDs) <= obj.pointer {
return false
}
if len(input) != 1 { // in case we get given bad data
log.Fatal("Expecting a single value!")
}
return true // keep going
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *UserRes) UIDs() []ResUID {
x := &UserUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *UserRes) GroupCmp(r Res) bool {
_, ok := r.(*UserRes)
if !ok {
return false
}
return false
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *UserRes) Compare(r Res) bool { func (obj *UserRes) Compare(r engine.Res) bool {
// we can only compare UserRes to others of the same resource kind // we can only compare UserRes to others of the same resource kind
res, ok := r.(*UserRes) res, ok := r.(*UserRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State { if obj.State != res.State {
return false return false
} }
@@ -430,6 +349,91 @@ func (obj *UserRes) Compare(r Res) bool {
return true return true
} }
// UserUID is the UID struct for UserRes.
type UserUID struct {
engine.BaseUID
name string
}
// UserResAutoEdges holds the state of the auto edge generator.
type UserResAutoEdges struct {
UIDs []engine.ResUID
pointer int
}
// AutoEdges returns edges from the user resource to each group found in
// its definition. The groups can be in any of the three applicable fields
// (GID, Group and Groups.) If the user exists, reversed ensures the edge
// goes from group to user, and if the user is absent the edge goes from
// user to group. This ensures that we don't add users to groups that
// don't exist or delete groups before we delete their members.
func (obj *UserRes) AutoEdges() (engine.AutoEdge, error) {
var result []engine.ResUID
var reversed bool
if obj.State == "exists" {
reversed = true
}
if obj.GID != nil {
result = append(result, &GroupUID{
BaseUID: engine.BaseUID{
Reversed: &reversed,
},
gid: obj.GID,
})
}
if obj.Group != nil {
result = append(result, &GroupUID{
BaseUID: engine.BaseUID{
Reversed: &reversed,
},
name: *obj.Group,
})
}
for _, group := range obj.Groups {
result = append(result, &GroupUID{
BaseUID: engine.BaseUID{
Reversed: &reversed,
},
name: group,
})
}
return &UserResAutoEdges{
UIDs: result,
pointer: 0,
}, nil
}
// Next returns the next automatic edge.
func (obj *UserResAutoEdges) Next() []engine.ResUID {
if len(obj.UIDs) == 0 {
return nil
}
value := obj.UIDs[obj.pointer]
obj.pointer++
return []engine.ResUID{value}
}
// Test gets results of the earlier Next() call, & returns if we should continue.
func (obj *UserResAutoEdges) Test(input []bool) bool {
if len(obj.UIDs) <= obj.pointer {
return false
}
if len(input) != 1 { // in case we get given bad data
panic(fmt.Sprintf("Expecting a single value!"))
}
return true // keep going
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *UserRes) UIDs() []engine.ResUID {
x := &UserUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *UserRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *UserRes) UnmarshalYAML(unmarshal func(interface{}) error) error {

View File

@@ -21,26 +21,23 @@ package resources
import ( import (
"fmt" "fmt"
"log"
"math/rand" "math/rand"
"net/url" "net/url"
"os/user"
"path"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
multierr "github.com/hashicorp/go-multierror"
"github.com/libvirt/libvirt-go" "github.com/libvirt/libvirt-go"
libvirtxml "github.com/libvirt/libvirt-go-xml" libvirtxml "github.com/libvirt/libvirt-go-xml"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
func init() { func init() {
RegisterResource("virt", func() Res { return &VirtRes{} }) engine.RegisterResource("virt", func() engine.Res { return &VirtRes{} })
} }
const ( const (
@@ -65,17 +62,15 @@ const (
lxcURI lxcURI
) )
// VirtAuth is used to pass credentials to libvirt.
type VirtAuth struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
// VirtRes is a libvirt resource. A transient virt resource, which has its state // VirtRes is a libvirt resource. A transient virt resource, which has its state
// set to `shutoff` is one which does not exist. The parallel equivalent is a // set to `shutoff` is one which does not exist. The parallel equivalent is a
// file resource which removes a particular path. // file resource which removes a particular path.
type VirtRes struct { type VirtRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
traits.Refreshable
init *engine.Init
URI string `yaml:"uri"` // connection uri, eg: qemu:///session URI string `yaml:"uri"` // connection uri, eg: qemu:///session
State string `yaml:"state"` // running, paused, shutoff State string `yaml:"state"` // running, paused, shutoff
Transient bool `yaml:"transient"` // defined (false) or undefined (true) Transient bool `yaml:"transient"` // defined (false) or undefined (true)
@@ -106,13 +101,15 @@ type VirtRes struct {
guestAgentConnected bool // our tracking of if guest agent is running guestAgentConnected bool // our tracking of if guest agent is running
} }
// Default returns some sensible defaults for this resource. // VirtAuth is used to pass credentials to libvirt.
func (obj *VirtRes) Default() Res { type VirtAuth struct {
return &VirtRes{ Username string `yaml:"username"`
BaseRes: BaseRes{ Password string `yaml:"password"`
MetaParams: DefaultMetaParams, // force a default }
},
// Default returns some sensible defaults for this resource.
func (obj *VirtRes) Default() engine.Res {
return &VirtRes{
MaxCPUs: DefaultMaxCPUs, MaxCPUs: DefaultMaxCPUs,
HotCPUs: true, // we're a dynamic engine, be dynamic by default! HotCPUs: true, // we're a dynamic engine, be dynamic by default!
@@ -125,11 +122,13 @@ func (obj *VirtRes) Validate() error {
if obj.CPUs > obj.MaxCPUs { if obj.CPUs > obj.MaxCPUs {
return fmt.Errorf("the number of CPUs (%d) must not be greater than MaxCPUs (%d)", obj.CPUs, obj.MaxCPUs) return fmt.Errorf("the number of CPUs (%d) must not be greater than MaxCPUs (%d)", obj.CPUs, obj.MaxCPUs)
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *VirtRes) Init() error { func (obj *VirtRes) Init(init *engine.Init) error {
obj.init = init // save for later
if !libvirtInitialized { if !libvirtInitialized {
if err := libvirt.EventRegisterDefaultImpl(); err != nil { if err := libvirt.EventRegisterDefaultImpl(); err != nil {
return errwrap.Wrapf(err, "method EventRegisterDefaultImpl failed") return errwrap.Wrapf(err, "method EventRegisterDefaultImpl failed")
@@ -154,7 +153,7 @@ func (obj *VirtRes) Init() error {
} }
// check for hard to change properties // check for hard to change properties
dom, err := obj.conn.LookupDomainByName(obj.GetName()) dom, err := obj.conn.LookupDomainByName(obj.Name())
if err == nil { if err == nil {
defer dom.Free() defer dom.Free()
} else if !isNotFound(err) { } else if !isNotFound(err) {
@@ -194,7 +193,7 @@ func (obj *VirtRes) Init() error {
} }
} }
obj.wg = &sync.WaitGroup{} obj.wg = &sync.WaitGroup{}
return obj.BaseRes.Init() // call base init, b/c we're overriding return nil
} }
// Close runs some cleanup code for this resource. // Close runs some cleanup code for this resource.
@@ -209,12 +208,6 @@ func (obj *VirtRes) Close() error {
_, err := obj.conn.Close() // close libvirt conn that was opened in Init _, err := obj.conn.Close() // close libvirt conn that was opened in Init
obj.conn = nil // set to nil to help catch any nil ptr bugs! obj.conn = nil // set to nil to help catch any nil ptr bugs!
// call base close, b/c we're overriding
if e := obj.BaseRes.Close(); err == nil {
err = e
} else if e != nil {
err = multierr.Append(err, e) // list of errors
}
return err return err
} }
@@ -279,7 +272,7 @@ func (obj *VirtRes) Watch() error {
go func() { go func() {
defer obj.wg.Done() defer obj.wg.Done()
defer wg.Done() defer wg.Done()
defer log.Printf("EventRunDefaultImpl exited!") defer obj.init.Logf("EventRunDefaultImpl exited!")
for { for {
// TODO: can we merge this into our main for loop below? // TODO: can we merge this into our main for loop below?
select { select {
@@ -287,7 +280,7 @@ func (obj *VirtRes) Watch() error {
return return
default: default:
} }
//log.Printf("EventRunDefaultImpl started!") //obj.init.Logf("EventRunDefaultImpl started!")
if err := libvirt.EventRunDefaultImpl(); err != nil { if err := libvirt.EventRunDefaultImpl(); err != nil {
select { select {
case errorChan <- errwrap.Wrapf(err, "EventRunDefaultImpl failed"): case errorChan <- errwrap.Wrapf(err, "EventRunDefaultImpl failed"):
@@ -296,14 +289,14 @@ func (obj *VirtRes) Watch() error {
} }
return return
} }
//log.Printf("EventRunDefaultImpl looped!") //obj.init.Logf("EventRunDefaultImpl looped!")
} }
}() }()
// domain events callback // domain events callback
domCallback := func(c *libvirt.Connect, d *libvirt.Domain, ev *libvirt.DomainEventLifecycle) { domCallback := func(c *libvirt.Connect, d *libvirt.Domain, ev *libvirt.DomainEventLifecycle) {
domName, _ := d.GetName() domName, _ := d.GetName()
if domName == obj.GetName() { if domName == obj.Name() {
select { select {
case domChan <- ev.Event: // send case domChan <- ev.Event: // send
case <-exitChan: case <-exitChan:
@@ -320,7 +313,7 @@ func (obj *VirtRes) Watch() error {
// guest agent events callback // guest agent events callback
gaCallback := func(c *libvirt.Connect, d *libvirt.Domain, eva *libvirt.DomainEventAgentLifecycle) { gaCallback := func(c *libvirt.Connect, d *libvirt.Domain, eva *libvirt.DomainEventAgentLifecycle) {
domName, _ := d.GetName() domName, _ := d.GetName()
if domName == obj.GetName() { if domName == obj.Name() {
select { select {
case gaChan <- eva: // send case gaChan <- eva: // send
case <-exitChan: case <-exitChan:
@@ -334,13 +327,11 @@ func (obj *VirtRes) Watch() error {
defer obj.conn.DomainEventDeregister(gaCallbackID) defer obj.conn.DomainEventDeregister(gaCallbackID)
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var send = false var send = false // send event?
var exit *error // if ptr exists, that is the exit error to return
for { for {
processExited := false // did the process exit fully (shutdown)? processExited := false // did the process exit fully (shutdown)?
select { select {
@@ -349,31 +340,31 @@ func (obj *VirtRes) Watch() error {
switch event { switch event {
case libvirt.DOMAIN_EVENT_DEFINED: case libvirt.DOMAIN_EVENT_DEFINED:
if obj.Transient { if obj.Transient {
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
} }
case libvirt.DOMAIN_EVENT_UNDEFINED: case libvirt.DOMAIN_EVENT_UNDEFINED:
if !obj.Transient { if !obj.Transient {
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
} }
case libvirt.DOMAIN_EVENT_STARTED: case libvirt.DOMAIN_EVENT_STARTED:
fallthrough fallthrough
case libvirt.DOMAIN_EVENT_RESUMED: case libvirt.DOMAIN_EVENT_RESUMED:
if obj.State != "running" { if obj.State != "running" {
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
} }
case libvirt.DOMAIN_EVENT_SUSPENDED: case libvirt.DOMAIN_EVENT_SUSPENDED:
if obj.State != "paused" { if obj.State != "paused" {
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
} }
case libvirt.DOMAIN_EVENT_STOPPED: case libvirt.DOMAIN_EVENT_STOPPED:
fallthrough fallthrough
case libvirt.DOMAIN_EVENT_SHUTDOWN: case libvirt.DOMAIN_EVENT_SHUTDOWN:
if obj.State != "shutoff" { if obj.State != "shutoff" {
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
} }
processExited = true processExited = true
@@ -384,7 +375,7 @@ func (obj *VirtRes) Watch() error {
// verify, detect and patch appropriately! // verify, detect and patch appropriately!
fallthrough fallthrough
case libvirt.DOMAIN_EVENT_CRASHED: case libvirt.DOMAIN_EVENT_CRASHED:
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
processExited = true // FIXME: is this okay for PMSUSPENDED ? processExited = true // FIXME: is this okay for PMSUSPENDED ?
} }
@@ -399,16 +390,16 @@ func (obj *VirtRes) Watch() error {
if state == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_STATE_CONNECTED { if state == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_STATE_CONNECTED {
obj.guestAgentConnected = true obj.guestAgentConnected = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
send = true send = true
log.Printf("%s: Guest agent connected", obj) obj.init.Logf("Guest agent connected")
} else if state == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_STATE_DISCONNECTED { } else if state == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_STATE_DISCONNECTED {
obj.guestAgentConnected = false obj.guestAgentConnected = false
// ignore CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_DOMAIN_STARTED // ignore CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_DOMAIN_STARTED
// events because they just tell you that guest agent channel was added // events because they just tell you that guest agent channel was added
if reason == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_CHANNEL { if reason == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_CHANNEL {
log.Printf("%s: Guest agent disconnected", obj) obj.init.Logf("Guest agent disconnected")
} }
} else { } else {
@@ -418,15 +409,21 @@ func (obj *VirtRes) Watch() error {
case err := <-errorChan: case err := <-errorChan:
return fmt.Errorf("unknown %s libvirt error: %s", obj, err) return fmt.Errorf("unknown %s libvirt error: %s", obj, err)
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// do all our event sending all together to avoid duplicate msgs
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -454,7 +451,7 @@ func (obj *VirtRes) domainCreate() (*libvirt.Domain, bool, error) {
if err != nil { if err != nil {
return dom, false, err // returned dom is invalid return dom, false, err // returned dom is invalid
} }
log.Printf("%s: Domain transient %s", state, obj) obj.init.Logf("Domain transient %s", state)
return dom, false, nil return dom, false, nil
} }
@@ -462,20 +459,20 @@ func (obj *VirtRes) domainCreate() (*libvirt.Domain, bool, error) {
if err != nil { if err != nil {
return dom, false, err // returned dom is invalid return dom, false, err // returned dom is invalid
} }
log.Printf("%s: Domain defined", obj) obj.init.Logf("Domain defined")
if obj.State == "running" { if obj.State == "running" {
if err := dom.Create(); err != nil { if err := dom.Create(); err != nil {
return dom, false, err return dom, false, err
} }
log.Printf("%s: Domain started", obj) obj.init.Logf("Domain started")
} }
if obj.State == "paused" { if obj.State == "paused" {
if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil { if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil {
return dom, false, err return dom, false, err
} }
log.Printf("%s: Domain created paused", obj) obj.init.Logf("Domain created paused")
} }
return dom, false, nil return dom, false, nil
@@ -503,7 +500,7 @@ func (obj *VirtRes) stateCheckApply(apply bool, dom *libvirt.Domain) (bool, erro
} }
if domInfo.State == libvirt.DOMAIN_BLOCKED { if domInfo.State == libvirt.DOMAIN_BLOCKED {
// TODO: what should happen? // TODO: what should happen?
return false, fmt.Errorf("domain %s is blocked", obj.GetName()) return false, fmt.Errorf("domain %s is blocked", obj.Name())
} }
if !apply { if !apply {
return false, nil return false, nil
@@ -513,14 +510,14 @@ func (obj *VirtRes) stateCheckApply(apply bool, dom *libvirt.Domain) (bool, erro
return false, errwrap.Wrapf(err, "domain.Resume failed") return false, errwrap.Wrapf(err, "domain.Resume failed")
} }
checkOK = false checkOK = false
log.Printf("%s: Domain resumed", obj) obj.init.Logf("Domain resumed")
break break
} }
if err := dom.Create(); err != nil { if err := dom.Create(); err != nil {
return false, errwrap.Wrapf(err, "domain.Create failed") return false, errwrap.Wrapf(err, "domain.Create failed")
} }
checkOK = false checkOK = false
log.Printf("%s: Domain created", obj) obj.init.Logf("Domain created")
case "paused": case "paused":
if domInfo.State == libvirt.DOMAIN_PAUSED { if domInfo.State == libvirt.DOMAIN_PAUSED {
@@ -534,14 +531,14 @@ func (obj *VirtRes) stateCheckApply(apply bool, dom *libvirt.Domain) (bool, erro
return false, errwrap.Wrapf(err, "domain.Suspend failed") return false, errwrap.Wrapf(err, "domain.Suspend failed")
} }
checkOK = false checkOK = false
log.Printf("%s: Domain paused", obj) obj.init.Logf("Domain paused")
break break
} }
if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil { if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil {
return false, errwrap.Wrapf(err, "domain.CreateWithFlags failed") return false, errwrap.Wrapf(err, "domain.CreateWithFlags failed")
} }
checkOK = false checkOK = false
log.Printf("%s: Domain created paused", obj) obj.init.Logf("Domain created paused")
case "shutoff": case "shutoff":
if domInfo.State == libvirt.DOMAIN_SHUTOFF || domInfo.State == libvirt.DOMAIN_SHUTDOWN { if domInfo.State == libvirt.DOMAIN_SHUTOFF || domInfo.State == libvirt.DOMAIN_SHUTDOWN {
@@ -555,7 +552,7 @@ func (obj *VirtRes) stateCheckApply(apply bool, dom *libvirt.Domain) (bool, erro
return false, errwrap.Wrapf(err, "domain.Destroy failed") return false, errwrap.Wrapf(err, "domain.Destroy failed")
} }
checkOK = false checkOK = false
log.Printf("%s: Domain destroyed", obj) obj.init.Logf("Domain destroyed")
} }
return checkOK, nil return checkOK, nil
@@ -581,7 +578,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
if err := dom.SetMemory(obj.Memory); err != nil { if err := dom.SetMemory(obj.Memory); err != nil {
return false, errwrap.Wrapf(err, "domain.SetMemory failed") return false, errwrap.Wrapf(err, "domain.SetMemory failed")
} }
log.Printf("%s: Memory changed to %d", obj, obj.Memory) obj.init.Logf("Memory changed to %d", obj.Memory)
} }
// check cpus // check cpus
@@ -620,7 +617,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
return false, errwrap.Wrapf(err, "domain.SetVcpus failed") return false, errwrap.Wrapf(err, "domain.SetVcpus failed")
} }
checkOK = false checkOK = false
log.Printf("%s: CPUs (hot) changed to %d", obj, obj.CPUs) obj.init.Logf("CPUs (hot) changed to %d", obj.CPUs)
case libvirt.DOMAIN_SHUTOFF, libvirt.DOMAIN_SHUTDOWN: case libvirt.DOMAIN_SHUTOFF, libvirt.DOMAIN_SHUTDOWN:
if !obj.Transient { if !obj.Transient {
@@ -632,7 +629,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
return false, errwrap.Wrapf(err, "domain.SetVcpus failed") return false, errwrap.Wrapf(err, "domain.SetVcpus failed")
} }
checkOK = false checkOK = false
log.Printf("%s: CPUs (cold) changed to %d", obj, obj.CPUs) obj.init.Logf("CPUs (cold) changed to %d", obj.CPUs)
} }
default: default:
@@ -663,7 +660,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
return false, errwrap.Wrapf(err, "domain.SetVcpus failed") return false, errwrap.Wrapf(err, "domain.SetVcpus failed")
} }
checkOK = false checkOK = false
log.Printf("%s: CPUs (guest) changed to %d", obj, obj.CPUs) obj.init.Logf("CPUs (guest) changed to %d", obj.CPUs)
} }
} }
@@ -687,7 +684,7 @@ func (obj *VirtRes) domainShutdownSync(apply bool, dom *libvirt.Domain) (bool, e
return false, errwrap.Wrapf(err, "domain.GetInfo failed") return false, errwrap.Wrapf(err, "domain.GetInfo failed")
} }
if domInfo.State == libvirt.DOMAIN_SHUTOFF || domInfo.State == libvirt.DOMAIN_SHUTDOWN { if domInfo.State == libvirt.DOMAIN_SHUTOFF || domInfo.State == libvirt.DOMAIN_SHUTDOWN {
log.Printf("%s: Shutdown", obj) obj.init.Logf("Shutdown")
break break
} }
@@ -699,7 +696,7 @@ func (obj *VirtRes) domainShutdownSync(apply bool, dom *libvirt.Domain) (bool, e
obj.processExitChan = make(chan struct{}) obj.processExitChan = make(chan struct{})
// if machine shuts down before we call this, we error; // if machine shuts down before we call this, we error;
// this isn't ideal, but it happened due to user error! // this isn't ideal, but it happened due to user error!
log.Printf("%s: Running shutdown", obj) obj.init.Logf("Running shutdown")
if err := dom.Shutdown(); err != nil { if err := dom.Shutdown(); err != nil {
// FIXME: if machine is already shutdown completely, return early // FIXME: if machine is already shutdown completely, return early
return false, errwrap.Wrapf(err, "domain.Shutdown failed") return false, errwrap.Wrapf(err, "domain.Shutdown failed")
@@ -735,7 +732,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
} }
// if we do the restart, we must flip the flag back to false as evidence // if we do the restart, we must flip the flag back to false as evidence
var restart bool // do we need to do a restart? var restart bool // do we need to do a restart?
if obj.RestartOnRefresh && obj.Refresh() { // a refresh is a restart ask if obj.RestartOnRefresh && obj.init.Refresh() { // a refresh is a restart ask
restart = true restart = true
} }
@@ -750,7 +747,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
var checkOK = true var checkOK = true
dom, err := obj.conn.LookupDomainByName(obj.GetName()) dom, err := obj.conn.LookupDomainByName(obj.Name())
if err == nil { if err == nil {
// pass // pass
} else if isNotFound(err) { } else if isNotFound(err) {
@@ -792,7 +789,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
if err := dom.Undefine(); err != nil { if err := dom.Undefine(); err != nil {
return false, errwrap.Wrapf(err, "domain.Undefine failed") return false, errwrap.Wrapf(err, "domain.Undefine failed")
} }
log.Printf("%s: Domain undefined", obj) obj.init.Logf("Domain undefined")
} else { } else {
domXML, err := dom.GetXMLDesc(libvirt.DOMAIN_XML_INACTIVE) domXML, err := dom.GetXMLDesc(libvirt.DOMAIN_XML_INACTIVE)
if err != nil { if err != nil {
@@ -801,7 +798,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
if _, err = obj.conn.DomainDefineXML(domXML); err != nil { if _, err = obj.conn.DomainDefineXML(domXML); err != nil {
return false, errwrap.Wrapf(err, "conn.DomainDefineXML failed") return false, errwrap.Wrapf(err, "conn.DomainDefineXML failed")
} }
log.Printf("%s: Domain defined", obj) obj.init.Logf("Domain defined")
} }
checkOK = false checkOK = false
} }
@@ -890,7 +887,7 @@ func (obj *VirtRes) getDomainXML() string {
var b string var b string
b += obj.getDomainType() // start domain b += obj.getDomainType() // start domain
b += fmt.Sprintf("<name>%s</name>", obj.GetName()) b += fmt.Sprintf("<name>%s</name>", obj.Name())
b += fmt.Sprintf("<memory unit='KiB'>%d</memory>", obj.Memory) b += fmt.Sprintf("<memory unit='KiB'>%d</memory>", obj.Memory)
if obj.HotCPUs { if obj.HotCPUs {
@@ -996,7 +993,7 @@ type filesystemDevice struct {
} }
func (d *diskDevice) GetXML(idx int) string { func (d *diskDevice) GetXML(idx int) string {
source, _ := expandHome(d.Source) // TODO: should we handle errors? source, _ := util.ExpandHome(d.Source) // TODO: should we handle errors?
var b string var b string
b += "<disk type='file' device='disk'>" b += "<disk type='file' device='disk'>"
b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type) b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type)
@@ -1007,7 +1004,7 @@ func (d *diskDevice) GetXML(idx int) string {
} }
func (d *cdRomDevice) GetXML(idx int) string { func (d *cdRomDevice) GetXML(idx int) string {
source, _ := expandHome(d.Source) // TODO: should we handle errors? source, _ := util.ExpandHome(d.Source) // TODO: should we handle errors?
var b string var b string
b += "<disk type='file' device='cdrom'>" b += "<disk type='file' device='cdrom'>"
b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type) b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type)
@@ -1031,7 +1028,7 @@ func (d *networkDevice) GetXML(idx int) string {
} }
func (d *filesystemDevice) GetXML(idx int) string { func (d *filesystemDevice) GetXML(idx int) string {
source, _ := expandHome(d.Source) // TODO: should we handle errors? source, _ := util.ExpandHome(d.Source) // TODO: should we handle errors?
var b string var b string
b += "<filesystem" // open b += "<filesystem" // open
if d.Access != "" { if d.Access != "" {
@@ -1047,43 +1044,21 @@ func (d *filesystemDevice) GetXML(idx int) string {
return b return b
} }
// VirtUID is the UID struct for FileRes. // Cmp compares two resources and returns an error if they are not equivalent.
type VirtUID struct { func (obj *VirtRes) Cmp(r engine.Res) error {
BaseUID if !obj.Compare(r) {
} return fmt.Errorf("did not compare")
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *VirtRes) UIDs() []ResUID {
x := &VirtUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
// TODO: add more properties here so we can link to vm dependencies
} }
return []ResUID{x} return nil
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *VirtRes) GroupCmp(r Res) bool {
_, ok := r.(*VirtRes)
if !ok {
return false
}
return false // not possible atm
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *VirtRes) Compare(r Res) bool { func (obj *VirtRes) Compare(r engine.Res) bool {
// we can only compare VirtRes to others of the same resource kind // we can only compare VirtRes to others of the same resource kind
res, ok := r.(*VirtRes) res, ok := r.(*VirtRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.URI != res.URI { if obj.URI != res.URI {
return false return false
@@ -1131,6 +1106,21 @@ func (obj *VirtRes) Compare(r Res) bool {
return true return true
} }
// VirtUID is the UID struct for FileRes.
type VirtUID struct {
engine.BaseUID
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *VirtRes) UIDs() []engine.ResUID {
x := &VirtUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
// TODO: add more properties here so we can link to vm dependencies
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *VirtRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *VirtRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -1171,30 +1161,3 @@ func isNotFound(err error) bool {
} }
return false // some other error return false // some other error
} }
// expandHome does an expansion of ~/ or ~james/ into user's home dir value.
func expandHome(p string) (string, error) {
if strings.HasPrefix(p, "~/") {
usr, err := user.Current()
if err != nil {
return p, fmt.Errorf("can't expand ~ into home directory")
}
return path.Join(usr.HomeDir, p[len("~/"):]), nil
}
// check if provided path is in format ~username and keep track of provided username
r, err := regexp.Compile("~([^/]+)/")
if err != nil {
return p, errwrap.Wrapf(err, "can't compile regexp")
}
if match := r.FindStringSubmatch(p); match != nil {
username := match[len(match)-1]
usr, err := user.Lookup(username)
if err != nil {
return p, fmt.Errorf("can't expand %s into home directory", match[0])
}
return path.Join(usr.HomeDir, p[len(match[0]):]), nil
}
return p, nil
}

67
engine/sendrecv.go Normal file
View File

@@ -0,0 +1,67 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
// SendableRes is the interface a resource must implement to support sending
// named parameters. You must specify to the engine what kind of values (and
// with their types) you will be sending. This is used for static type checking.
// Formerly, you had to make sure not to overwrite omitted parameters, otherwise
// it will be as if you've now declared a fixed state for that param. For that
// example, if a parameter `Foo string` had the zero value to mean that it was
// undefined, and you learned that the value is actually `up`, then sending on
// that param would cause that state to be managed, when it was previously not.
// This new interface actually provides a different namespace for sending keys.
type SendableRes interface {
Res // implement everything in Res but add the additional requirements
// Sends returns a struct containing the defaults of the type we send.
Sends() interface{}
// Send is used in CheckApply to send the desired data. It returns an
// error if the data is malformed or doesn't type check.
Send(st interface{}) error
// Sent returns the most recently sent data. This is used by the engine.
Sent() interface{}
}
// RecvableRes is the interface a resource must implement to support receiving
// on public parameters. The resource only has to include the correct trait for
// this interface to be fulfilled, as no additional methods need to be added. To
// get information about received changes, you can use the Recv method from the
// input API that comes in via Init.
type RecvableRes interface {
Res
// SetRecv stores the map of sendable data which should arrive here. It
// is called by the GAPI when building the resource.
SetRecv(recv map[string]*Send)
// Recv is used by the resource to get information on changes. This data
// can be used to invalidate caches, restart watches, or it can be
// ignored entirely.
Recv() map[string]*Send
}
// Send points to a value that a resource will send.
type Send struct {
Res SendableRes // a handle to the resource which is sending a value
Key string // the key in the resource that we're sending
Changed bool // set to true if this key was updated, read only!
}

42
engine/traits/autoedge.go Normal file
View File

@@ -0,0 +1,42 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Edgeable contains a general implementation with some of the properties and
// methods needed to support autoedges on resources. It may be used as a start
// point to avoid re-implementing the straightforward methods.
type Edgeable struct {
meta *engine.AutoEdgeMeta
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// AutoEdgeMeta lets you get or set meta params for the automatic edges trait.
func (obj *Edgeable) AutoEdgeMeta() *engine.AutoEdgeMeta {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = &engine.AutoEdgeMeta{
Disabled: false,
}
}
return obj.meta
}

View File

@@ -0,0 +1,89 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
)
// Groupable contains a general implementation with most of the properties and
// methods needed to support autogrouping on resources. It may be used as a
// starting point to avoid re-implementing the straightforward methods.
type Groupable struct {
meta *engine.AutoGroupMeta
isGrouped bool // am i contained within a group?
grouped []engine.GroupableRes // list of any grouped resources
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// AutoGroupMeta lets you get or set meta params for the automatic grouping
// trait.
func (obj *Groupable) AutoGroupMeta() *engine.AutoGroupMeta {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = &engine.AutoGroupMeta{
Disabled: false,
}
}
return obj.meta
}
// GroupCmp compares two resources and decides if they're suitable for grouping.
// You'll probably want to override this method when implementing a resource...
// This base implementation assumes not, so override me!
func (obj *Groupable) GroupCmp(res engine.GroupableRes) error {
return fmt.Errorf("the default grouping compare is not nil")
}
// GroupRes groups resource argument (res) into self.
func (obj *Groupable) GroupRes(res engine.GroupableRes) error {
if l := len(res.GetGroup()); l > 0 {
return fmt.Errorf("the `%s` resource already contains %d grouped resources", res, l)
}
if res.IsGrouped() {
return fmt.Errorf("the `%s` resource is already grouped", res)
}
obj.grouped = append(obj.grouped, res)
res.SetGrouped(true) // i am contained _in_ a group
return nil
}
// IsGrouped determines if we are grouped.
func (obj *Groupable) IsGrouped() bool { // am I grouped?
return obj.isGrouped
}
// SetGrouped sets a flag to tell if we are grouped.
func (obj *Groupable) SetGrouped(b bool) {
obj.isGrouped = b
}
// GetGroup returns everyone grouped inside me.
func (obj *Groupable) GetGroup() []engine.GroupableRes {
return obj.grouped
}
// SetGroup sets the grouped resources into me.
func (obj *Groupable) SetGroup(grouped []engine.GroupableRes) {
obj.grouped = grouped
}

36
engine/traits/base.go Normal file
View File

@@ -0,0 +1,36 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Base contains all the minimum necessary structs to build a resource. It
// should be used as a starting point to avoid re-implementing the
// straightforward methods.
type Base struct {
Kinded
Named
Meta
}
// String returns a string representation of a resource.
func (obj *Base) String() string {
return engine.Repr(obj.Kind(), obj.Name())
}

39
engine/traits/kind.go Normal file
View File

@@ -0,0 +1,39 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Kinded contains a general implementation of the properties and methods needed
// to support the resource kind. It should be used as a starting point to avoid
// re-implementing the straightforward kind methods.
type Kinded struct {
kind string
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Kind returns the string representation for the kind this resource is.
func (obj *Kinded) Kind() string {
return obj.kind
}
// SetKind sets the kind string for this resource. It must only be set by the
// engine.
func (obj *Kinded) SetKind(kind string) {
obj.kind = kind
}

40
engine/traits/meta.go Normal file
View File

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Meta contains a general implementation of the properties and methods needed
// to support meta parameters. It should be used as a starting point to avoid
// re-implementing the straightforward meta methods.
type Meta struct {
meta *engine.MetaParams
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// MetaParams lets you get or set meta params for this trait.
func (obj *Meta) MetaParams() *engine.MetaParams {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = engine.DefaultMetaParams.Copy()
}
return obj.meta
}

40
engine/traits/named.go Normal file
View File

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Named contains a general implementation of the properties and methods needed
// to support named resources. It should be used as a starting point to avoid
// re-implementing the straightforward name methods.
type Named struct {
name string
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Name returns the unique name this resource has. It is only unique within its
// own kind.
func (obj *Named) Name() string {
return obj.name
}
// SetName sets the unique name for this resource. It must only be unique within
// its own kind.
func (obj *Named) SetName(name string) {
obj.name = name
}

40
engine/traits/refresh.go Normal file
View File

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Refreshable functions as flag storage for resources to signal that they
// support receiving refresh notifications, and what that value is. These are
// commonly used to send information that some aspect of the state is invalid
// due to an unlinked change. The canonical example is a svc resource that needs
// reloading after a configuration file changes.
type Refreshable struct {
refresh bool
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Refresh returns the refresh notification state.
func (obj *Refreshable) Refresh() bool {
return obj.refresh
}
// SetRefresh sets the refresh notification state.
func (obj *Refreshable) SetRefresh(b bool) {
obj.refresh = b
}

75
engine/traits/sendrecv.go Normal file
View File

@@ -0,0 +1,75 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Sendable contains a general implementation with some of the properties and
// methods needed to implement sending from resources. You'll need to implement
// the Sends method, and call the Send method in CheckApply via the Init API.
type Sendable struct {
send interface{}
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Sends returns a struct containing the defaults of the type we send. This
// needs to be implemented (overridden) by the struct with the Sendable trait to
// be able to send any values. The public struct field names are the keys used.
func (obj *Sendable) Sends() interface{} {
return nil
}
// Send is used to send a struct in CheckApply. This is typically wrapped in the
// resource API and consumed that way.
func (obj *Sendable) Send(st interface{}) error {
// TODO: can we (or should we) run the type checking here instead?
obj.send = st
return nil
}
// Sent returns the struct of values that have been sent by this resource.
func (obj *Sendable) Sent() interface{} {
return obj.send
}
// Recvable contains a general implementation with some of the properties and
// methods needed to implement receiving from resources.
type Recvable struct {
recv map[string]*engine.Send
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// SetRecv is used to inject incoming values into the resource.
func (obj *Recvable) SetRecv(recv map[string]*engine.Send) {
//if obj.recv == nil {
// obj.recv = make(map[string]*engine.Send)
//}
obj.recv = recv
}
// Recv is used to get information that was passed in. This data can then be
// used to run the Send/Recv data transfer.
func (obj *Recvable) Recv() map[string]*engine.Send {
return obj.recv
}

41
engine/util.go Normal file
View File

@@ -0,0 +1,41 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"sort"
)
// ResourceSlice is a linear list of resources. It can be sorted.
type ResourceSlice []Res
func (rs ResourceSlice) Len() int { return len(rs) }
func (rs ResourceSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs ResourceSlice) Less(i, j int) bool { return rs[i].String() < rs[j].String() }
// Sort the list of resources and return a copy without modifying the input.
func Sort(rs []Res) []Res {
resources := []Res{}
for _, r := range rs { // copy
resources = append(resources, r)
}
sort.Sort(ResourceSlice(resources))
return resources
// sort.Sort(ResourceSlice(rs)) // this is wrong, it would modify input!
//return rs
}

View File

@@ -15,7 +15,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources package util
import ( import (
"bytes" "bytes"
@@ -24,10 +24,10 @@ import (
"fmt" "fmt"
"os/user" "os/user"
"reflect" "reflect"
"sort"
"strconv" "strconv"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -36,29 +36,18 @@ import (
const ( const (
// StructTag is the key we use in struct field names for key mapping. // StructTag is the key we use in struct field names for key mapping.
StructTag = "lang" StructTag = "lang"
// DBusInterface is the dbus interface that contains genereal methods.
DBusInterface = "org.freedesktop.DBus"
// DBusAddMatch is the dbus method to receive a subset of dbus broadcast
// signals.
DBusAddMatch = DBusInterface + ".AddMatch"
// DBusRemoveMatch is the dbus method to remove a previously defined
// AddMatch rule.
DBusRemoveMatch = DBusInterface + ".RemoveMatch"
) )
// ResourceSlice is a linear list of resources. It can be sorted.
type ResourceSlice []Res
func (rs ResourceSlice) Len() int { return len(rs) }
func (rs ResourceSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs ResourceSlice) Less(i, j int) bool { return rs[i].String() < rs[j].String() }
// Sort the list of resources and return a copy without modifying the input.
func Sort(rs []Res) []Res {
resources := []Res{}
for _, r := range rs { // copy
resources = append(resources, r)
}
sort.Sort(ResourceSlice(resources))
return resources
// sort.Sort(ResourceSlice(rs)) // this is wrong, it would modify input!
//return rs
}
// ResToB64 encodes a resource to a base64 encoded string (after serialization). // ResToB64 encodes a resource to a base64 encoded string (after serialization).
func ResToB64(res Res) (string, error) { func ResToB64(res engine.Res) (string, error) {
b := bytes.Buffer{} b := bytes.Buffer{}
e := gob.NewEncoder(&b) e := gob.NewEncoder(&b)
err := e.Encode(&res) // pass with & err := e.Encode(&res) // pass with &
@@ -69,7 +58,7 @@ func ResToB64(res Res) (string, error) {
} }
// B64ToRes decodes a resource from a base64 encoded string (after deserialization). // B64ToRes decodes a resource from a base64 encoded string (after deserialization).
func B64ToRes(str string) (Res, error) { func B64ToRes(str string) (engine.Res, error) {
var output interface{} var output interface{}
bb, err := base64.StdEncoding.DecodeString(str) bb, err := base64.StdEncoding.DecodeString(str)
if err != nil { if err != nil {
@@ -80,7 +69,7 @@ func B64ToRes(str string) (Res, error) {
if err := d.Decode(&output); err != nil { // pass with & if err := d.Decode(&output); err != nil { // pass with &
return nil, errwrap.Wrapf(err, "gob failed to decode") return nil, errwrap.Wrapf(err, "gob failed to decode")
} }
res, ok := output.(Res) res, ok := output.(engine.Res)
if !ok { if !ok {
return nil, fmt.Errorf("output `%v` is not a Res", output) return nil, fmt.Errorf("output `%v` is not a Res", output)
} }
@@ -89,7 +78,7 @@ func B64ToRes(str string) (Res, error) {
// StructTagToFieldName returns a mapping from recommended alias to actual field // StructTagToFieldName returns a mapping from recommended alias to actual field
// name. It returns an error if it finds a collision. It uses the `lang` tags. // name. It returns an error if it finds a collision. It uses the `lang` tags.
func StructTagToFieldName(res Res) (map[string]string, error) { func StructTagToFieldName(res engine.Res) (map[string]string, error) {
// TODO: fallback to looking up yaml tags, although harder to parse // TODO: fallback to looking up yaml tags, although harder to parse
result := make(map[string]string) // `lang` field tag -> field name result := make(map[string]string) // `lang` field tag -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res st := reflect.TypeOf(res).Elem() // elem for ptr to res
@@ -113,7 +102,7 @@ func StructTagToFieldName(res Res) (map[string]string, error) {
// LowerStructFieldNameToFieldName returns a mapping from the lower case version // LowerStructFieldNameToFieldName returns a mapping from the lower case version
// of each field name to the actual field name. It only returns public fields. // of each field name to the actual field name. It only returns public fields.
// It returns an error if it finds a collision. // It returns an error if it finds a collision.
func LowerStructFieldNameToFieldName(res Res) (map[string]string, error) { func LowerStructFieldNameToFieldName(res engine.Res) (map[string]string, error) {
result := make(map[string]string) // lower field name -> field name result := make(map[string]string) // lower field name -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res st := reflect.TypeOf(res).Elem() // elem for ptr to res
for i := 0; i < st.NumField(); i++ { for i := 0; i < st.NumField(); i++ {
@@ -142,7 +131,7 @@ func LowerStructFieldNameToFieldName(res Res) (map[string]string, error) {
// but this is currently not implemented. // but this is currently not implemented.
// TODO: should this behaviour be changed? // TODO: should this behaviour be changed?
func LangFieldNameToStructFieldName(kind string) (map[string]string, error) { func LangFieldNameToStructFieldName(kind string) (map[string]string, error) {
res, err := NewResource(kind) res, err := engine.NewResource(kind)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -163,7 +152,7 @@ func LangFieldNameToStructFieldName(kind string) (map[string]string, error) {
// StructKindToFieldNameTypeMap returns a map from field name to expected type // StructKindToFieldNameTypeMap returns a map from field name to expected type
// in the lang type system. // in the lang type system.
func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) { func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) {
res, err := NewResource(kind) res, err := engine.NewResource(kind)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -180,7 +169,7 @@ func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) {
field := st.Field(i) field := st.Field(i)
name := field.Name name := field.Name
// TODO: in future, skip over fields that don't have a `lang` tag // TODO: in future, skip over fields that don't have a `lang` tag
//if name == "BaseRes" { // TODO: hack!!! //if name == "Base" { // TODO: hack!!!
// continue // continue
//} //}

107
engine/util/util_test.go Normal file
View File

@@ -0,0 +1,107 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package util
import (
"os/user"
"strconv"
"testing"
)
func TestUnknownGroup(t *testing.T) {
gid, err := GetGID("unknowngroup")
if err == nil {
t.Errorf("expected failure, but passed with: %d", gid)
}
}
func TestUnknownUser(t *testing.T) {
uid, err := GetUID("unknownuser")
if err == nil {
t.Errorf("expected failure, but passed with: %d", uid)
}
}
func TestCurrentUserGroupByName(t *testing.T) {
// get current user
userObj, err := user.Current()
if err != nil {
t.Errorf("error trying to lookup current user: %s", err.Error())
}
currentUID := userObj.Uid
currentGID := userObj.Gid
var uid int
var gid int
// now try to get the uid/gid via our API (via username and group name)
if uid, err = GetUID(userObj.Username); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(uid) != currentUID {
t.Errorf("uid didn't match current user's: %s vs %s", strconv.Itoa(uid), currentUID)
}
// macOS users do not have a group with their name on it, so not assuming this here
group, err := user.LookupGroupId(currentGID)
if err != nil {
t.Errorf("failed to lookup group by id: %s", currentGID)
}
if gid, err = GetGID(group.Name); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(gid) != currentGID {
t.Errorf("gid didn't match current user's: %s vs %s", strconv.Itoa(gid), currentGID)
}
}
func TestCurrentUserGroupById(t *testing.T) {
// get current user
userObj, err := user.Current()
if err != nil {
t.Errorf("error trying to lookup current user: %s", err.Error())
}
currentUID := userObj.Uid
currentGID := userObj.Gid
var uid int
var gid int
// now try to get the uid/gid via our API (via uid and gid)
if uid, err = GetUID(currentUID); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(uid) != currentUID {
t.Errorf("uid didn't match current user's: %s vs %s", strconv.Itoa(uid), currentUID)
}
if gid, err = GetGID(currentGID); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(gid) != currentGID {
t.Errorf("gid didn't match current user's: %s vs %s", strconv.Itoa(gid), currentGID)
}
}

48
engine/world.go Normal file
View File

@@ -0,0 +1,48 @@
// Mgmt
// Copyright (C) 2013-2018+ 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 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"github.com/purpleidea/mgmt/etcd/scheduler"
)
// World is an interface to the rest of the different graph state. It allows
// the GAPI to store state and exchange information throughout the cluster. It
// is the interface each machine uses to communicate with the rest of the world.
type World interface { // TODO: is there a better name for this interface?
ResWatch() chan error
ResExport([]Res) error
// FIXME: should this method take a "filter" data struct instead of many args?
ResCollect(hostnameFilter, kindFilter []string) ([]Res, error)
StrWatch(namespace string) chan error
StrIsNotExist(error) bool
StrGet(namespace string) (string, error)
StrSet(namespace, value string) error
StrDel(namespace string) error
// XXX: add the exchange primitives in here directly?
StrMapWatch(namespace string) chan error
StrMapGet(namespace string) (map[string]string, error)
StrMapSet(namespace, value string) error
StrMapDel(namespace string) error
Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error)
Fs(uri string) (Fs, error)
}

View File

@@ -32,7 +32,7 @@
// * If a seed is given, connect as a client, and optionally volunteer to be a server. // * If a seed is given, connect as a client, and optionally volunteer to be a server.
// * All volunteering clients should listen for a message from the master for nomination. // * All volunteering clients should listen for a message from the master for nomination.
// * If a client has been nominated, it should startup a server. // * If a client has been nominated, it should startup a server.
// * All servers should list for their nomination to be removed and shutdown if so. // * All servers should listen for their nomination to be removed and shutdown if so.
// * The elected leader should decide who to nominate/unnominate to keep the right number of servers. // * The elected leader should decide who to nominate/unnominate to keep the right number of servers.
// //
// Smoke testing: // Smoke testing:
@@ -64,7 +64,7 @@ import (
"time" "time"
"github.com/purpleidea/mgmt/converger" "github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/event" "github.com/purpleidea/mgmt/etcd/event"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
etcd "github.com/coreos/etcd/clientv3" // "clientv3" etcd "github.com/coreos/etcd/clientv3" // "clientv3"
@@ -87,8 +87,10 @@ const (
selfRemoveTimeout = 3 // give unnominated members a chance to self exit selfRemoveTimeout = 3 // give unnominated members a chance to self exit
exitDelay = 3 // number of sec of inactivity after exit to clean up exitDelay = 3 // number of sec of inactivity after exit to clean up
DefaultIdealClusterSize = 5 // default ideal cluster size target for initial seed DefaultIdealClusterSize = 5 // default ideal cluster size target for initial seed
DefaultClientURL = "127.0.0.1:2379"
DefaultServerURL = "127.0.0.1:2380" DefaultClientURL = embed.DefaultListenClientURLs // 127.0.0.1:2379
DefaultServerURL = embed.DefaultListenPeerURLs // 127.0.0.1:2380
// DefaultMaxTxnOps is the maximum number of operations to run in a // DefaultMaxTxnOps is the maximum number of operations to run in a
// single etcd transaction. If you exceed this limit, it is possible // single etcd transaction. If you exceed this limit, it is possible
// that you have either an extremely large code base, or that you have // that you have either an extremely large code base, or that you have
@@ -262,8 +264,11 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient
// TODO: add some sort of auto assign method for picking these defaults // TODO: add some sort of auto assign method for picking these defaults
// add a default so that our local client can connect locally if needed // add a default so that our local client can connect locally if needed
if len(obj.LocalhostClientURLs()) == 0 { // if we don't have any localhost URLs if len(obj.LocalhostClientURLs()) == 0 { // if we don't have any localhost URLs
u := url.URL{Scheme: "http", Host: DefaultClientURL} // default u, err := url.Parse(DefaultClientURL)
obj.clientURLs = append([]url.URL{u}, obj.clientURLs...) // prepend if err != nil {
return nil // TODO: change interface to return an error
}
obj.clientURLs = append([]url.URL{*u}, obj.clientURLs...) // prepend
} }
// add a default for local use and testing, harmless and useful! // add a default for local use and testing, harmless and useful!
@@ -271,8 +276,18 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient
if len(obj.endpoints) > 0 { if len(obj.endpoints) > 0 {
obj.noServer = true // we didn't have enough to be a server obj.noServer = true // we didn't have enough to be a server
} }
u := url.URL{Scheme: "http", Host: DefaultServerURL} // default u, err := url.Parse(DefaultServerURL) // default
obj.serverURLs = []url.URL{u} if err != nil {
return nil // TODO: change interface to return an error
}
obj.serverURLs = []url.URL{*u}
}
if converger != nil {
converger.AddStateFn("etcd-hostname", func(converged bool) error {
// send our individual state into etcd for others to see
return SetHostnameConverged(obj, hostname, converged) // TODO: what should happen on error?
})
} }
return obj return obj

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package etcd package etcd
import ( import (

View File

@@ -22,54 +22,10 @@ import (
"fmt" "fmt"
) )
//go:generate stringer -type=Kind -output=kind_stringer.go
// Kind represents the type of event being passed.
type Kind int
// The different event kinds are used in different contexts.
const (
EventNil Kind = iota
EventExit
EventStart
EventPause
EventPoke
EventBackPoke
)
// Resp is a channel to be used for boolean responses. A nil represents an ACK, // Resp is a channel to be used for boolean responses. A nil represents an ACK,
// and a non-nil represents a NACK (false). This also lets us use custom errors. // and a non-nil represents a NACK (false). This also lets us use custom errors.
type Resp chan error type Resp chan error
// Event is the main struct that stores event information and responses.
type Event struct {
Kind Kind
Resp Resp // channel to send an ack response on, nil to skip
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
Err error // store an error in our event
}
// ACK sends a single acknowledgement on the channel if one was requested.
func (event *Event) ACK() {
if event.Resp != nil { // if they've requested an ACK
event.Resp.ACK()
}
}
// NACK sends a negative acknowledgement message on the channel if one was requested.
func (event *Event) NACK() {
if event.Resp != nil { // if they've requested a NACK
event.Resp.NACK()
}
}
// ACKNACK sends a custom ACK or NACK message on the channel if one was requested.
func (event *Event) ACKNACK(err error) {
if event.Resp != nil { // if they've requested a NACK
event.Resp.ACKNACK(err)
}
}
// NewResp is just a helper to return the right type of response channel. // NewResp is just a helper to return the right type of response channel.
func NewResp() Resp { func NewResp() Resp {
resp := make(chan error) resp := make(chan error)
@@ -112,8 +68,3 @@ func (resp Resp) ACKWait() {
} }
} }
} }
// Error returns the stored error value.
func (event *Event) Error() error {
return event.Err
}

View File

@@ -15,6 +15,8 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !root
package fs_test // named this way to make it easier for examples package fs_test // named this way to make it easier for examples
import ( import (

View File

@@ -22,7 +22,8 @@ import (
"log" "log"
"strings" "strings"
"github.com/purpleidea/mgmt/resources" "github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
etcd "github.com/coreos/etcd/clientv3" etcd "github.com/coreos/etcd/clientv3"
@@ -60,7 +61,7 @@ func WatchResources(obj *EmbdEtcd) chan error {
} }
// SetResources exports all of the resources which we pass in to etcd. // SetResources exports all of the resources which we pass in to etcd.
func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res) error { func SetResources(obj *EmbdEtcd, hostname string, resourceList []engine.Res) error {
// key structure is $NS/exported/$hostname/resources/$uid = $data // key structure is $NS/exported/$hostname/resources/$uid = $data
var kindFilter []string // empty to get from everyone var kindFilter []string // empty to get from everyone
@@ -79,12 +80,12 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
ifs := []etcd.Cmp{} // list matching the desired state ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this transaction ops := []etcd.Op{} // list of ops in this transaction
for _, res := range resourceList { for _, res := range resourceList {
if res.GetKind() == "" { if res.Kind() == "" {
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName()) log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.Name())
} }
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName()) uid := fmt.Sprintf("%s/%s", res.Kind(), res.Name())
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid) path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
if data, err := resources.ResToB64(res); err == nil { if data, err := engineUtil.ResToB64(res); err == nil {
ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state
ops = append(ops, etcd.OpPut(path, data)) ops = append(ops, etcd.OpPut(path, data))
} else { } else {
@@ -92,9 +93,9 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
} }
} }
match := func(res resources.Res, resourceList []resources.Res) bool { // helper lambda match := func(res engine.Res, resourceList []engine.Res) bool { // helper lambda
for _, x := range resourceList { for _, x := range resourceList {
if res.GetKind() == x.GetKind() && res.GetName() == x.GetName() { if res.Kind() == x.Kind() && res.Name() == x.Name() {
return true return true
} }
} }
@@ -104,10 +105,10 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
hasDeletes := false hasDeletes := false
// delete old, now unused resources here... // delete old, now unused resources here...
for _, res := range originals { for _, res := range originals {
if res.GetKind() == "" { if res.Kind() == "" {
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName()) log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.Name())
} }
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName()) uid := fmt.Sprintf("%s/%s", res.Kind(), res.Name())
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid) path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
if match(res, resourceList) { // if we match, no need to delete! if match(res, resourceList) { // if we match, no need to delete!
@@ -135,10 +136,10 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
// TODO: Expand this with a more powerful filter based on what we eventually // TODO: Expand this with a more powerful filter based on what we eventually
// support in our collect DSL. Ideally a server side filter like WithFilter() // support in our collect DSL. Ideally a server side filter like WithFilter()
// We could do this if the pattern was $NS/exported/$kind/$hostname/$uid = $data. // We could do this if the pattern was $NS/exported/$kind/$hostname/$uid = $data.
func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resources.Res, error) { func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]engine.Res, error) {
// key structure is $NS/exported/$hostname/resources/$uid = $data // key structure is $NS/exported/$hostname/resources/$uid = $data
path := fmt.Sprintf("%s/exported/", NS) path := fmt.Sprintf("%s/exported/", NS)
resourceList := []resources.Res{} resourceList := []engine.Res{}
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend)) keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
if err != nil { if err != nil {
return nil, fmt.Errorf("could not get resources: %v", err) return nil, fmt.Errorf("could not get resources: %v", err)
@@ -170,7 +171,7 @@ func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resourc
continue continue
} }
if obj, err := resources.B64ToRes(val); err == nil { if obj, err := engineUtil.B64ToRes(val); err == nil {
log.Printf("Etcd: Get: (Hostname, Kind, Name): (%s, %s, %s)", hostname, kind, name) log.Printf("Etcd: Get: (Hostname, Kind, Name): (%s, %s, %s)", hostname, kind, name)
resourceList = append(resourceList, obj) resourceList = append(resourceList, obj)
} else { } else {

View File

@@ -22,9 +22,9 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
etcdfs "github.com/purpleidea/mgmt/etcd/fs" etcdfs "github.com/purpleidea/mgmt/etcd/fs"
"github.com/purpleidea/mgmt/etcd/scheduler" "github.com/purpleidea/mgmt/etcd/scheduler"
"github.com/purpleidea/mgmt/resources"
) )
// World is an etcd backed implementation of the World interface. // World is an etcd backed implementation of the World interface.
@@ -33,7 +33,7 @@ type World struct {
EmbdEtcd *EmbdEtcd EmbdEtcd *EmbdEtcd
MetadataPrefix string // expected metadata prefix MetadataPrefix string // expected metadata prefix
StoragePrefix string // storage prefix for etcdfs storage StoragePrefix string // storage prefix for etcdfs storage
StandaloneFs resources.Fs // store an fs here for local usage StandaloneFs engine.Fs // store an fs here for local usage
Debug bool Debug bool
Logf func(format string, v ...interface{}) Logf func(format string, v ...interface{})
} }
@@ -46,13 +46,13 @@ func (obj *World) ResWatch() chan error {
// ResExport exports a list of resources under our hostname namespace. // ResExport exports a list of resources under our hostname namespace.
// Subsequent calls replace the previously set collection atomically. // Subsequent calls replace the previously set collection atomically.
func (obj *World) ResExport(resourceList []resources.Res) error { func (obj *World) ResExport(resourceList []engine.Res) error {
return SetResources(obj.EmbdEtcd, obj.Hostname, resourceList) return SetResources(obj.EmbdEtcd, obj.Hostname, resourceList)
} }
// ResCollect gets the collection of exported resources which match the filter. // ResCollect gets the collection of exported resources which match the filter.
// It does this atomically so that a call always returns a complete collection. // It does this atomically so that a call always returns a complete collection.
func (obj *World) ResCollect(hostnameFilter, kindFilter []string) ([]resources.Res, error) { func (obj *World) ResCollect(hostnameFilter, kindFilter []string) ([]engine.Res, error) {
// XXX: should we be restricted to retrieving resources that were // XXX: should we be restricted to retrieving resources that were
// exported with a tag that allows or restricts our hostname? We could // exported with a tag that allows or restricts our hostname? We could
// enforce that here if the underlying API supported it... Add this? // enforce that here if the underlying API supported it... Add this?
@@ -122,7 +122,7 @@ func (obj *World) Scheduler(namespace string, opts ...scheduler.Option) (*schedu
// execution that doesn't span more than a single host, this file system might // execution that doesn't span more than a single host, this file system might
// actually be a local or memory backed file system, so actually only // actually be a local or memory backed file system, so actually only
// distributed within the boredom that is a single host cluster. // distributed within the boredom that is a single host cluster.
func (obj *World) Fs(uri string) (resources.Fs, error) { func (obj *World) Fs(uri string) (engine.Fs, error) {
u, err := url.Parse(uri) u, err := url.Parse(uri)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,6 @@
docker:container "mgmt-nginx" {
state => "running",
image => "nginx",
cmd => ["nginx", "-g", "daemon off;",],
ports => {"tcp" => {80 => 8080,},},
}

5
examples/lang/mount0.mcl Normal file
View File

@@ -0,0 +1,5 @@
mount "/mnt/foo" {
state => "exists",
device => "/dev/sdb1",
type => "ext4",
}

5
examples/lang/mount1.mcl Normal file
View File

@@ -0,0 +1,5 @@
mount "/mnt/foo" {
state => "exists",
device => "UUID=00112233-4455-6677-8899-aabbccddeeff",
type => "ext4",
}

13
examples/lang/mount2.mcl Normal file
View File

@@ -0,0 +1,13 @@
mount "/media/cdrom" {
state => "exists",
device => "/dev/cdrom",
type => "iso9660",
options => {
"ro"=>"",
"relatime"=>"",
"nojoliet"=>"",
"check"=>"s",
"map"=>"n",
"blocksize"=>"2048",
},
}

5
examples/lang/net1.mcl Normal file
View File

@@ -0,0 +1,5 @@
net "eth0" {
state => "up",
addrs => ["192.168.42.13/24",],
gateway => "192.168.42.1",
}

3
examples/lang/net2.mcl Normal file
View File

@@ -0,0 +1,3 @@
net "eth0" {
state => "up",
}

3
examples/lang/net3.mcl Normal file
View File

@@ -0,0 +1,3 @@
net "eth0" {
state => "down",
}

View File

@@ -10,10 +10,11 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib" mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -49,7 +50,7 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *MyGAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) { if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" { if s != "" {
return nil, fmt.Errorf("input is not empty") return nil, fmt.Errorf("input is not empty")
@@ -103,66 +104,46 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err return nil, err
} }
metaparams := resources.DefaultMetaParams
exec1 := &resources.ExecRes{ exec1 := &resources.ExecRes{
BaseRes: resources.BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: metaparams,
},
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
Shell: "/bin/bash", Shell: "/bin/bash",
} }
g.AddVertex(exec1) g.AddVertex(exec1)
output := &resources.FileRes{ output := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "output",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
"Content": {Res: exec1, Key: "Output"},
},
},
Path: "/tmp/mgmt/output", Path: "/tmp/mgmt/output",
State: "present", State: "present",
} }
// XXX: add send->recv!
//Recv: map[string]*engine.Send{
// "Content": {Res: exec1, Key: "Output"},
//},
g.AddVertex(output) g.AddVertex(output)
g.AddEdge(exec1, output, &resources.Edge{Name: "e0"}) g.AddEdge(exec1, output, &engine.Edge{Name: "e0"})
stdout := &resources.FileRes{ stdout := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "stdout",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
"Content": {Res: exec1, Key: "Stdout"},
},
},
Path: "/tmp/mgmt/stdout", Path: "/tmp/mgmt/stdout",
State: "present", State: "present",
} }
// XXX: add send->recv!
//Recv: map[string]*engine.Send{
// "Content": {Res: exec1, Key: "Stdout"},
//},
g.AddVertex(stdout) g.AddVertex(stdout)
g.AddEdge(exec1, stdout, &resources.Edge{Name: "e1"}) g.AddEdge(exec1, stdout, &engine.Edge{Name: "e1"})
stderr := &resources.FileRes{ stderr := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "stderr",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
"Content": {Res: exec1, Key: "Stderr"},
},
},
Path: "/tmp/mgmt/stderr", Path: "/tmp/mgmt/stderr",
State: "present", State: "present",
} }
// XXX: add send->recv!
//Recv: map[string]*engine.Send{
// "Content": {Res: exec1, Key: "Stderr"},
//},
g.AddVertex(stderr) g.AddVertex(stderr)
g.AddEdge(exec1, stderr, &resources.Edge{Name: "e2"}) g.AddEdge(exec1, stderr, &engine.Edge{Name: "e2"})
//g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop) //g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop)
return g, nil return g, nil

View File

@@ -10,10 +10,11 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib" mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
"github.com/urfave/cli" "github.com/urfave/cli"
@@ -54,7 +55,7 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *MyGAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) { if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" { if s != "" {
return nil, fmt.Errorf("input is not empty") return nil, fmt.Errorf("input is not empty")
@@ -103,27 +104,13 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) {
return nil, err return nil, err
} }
metaparams := resources.DefaultMetaParams
f1 := &resources.FileRes{ f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/sub1", Path: "/tmp/mgmt/sub1",
State: "present", State: "present",
} }
g.AddVertex(f1) g.AddVertex(f1)
n1 := &resources.NoopRes{ n1 := &resources.NoopRes{}
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
g.AddVertex(n1) g.AddVertex(n1)
return g, nil return g, nil
@@ -140,14 +127,8 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err return nil, err
} }
metaparams := resources.DefaultMetaParams
content := "I created a subgraph!\n" content := "I created a subgraph!\n"
f0 := &resources.FileRes{ f0 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "README",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/README", Path: "/tmp/mgmt/README",
Content: &content, Content: &content,
State: "present", State: "present",
@@ -160,7 +141,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
} }
edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge { edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge {
edge := &resources.Edge{ edge := &engine.Edge{
Name: fmt.Sprintf("edge: %s->%s", v1, v2), Name: fmt.Sprintf("edge: %s->%s", v1, v2),
} }

View File

@@ -10,10 +10,11 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib" mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -49,7 +50,7 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *MyGAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) { if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" { if s != "" {
return nil, fmt.Errorf("input is not empty") return nil, fmt.Errorf("input is not empty")
@@ -103,15 +104,8 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err return nil, err
} }
metaparams := resources.DefaultMetaParams
content := "I created a subgraph!\n" content := "I created a subgraph!\n"
f0 := &resources.FileRes{ f0 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "README",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/README", Path: "/tmp/mgmt/README",
Content: &content, Content: &content,
State: "present", State: "present",
@@ -126,40 +120,24 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
// add elements into the sub graph // add elements into the sub graph
f1 := &resources.FileRes{ f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/sub1", Path: "/tmp/mgmt/sub1",
State: "present", State: "present",
} }
subGraph.AddVertex(f1) subGraph.AddVertex(f1)
n1 := &resources.NoopRes{ n1 := &resources.NoopRes{}
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
subGraph.AddVertex(n1) subGraph.AddVertex(n1)
e0 := &resources.Edge{Name: "e0"} e0 := &engine.Edge{Name: "e0"}
e0.Notify = true // send a notification from v0 to v1 e0.Notify = true // send a notification from v0 to v1
subGraph.AddEdge(f1, n1, e0) subGraph.AddEdge(f1, n1, e0)
// create the actual resource to hold the sub graph // create the actual resource to hold the sub graph
subGraphRes0 := &resources.GraphRes{ // TODO: should we name this SubGraphRes ? //subGraphRes0 := &resources.GraphRes{ // TODO: should we name this SubGraphRes ?
BaseRes: resources.BaseRes{ // Graph: subGraph,
Name: "subgraph1", //}
Kind: "graph", //g.AddVertex(subGraphRes0) // add it to the main graph
MetaParams: metaparams,
},
Graph: subGraph,
}
g.AddVertex(subGraphRes0) // add it to the main graph
//g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop) //g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop)
return g, nil return g, nil

View File

@@ -1,246 +0,0 @@
// libmgmt example
package main
import (
"fmt"
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/purpleidea/mgmt/yamlgraph"
"github.com/urfave/cli"
)
// XXX: this has not been updated to latest GAPI/Deploy API. Patches welcome!
const (
// Name is the name of this frontend.
Name = "libmgmt"
)
// MyGAPI implements the main GAPI interface.
type MyGAPI struct {
Name string // graph name
Interval uint // refresh interval, 0 to never refresh
data gapi.Data
initialized bool
closeChan chan struct{}
wg sync.WaitGroup // sync group for tunnel go routines
}
// NewMyGAPI creates a new MyGAPI struct and calls Init().
func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
obj := &MyGAPI{
Name: name,
Interval: interval,
}
return obj, obj.Init(data)
}
// Cli takes a cli.Context, and returns our GAPI if activated. All arguments
// should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" {
return nil, fmt.Errorf("input is not empty")
}
return &gapi.Deploy{
Name: obj.Name,
Noop: c.GlobalBool("noop"),
Sema: c.GlobalInt("sema"),
GAPI: &MyGAPI{
// TODO: add properties here...
},
}, nil
}
return nil, nil // we weren't activated!
}
// CliFlags returns a list of flags used by this deploy subcommand.
func (obj *MyGAPI) CliFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: obj.Name,
Value: "",
Usage: "run",
},
}
}
// Init initializes the MyGAPI struct.
func (obj *MyGAPI) Init(data gapi.Data) error {
if obj.initialized {
return fmt.Errorf("already initialized")
}
if obj.Name == "" {
return fmt.Errorf("the graph name must be specified")
}
obj.data = data // store for later
obj.closeChan = make(chan struct{})
obj.initialized = true
return nil
}
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
n1, err := resources.NewNamedResource("noop", "noop1")
if err != nil {
return nil, err
}
// NOTE: This is considered the legacy method to build graphs. Avoid
// importing the legacy `yamlgraph` lib if possible for custom graphs.
// we can still build a graph via the yaml method
gc := &yamlgraph.GraphConfig{
Graph: obj.Name,
Resources: yamlgraph.Resources{ // must redefine anonymous struct :(
// in alphabetical order
Exec: []*resources.ExecRes{},
File: []*resources.FileRes{},
Msg: []*resources.MsgRes{},
Noop: []*resources.NoopRes{n1.(*resources.NoopRes)},
Pkg: []*resources.PkgRes{},
Svc: []*resources.SvcRes{},
Timer: []*resources.TimerRes{},
Virt: []*resources.VirtRes{},
},
//Collector: []collectorResConfig{},
//Edges: []Edge{},
Comment: "comment!",
}
g, err := gc.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop)
return g, err
}
// Next returns nil errors every time there could be a new graph.
func (obj *MyGAPI) Next() chan gapi.Next {
ch := make(chan gapi.Next)
obj.wg.Add(1)
go func() {
defer obj.wg.Done()
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
}
startChan := make(chan struct{}) // start signal
close(startChan) // kick it off!
ticker := make(<-chan time.Time)
if obj.data.NoStreamWatch || obj.Interval <= 0 {
ticker = nil
} else {
// arbitrarily change graph every interval seconds
t := time.NewTicker(time.Duration(obj.Interval) * time.Second)
defer t.Stop()
ticker = t.C
}
for {
select {
case <-startChan: // kick the loop once at start
startChan = nil // disable
// pass
case <-ticker:
// pass
case <-obj.closeChan:
return
}
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
return
}
}
}()
return ch
}
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
obj.initialized = false // closed = true
return nil
}
// Run runs an embedded mgmt server.
func Run() error {
obj := &mgmt.Main{}
obj.Program = "libmgmt" // TODO: set on compilation
obj.Version = "0.0.1" // TODO: set on compilation
obj.TmpPrefix = true
obj.IdealClusterSize = -1
obj.ConvergedTimeout = -1
obj.Noop = true
//obj.GAPI = &MyGAPI{ // graph API
// Name: "libmgmt", // TODO: set on compilation
// Interval: 15, // arbitrarily change graph every 15 seconds
//}
if err := obj.Init(); err != nil {
return err
}
// install the exit signal handler
exit := make(chan struct{})
defer close(exit)
go func() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM)
select {
case sig := <-signals: // any signal will do
if sig == os.Interrupt {
log.Println("Interrupted by ^C")
obj.Exit(nil)
return
}
log.Println("Interrupted by signal")
obj.Exit(fmt.Errorf("killed by %v", sig))
return
case <-exit:
return
}
}()
return obj.Run()
}
func main() {
log.Printf("Hello!")
if err := Run(); err != nil {
fmt.Println(err)
os.Exit(1)
return
}
log.Printf("Goodbye!")
}

Some files were not shown because too many files have changed in this diff Show More