Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ec00fe57f | ||
|
|
80931e1cb4 | ||
|
|
cc02e96a13 | ||
|
|
51ec91dd16 | ||
|
|
916a92c3d8 | ||
|
|
5431bfdc29 | ||
|
|
43b5b4f5a4 | ||
|
|
f342e06ef0 | ||
|
|
81bb87f4cd | ||
|
|
c4b97fadcc | ||
|
|
05f6ba7297 | ||
|
|
c62b8a5d4f | ||
|
|
83dab30ecf | ||
|
|
24b08a332d | ||
|
|
70ccb3022a | ||
|
|
8019b90b8a | ||
|
|
5f12ff6178 | ||
|
|
6e20e48489 | ||
|
|
f29a72235c | ||
|
|
e25d499eeb | ||
|
|
9cae339546 | ||
|
|
a049af6262 | ||
|
|
a402f50f9b | ||
|
|
9f89ea9be6 | ||
|
|
e538aacf9d | ||
|
|
968c609697 | ||
|
|
c11cfa0a62 | ||
|
|
074f4677d5 | ||
|
|
9ea5c03371 | ||
|
|
22c0ff3cf5 | ||
|
|
3ced981d28 | ||
|
|
299080f590 | ||
|
|
a407771eaf | ||
|
|
d26a6de759 | ||
|
|
9baad56197 | ||
|
|
a589e2ecf3 | ||
|
|
d7029871b1 | ||
|
|
b80a505be5 | ||
|
|
412a25462e | ||
|
|
9a8408a092 | ||
|
|
86a9181e9b | ||
|
|
9969286224 | ||
|
|
ef49aa7e08 | ||
|
|
acdb497b80 | ||
|
|
4d8faeb826 | ||
|
|
6e0dfdb16f | ||
|
|
754480a9b6 | ||
|
|
15681ddca9 | ||
|
|
3c8d424a43 | ||
|
|
7d7eb3d1cd | ||
|
|
8500339ba6 | ||
|
|
06ee05026b | ||
|
|
ddefb4e987 | ||
|
|
62d1fc7ed3 | ||
|
|
f3b99b3940 | ||
|
|
97c11c18d0 | ||
|
|
93a909551f | ||
|
|
ea52eb78d9 | ||
|
|
fdd698dade | ||
|
|
173ccf6861 | ||
|
|
a5c3db6303 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -13,4 +13,4 @@ mgmt.static
|
||||
build/mgmt-*
|
||||
mgmt.iml
|
||||
rpmbuild/
|
||||
*.deb
|
||||
releases/
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -22,3 +22,12 @@
|
||||
[submodule "vendor/github.com/ugorji/go"]
|
||||
path = vendor/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
|
||||
|
||||
@@ -8,6 +8,9 @@ go:
|
||||
go_import_path: github.com/purpleidea/mgmt
|
||||
sudo: true
|
||||
dist: trusty
|
||||
# travis requires that you update manually, and provides this key to trigger it
|
||||
apt:
|
||||
update: true
|
||||
before_install:
|
||||
# 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
|
||||
|
||||
124
Makefile
124
Makefile
@@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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
|
||||
|
||||
# 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))
|
||||
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
|
||||
PKGNAME := $(shell go list .)
|
||||
ifeq ($(VERSION),$(SVERSION))
|
||||
RELEASE = 1
|
||||
else
|
||||
@@ -47,12 +48,19 @@ GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
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
|
||||
|
||||
#
|
||||
# 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:
|
||||
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
|
||||
|
||||
# show the current version
|
||||
version:
|
||||
version: ## show the current version
|
||||
@echo $(VERSION)
|
||||
|
||||
program:
|
||||
program: ## show the program name
|
||||
@echo $(PROGRAM)
|
||||
|
||||
path:
|
||||
path: ## create working paths
|
||||
./misc/make-path.sh
|
||||
|
||||
deps:
|
||||
deps: ## install system and golang dependencies
|
||||
./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)"
|
||||
|
||||
# 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)"
|
||||
|
||||
# generate go files from non-go source
|
||||
bindata:
|
||||
bindata: ## generate go files from non-go sources
|
||||
@echo "Generating: bindata..."
|
||||
$(MAKE) --quiet -C bindata
|
||||
|
||||
generate:
|
||||
go generate
|
||||
|
||||
lang:
|
||||
lang: ## generates the lexer/parser for the language frontend
|
||||
@# recursively run make in child dir named lang
|
||||
@echo "Generating: lang..."
|
||||
$(MAKE) --quiet -C lang
|
||||
|
||||
# build a `mgmt` binary for current host os/arch
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH}
|
||||
cp $< $@
|
||||
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
|
||||
cp -a $< $@
|
||||
|
||||
$(PROGRAM).static: $(GO_FILES)
|
||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||
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);
|
||||
|
||||
build: LDFLAGS=-s -w
|
||||
build: LDFLAGS=-s -w ## build a fresh mgmt binary
|
||||
build: $(PROGRAM)
|
||||
|
||||
build-debug: LDFLAGS=
|
||||
@@ -142,13 +150,18 @@ GOARCH=$(lastword $(subst -, ,$*))
|
||||
build/mgmt-%: $(GO_FILES) | bindata lang
|
||||
@echo "Building: $(PROGRAM), os/arch: $*, version: $(SVERSION)..."
|
||||
@# 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
|
||||
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
|
||||
crossbuild: ${crossbuild_targets}
|
||||
|
||||
clean:
|
||||
clean: ## clean things up
|
||||
$(MAKE) --quiet -C bindata clean
|
||||
$(MAKE) --quiet -C lang clean
|
||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||
@@ -157,7 +170,7 @@ clean:
|
||||
# crossbuild artifacts
|
||||
rm -f build/mgmt-*
|
||||
|
||||
test: build
|
||||
test: build ## run tests
|
||||
./test.sh
|
||||
|
||||
# create all test targets for make tab completion (eg: make test-gofmt)
|
||||
@@ -179,9 +192,9 @@ gofmt:
|
||||
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" \;
|
||||
|
||||
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
|
||||
pandoc docs/documentation.md -o docs/'$(PROGRAM)-documentation.pdf'
|
||||
@@ -206,7 +219,7 @@ rpmbuild/SOURCES/: tar
|
||||
rpmbuild/SRPMS/: srpm
|
||||
rpmbuild/RPMS/: rpm
|
||||
|
||||
upload: upload-sources upload-srpms upload-rpms
|
||||
upload: upload-sources upload-srpms upload-rpms ## upload sources
|
||||
# do nothing
|
||||
|
||||
#
|
||||
@@ -314,30 +327,83 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
|
||||
#
|
||||
# copr build
|
||||
#
|
||||
copr: upload-srpms
|
||||
copr: upload-srpms ## build in copr
|
||||
./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
|
||||
dpkg-buildpackage
|
||||
# especially when building in Docker container, pull build artifact in project directory.
|
||||
cp ../mgmt_*_amd64.deb ./
|
||||
# cleanup
|
||||
rm -rf debian/mgmt/
|
||||
#
|
||||
# release
|
||||
#
|
||||
release: releases/$(VERSION)/mgmt-release.url ## generates and uploads a release
|
||||
|
||||
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 run -td --name mgmt-build purpleidea/mgmt-build
|
||||
docker cp mgmt-build:/root/gopath/src/github.com/purpleidea/mgmt/mgmt .
|
||||
docker build -t purpleidea/mgmt -f docker/Dockerfile.static .
|
||||
docker rm mgmt-build || true
|
||||
|
||||
clean_container:
|
||||
clean_container: ## removes the container
|
||||
docker rmi purpleidea/mgmt-build
|
||||
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
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://www.patreon.com/purpleidea)
|
||||
[](https://liberapay.com/purpleidea/donate)
|
||||
|
||||
## Community:
|
||||
|
||||
@@ -17,6 +18,8 @@ Come join us in the `mgmt` community!
|
||||
| IRC | [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode |
|
||||
| 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) |
|
||||
| Patreon | [purpleidea](https://www.patreon.com/purpleidea) on Patreon |
|
||||
| Liberapay | [purpleidea](https://liberapay.com/purpleidea/donate) on Liberapay |
|
||||
|
||||
## Status:
|
||||
|
||||
|
||||
4
Vagrantfile
vendored
4
Vagrantfile
vendored
@@ -6,7 +6,7 @@ Vagrant.configure(2) do |config|
|
||||
config.vm.synced_folder ".", "/vagrant", disabled: true
|
||||
|
||||
config.vm.define "mgmt-dev" do |instance|
|
||||
instance.vm.box = "fedora/26-cloud-base"
|
||||
instance.vm.box = "fedora/28-cloud-base"
|
||||
end
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
@@ -24,7 +24,7 @@ Vagrant.configure(2) do |config|
|
||||
config.vm.provision "file", source: "~/.gitconfig", destination: ".gitconfig"
|
||||
|
||||
# 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
|
||||
config.vm.provision "shell" do |shell|
|
||||
|
||||
@@ -20,10 +20,13 @@ package converger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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
|
||||
@@ -40,8 +43,9 @@ type Converger interface { // TODO: need a better name
|
||||
Loop(bool)
|
||||
ConvergedTimer(UID) <-chan time.Time
|
||||
Status() map[uint64]bool
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
SetStateFn(func(bool) error) // sets the stateFn
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
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
|
||||
@@ -63,14 +67,15 @@ type UID interface {
|
||||
|
||||
// converger is an implementation of the Converger interface.
|
||||
type converger struct {
|
||||
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)
|
||||
channel chan struct{} // signal here to run an isConverged check
|
||||
control chan bool // control channel for start/pause
|
||||
mutex sync.RWMutex // used for controlling access to status and lastid
|
||||
timeout int // must be zero (instant) or greater seconds to run
|
||||
converged bool // did we converge (state changes of this run Fn)
|
||||
channel chan struct{} // signal here to run an isConverged check
|
||||
control chan bool // control channel for start/pause
|
||||
mutex *sync.RWMutex // used for controlling access to status and lastid
|
||||
lastid uint64
|
||||
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.
|
||||
@@ -78,21 +83,23 @@ type cuid struct {
|
||||
converger Converger
|
||||
id uint64
|
||||
name string // user defined, friendly name
|
||||
mutex sync.Mutex
|
||||
mutex *sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the above timer running?
|
||||
wg sync.WaitGroup
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
// NewConverger builds a new converger struct.
|
||||
func NewConverger(timeout int, stateFn func(bool) error) Converger {
|
||||
func NewConverger(timeout int) Converger {
|
||||
return &converger{
|
||||
timeout: timeout,
|
||||
stateFn: stateFn,
|
||||
channel: make(chan struct{}),
|
||||
control: make(chan bool),
|
||||
lastid: 0,
|
||||
status: make(map[uint64]bool),
|
||||
timeout: timeout,
|
||||
channel: make(chan struct{}),
|
||||
control: make(chan bool),
|
||||
mutex: &sync.RWMutex{},
|
||||
lastid: 0,
|
||||
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,
|
||||
id: obj.lastid,
|
||||
name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||
mutex: &sync.Mutex{},
|
||||
timer: nil,
|
||||
running: false,
|
||||
wg: &sync.WaitGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,11 +225,9 @@ func (obj *converger) Loop(startPaused bool) {
|
||||
case <-obj.channel:
|
||||
if !obj.isConverged() {
|
||||
if obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(false); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
// call the arbitrary functions (takes a read lock!)
|
||||
if err := obj.runStateFns(false); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
obj.converged = false
|
||||
@@ -230,11 +237,9 @@ func (obj *converger) Loop(startPaused bool) {
|
||||
// we have converged!
|
||||
if obj.timeout >= 0 { // only run if timeout is valid
|
||||
if !obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(true); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
// call the arbitrary functions (takes a read lock!)
|
||||
if err := obj.runStateFns(true); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -275,9 +280,46 @@ func (obj *converger) Timeout() int {
|
||||
return obj.timeout
|
||||
}
|
||||
|
||||
// SetStateFn sets the state function to be run on change of converged state.
|
||||
func (obj *converger) SetStateFn(stateFn func(bool) error) {
|
||||
obj.stateFn = stateFn
|
||||
// AddStateFn adds a state function to be run on change of converged state.
|
||||
func (obj *converger) AddStateFn(name string, stateFn func(bool) error) error {
|
||||
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.
|
||||
|
||||
@@ -351,18 +351,28 @@ GOTAGS=novirt make build
|
||||
|
||||
#### 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
|
||||
```
|
||||
|
||||
#### 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
|
||||
|
||||
You can combine multiple tags by using a space-separated list:
|
||||
|
||||
```
|
||||
GOTAGS="noaugeas novirt" make build
|
||||
GOTAGS="noaugeas novirt nodocker" make build
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -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:
|
||||
|
||||
- **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`
|
||||
|
||||
- **if**: produces up to one branch of statements based on a conditional
|
||||
expression
|
||||
|
||||
@@ -114,6 +115,31 @@ expression
|
||||
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
|
||||
`edges` and `resources`. A resource statement can produce a resource, whereas an
|
||||
`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
|
||||
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
|
||||
|
||||
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
|
||||
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?
|
||||
|
||||
Yes, the language is just one of the available "frontends" that passes a stream
|
||||
|
||||
@@ -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) |
|
||||
| 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) |
|
||||
| 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 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) |
|
||||
@@ -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 | 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) |
|
||||
| Jonathan Gold | blog | [Go Netlink and Select](https://jonathangold.ca/blog/go-netlink-and-select/) |
|
||||
|
||||
@@ -119,8 +119,11 @@ To build `mgmt` without augeas support please run:
|
||||
To build `mgmt` without libvirt support please run:
|
||||
`GOTAGS='novirt' make build`
|
||||
|
||||
To build `mgmt` without augeas or libvirt support please run:
|
||||
`GOTAGS='noaugeas novirt' make build`
|
||||
To build `mgmt` without docker support please run:
|
||||
`GOTAGS='nodocker' make build`
|
||||
|
||||
To build `mgmt` without augeas, libvirt or docker support please run:
|
||||
`GOTAGS='noaugeas novirt nodocker' make build`
|
||||
|
||||
## Binary Package Installation
|
||||
|
||||
|
||||
@@ -19,17 +19,80 @@ on this design, please read the
|
||||
[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
|
||||
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
|
||||
|
||||
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
|
||||
each.
|
||||
|
||||
### Default
|
||||
|
||||
```golang
|
||||
Default() Res
|
||||
Default() engine.Res
|
||||
```
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -67,7 +133,7 @@ func (obj *FooRes) Validate() error {
|
||||
if obj.Answer != 42 { // validate whatever you want
|
||||
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
|
||||
return an error. It should do any resource specific work, and finish by calling
|
||||
the `Init` method of the base resource.
|
||||
return an error. It should do any resource specific work such as initializing
|
||||
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
|
||||
|
||||
```golang
|
||||
// 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
|
||||
if some_error {
|
||||
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
|
||||
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
|
||||
|
||||
@@ -116,21 +193,13 @@ opened in the `Init` method and were using throughout the resource.
|
||||
// Close runs some cleanup code for this resource.
|
||||
func (obj *FooRes) Close() error {
|
||||
err := obj.conn.Close() // close some internal connection
|
||||
|
||||
// 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
|
||||
}
|
||||
obj.someMap = nil // free up some large data structure from memory
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
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`
|
||||
method! If you plan to return early if you hit an internal error, then at least
|
||||
call it with a defer!
|
||||
on an error if something went wrong.
|
||||
|
||||
### 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
|
||||
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
|
||||
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
|
||||
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
|
||||
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`
|
||||
facility will detect the change, ultimately resulting in a subsequent call to
|
||||
`CheckApply`.
|
||||
facility will detect the new change, ultimately resulting in a subsequent call
|
||||
to `CheckApply`.
|
||||
|
||||
#### 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) {
|
||||
// check the state
|
||||
if state_is_okay { return true, nil } // done early! :)
|
||||
|
||||
// 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!
|
||||
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
|
||||
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
|
||||
|
||||
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)`.
|
||||
* 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)`.
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
events from the engine via the `<-obj.Events()` call, and receive events for our
|
||||
resource itself!
|
||||
events from the engine via the `<-obj.init.Events` channel, and receive events
|
||||
for our resource itself!
|
||||
|
||||
#### Events
|
||||
|
||||
If we receive an internal event from the `<-obj.Events()` method, we can read it
|
||||
with the ReadEvent helper function. This function tells us if we should shutdown
|
||||
our resource, and if we should generate an event. 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 with the
|
||||
`StateOK(false)` function.
|
||||
If we receive an internal event from the `<-obj.init.Events` channel, we should
|
||||
read it with the `obj.init.Read` helper function. This function tells us if we
|
||||
should shutdown our resource. It also handles pause functionality which blocks
|
||||
our resource temporarily in this method. If this channel shuts down, then we
|
||||
should treat that as an exit signal.
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
been prepared for it by the engine. This is done by calling the `ConvergerUID`
|
||||
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.
|
||||
If you need this functionality, please contact one of the maintainers and ask
|
||||
about adding this feature and improving these docs right here.
|
||||
|
||||
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
|
||||
@@ -297,28 +355,31 @@ func (obj *FooRes) Watch() error {
|
||||
if err, obj.foo = OpenFoo(); err != nil {
|
||||
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
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
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
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
// shutdown engine
|
||||
// (it is okay if some `defer` code runs first)
|
||||
return nil
|
||||
}
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// the actual events!
|
||||
case event := <-obj.foo.Events:
|
||||
if is_an_event {
|
||||
send = true // used below
|
||||
obj.StateOK(false) // dirty
|
||||
send = true
|
||||
obj.init.Dirty() // dirty
|
||||
}
|
||||
|
||||
// event errors
|
||||
@@ -329,7 +390,9 @@ func (obj *FooRes) Watch() error {
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
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
|
||||
|
||||
* Remember to call the appropriate `converger` methods throughout the resource.
|
||||
* Remember to call `Startup` when the `Watch` is running successfully.
|
||||
* Remember to call `Running` when the `Watch` is running successfully.
|
||||
* Remember to process internal events and shutdown promptly if asked to.
|
||||
* 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.
|
||||
|
||||
### Compare
|
||||
### Cmp
|
||||
|
||||
```golang
|
||||
Compare(Res) bool
|
||||
Cmp(engine.Res) error
|
||||
```
|
||||
|
||||
Each resource must have a `Compare` method. This takes as input another resource
|
||||
and must return whether they are identical or not. This is used for identifying
|
||||
if an existing resource can be used in place of a new one with a similar set of
|
||||
parameters. In particular, when switching from one graph to a new (possibly
|
||||
identical) graph, this avoids recomputing the state for resources which don't
|
||||
change or that are sufficiently similar that they don't need to be swapped out.
|
||||
Each resource must have a `Cmp` method. It is an abbreviation for `Compare`. It
|
||||
takes as input another resource and must return whether they are identical or
|
||||
not. This is used for identifying if an existing resource can be used in place
|
||||
of a new one with a similar set of parameters. In particular, when switching
|
||||
from one graph to a new (possibly identical) graph, this avoids recomputing the
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
```golang
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *FooRes) Compare(r Res) bool {
|
||||
// Cmp compares two resources and returns if they are equivalent.
|
||||
func (obj *FooRes) Cmp(r engine.Res) error {
|
||||
// we can only compare FooRes to others of the same resource kind
|
||||
res, ok := r.(*FooRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.whatever != res.whatever {
|
||||
return false
|
||||
if obj.Whatever != res.Whatever {
|
||||
return fmt.Errorf("the Whatever param differs")
|
||||
}
|
||||
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
|
||||
UIDs() []ResUID
|
||||
UIDs() []engine.ResUID
|
||||
```
|
||||
|
||||
The `UIDs` method returns a list of `ResUID` interfaces that represent the
|
||||
particular resource uniquely. This is used with the AutoEdges API to determine
|
||||
if another resource can match a dependency to this one.
|
||||
|
||||
### AutoEdges
|
||||
#### AutoEdges
|
||||
|
||||
```golang
|
||||
AutoEdges() (AutoEdge, error)
|
||||
AutoEdges() (engine.AutoEdge, error)
|
||||
```
|
||||
|
||||
This returns a struct that implements the `AutoEdge` interface. This struct
|
||||
is used to match other resources that might be relevant dependencies for this
|
||||
resource.
|
||||
|
||||
### CollectPattern
|
||||
### Groupable
|
||||
|
||||
```golang
|
||||
CollectPattern() string
|
||||
```
|
||||
Groupable is a trait that can allow your resource automatically group itself to
|
||||
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.
|
||||
|
||||
### 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
|
||||
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
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
previous value, you can query the Recv parameter. It will contain a `map` of all
|
||||
the keys which can be received on, and the value has a `Changed` property which
|
||||
will indicate whether the value was updated on this particular `CheckApply`
|
||||
invocation. The type of the sending key must match that of the receiving one.
|
||||
This can _only_ be done inside of the `CheckApply` function!
|
||||
received, and if it changed (during this invocation of `CheckApply`) from the
|
||||
previous value, you can query the `obj.init.Recv()` method. It will contain a
|
||||
`map` of all the keys which can be received on, and the value has a `Changed`
|
||||
property which will indicate whether the value was updated on this particular
|
||||
`CheckApply` invocation. The type of the sending key must match that of the
|
||||
receiving one. This can _only_ be done inside of the `CheckApply` function!
|
||||
|
||||
```golang
|
||||
// inside CheckApply, probably near the top
|
||||
if val, exists := obj.Recv["SomeKey"]; exists {
|
||||
log.Printf("SomeKey was sent to us from: %s.%s", val.Res, val.Key)
|
||||
if val, exists := obj.init.Recv()["SomeKey"]; exists {
|
||||
obj.init.Logf("the SomeKey param was sent to us from: %s.%s", val.Res, val.Key)
|
||||
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
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Astute readers will note that there isn't anything that prevents a user from
|
||||
sending an identically typed value to some arbitrary (public) key that the
|
||||
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.
|
||||
The specifics of resource sending are not currently documented. Please send a
|
||||
patch here!
|
||||
|
||||
## Composite resources
|
||||
|
||||
@@ -624,6 +788,15 @@ us know!
|
||||
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!
|
||||
|
||||
### 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?
|
||||
|
||||
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
* [Augeas](#Augeas): Manipulate files using augeas.
|
||||
* [Docker](#Docker):[Container](#Container) Manage docker containers.
|
||||
* [Exec](#Exec): Execute shell commands on the system.
|
||||
* [File](#File): Manage files and directories.
|
||||
* [Group](#Group): Manage system groups.
|
||||
* [Hostname](#Hostname): Manages the hostname on the system.
|
||||
* [KV](#KV): Set a key value pair in our shared world database.
|
||||
* [Msg](#Msg): Send log messages.
|
||||
* [Net](#Net): Manage a local network interface.
|
||||
* [Noop](#Noop): A simple resource that does nothing.
|
||||
* [Nspawn](#Nspawn): Manage systemd-machined nspawn containers.
|
||||
* [Password](#Password): Create random password strings.
|
||||
* [Pkg](#Pkg): Manage system packages with PackageKit.
|
||||
* [Print](#Print): Print messages to the console.
|
||||
* [Svc](#Svc): Manage system systemd services.
|
||||
* [Test](#Test): A mostly harmless resource that is used for internal testing.
|
||||
* [Timer](#Timer): Manage system systemd services.
|
||||
* [User](#User): Manage system users.
|
||||
* [Virt](#Virt): Manage virtual machines with libvirt.
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
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
|
||||
property is not set to `true`, then this file resource will error.
|
||||
|
||||
## Group
|
||||
|
||||
The group resource manages the system groups from `/etc/group`.
|
||||
|
||||
## Hostname
|
||||
|
||||
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
|
||||
as systemd's journal.
|
||||
|
||||
## Net
|
||||
|
||||
The net resource manages a local network interface using netlink.
|
||||
|
||||
## Noop
|
||||
|
||||
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
|
||||
have great Debian (deb/dpkg) and Fedora (rpm/dnf) support simultaneously.
|
||||
|
||||
## Print
|
||||
|
||||
The print resource prints messages to the console.
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -15,18 +15,61 @@
|
||||
// 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
|
||||
package engine
|
||||
|
||||
import (
|
||||
"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").
|
||||
type ResUID interface {
|
||||
fmt.Stringer // String() string
|
||||
|
||||
GetName() string
|
||||
GetKind() string
|
||||
fmt.Stringer // String() string
|
||||
|
||||
IFF(ResUID) bool
|
||||
|
||||
@@ -72,7 +115,7 @@ func (obj *BaseUID) IFF(uid ResUID) bool {
|
||||
// happens before the generator.
|
||||
func (obj *BaseUID) IsReversed() bool {
|
||||
if obj.Reversed == nil {
|
||||
log.Fatal("Programming error!")
|
||||
panic("programming error!")
|
||||
}
|
||||
return *obj.Reversed
|
||||
}
|
||||
38
engine/autoedge_test.go
Normal file
38
engine/autoedge_test.go
Normal 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
84
engine/autogroup.go
Normal 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
126
engine/cmp.go
Normal 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
|
||||
}
|
||||
@@ -15,7 +15,11 @@
|
||||
// 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
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Edge is a struct that represents a graph's edge.
|
||||
type Edge struct {
|
||||
@@ -30,19 +34,19 @@ func (obj *Edge) String() string {
|
||||
return obj.Name
|
||||
}
|
||||
|
||||
// Compare returns true if two edges are equivalent. Otherwise it returns false.
|
||||
func (obj *Edge) Compare(edge *Edge) bool {
|
||||
// Cmp compares this edge to another. It returns nil if they are equivalent.
|
||||
func (obj *Edge) Cmp(edge *Edge) error {
|
||||
if obj.Name != edge.Name {
|
||||
return false
|
||||
return fmt.Errorf("edge names differ")
|
||||
}
|
||||
if obj.Notify != edge.Notify {
|
||||
return false
|
||||
return fmt.Errorf("notify values differ")
|
||||
}
|
||||
// FIXME: should we compare this as well?
|
||||
//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.
|
||||
32
engine/error.go
Normal file
32
engine/error.go
Normal 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
33
engine/event/event.go
Normal 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
|
||||
)
|
||||
@@ -15,42 +15,14 @@
|
||||
// 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
|
||||
package engine
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/purpleidea/mgmt/etcd/scheduler"
|
||||
|
||||
"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:
|
||||
// NopCloser(r io.Reader) io.ReadCloser // not implemented here
|
||||
// ReadAll(r io.Reader) ([]byte, error)
|
||||
474
engine/graph/actions.go
Normal file
474
engine/graph/actions.go
Normal 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
30
engine/graph/autoedge.go
Normal 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)
|
||||
}
|
||||
@@ -15,38 +15,88 @@
|
||||
// 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
|
||||
package autoedge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
multierr "github.com/hashicorp/go-multierror"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
// AutoEdge adds the automatic edges to the graph.
|
||||
func AutoEdge(graph *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
|
||||
logf("adding autoedges...")
|
||||
|
||||
// UIDExistsInUIDs wraps the IFF method when used with a list of UID's.
|
||||
func UIDExistsInUIDs(uid ResUID, uids []ResUID) bool {
|
||||
for _, u := range uids {
|
||||
if uid.IFF(u) {
|
||||
return true
|
||||
// initially get all of the autoedges to seek out all possible errors
|
||||
var err error
|
||||
autoEdgeObjMap := make(map[engine.EdgeableRes]engine.AutoEdge)
|
||||
sorted := []engine.EdgeableRes{}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 false
|
||||
return nil
|
||||
}
|
||||
|
||||
// addEdgesByMatchingUIDS adds edges to the vertex in a graph based on if it
|
||||
// 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!
|
||||
var result []bool
|
||||
|
||||
@@ -54,29 +104,36 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
|
||||
for _, uid := range uids {
|
||||
var found = false
|
||||
// uid is a ResUID object
|
||||
for _, vv := range g.Vertices() { // search
|
||||
if v == vv { // skip self
|
||||
for _, v := range graph.Vertices() { // search
|
||||
r, ok := v.(engine.EdgeableRes)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if b, ok := g.Value("debug"); ok && util.Bool(b) {
|
||||
log.Printf("Compile: AutoEdge: Match: %s with UID: %s", vv, uid)
|
||||
if r.AutoEdgeMeta().Disabled { // skip if this res is disabled
|
||||
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,
|
||||
// that is to say, the name value of a res is a helpful
|
||||
// handle, but it is not necessarily a unique identity!
|
||||
// remember, resources can return multiple UID's each!
|
||||
if UIDExistsInUIDs(uid, VtoR(vv).UIDs()) {
|
||||
// add edge from: vv -> v
|
||||
if UIDExistsInUIDs(uid, r.UIDs()) {
|
||||
// add edge from: r -> res
|
||||
if uid.IsReversed() {
|
||||
txt := fmt.Sprintf("AutoEdge: %s -> %s", vv, v)
|
||||
log.Printf("Compile: Adding %s", txt)
|
||||
edge := &Edge{Name: txt}
|
||||
g.AddEdge(vv, v, edge)
|
||||
txt := fmt.Sprintf("%s -> %s (autoedge)", r, res)
|
||||
logf("autoedge: adding: %s", txt)
|
||||
edge := &engine.Edge{Name: txt}
|
||||
graph.AddEdge(r, res, edge)
|
||||
} else { // edges go the "normal" way, eg: pkg resource
|
||||
txt := fmt.Sprintf("AutoEdge: %s -> %s", v, vv)
|
||||
log.Printf("Compile: Adding %s", txt)
|
||||
edge := &Edge{Name: txt}
|
||||
g.AddEdge(v, vv, edge)
|
||||
txt := fmt.Sprintf("%s -> %s (autoedge)", res, r)
|
||||
logf("autoedge: adding: %s", txt)
|
||||
edge := &engine.Edge{Name: txt}
|
||||
graph.AddEdge(res, r, edge)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
@@ -87,62 +144,12 @@ func addEdgesByMatchingUIDS(g *pgraph.Graph, v pgraph.Vertex, uids []ResUID) []b
|
||||
return result
|
||||
}
|
||||
|
||||
// AutoEdges adds the automatic edges to the graph.
|
||||
func AutoEdges(g *pgraph.Graph) error {
|
||||
log.Println("Compile: Adding AutoEdges...")
|
||||
|
||||
// initially get all of the autoedges to seek out all possible errors
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// match and add edges
|
||||
result := addEdgesByMatchingUIDS(g, v, uids)
|
||||
|
||||
// report back, and find out if we should continue
|
||||
if !autoEdgeObj.Test(result) {
|
||||
break
|
||||
}
|
||||
// UIDExistsInUIDs wraps the IFF method when used with a list of UID's.
|
||||
func UIDExistsInUIDs(uid engine.ResUID, uids []engine.ResUID) bool {
|
||||
for _, u := range uids {
|
||||
if uid.IFF(u) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return false
|
||||
}
|
||||
141
engine/graph/autogroup.go
Normal file
141
engine/graph/autogroup.go
Normal 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
|
||||
}
|
||||
71
engine/graph/autogroup/autogroup.go
Normal file
71
engine/graph/autogroup/autogroup.go
Normal 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
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
// 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
|
||||
// +build !root
|
||||
|
||||
package autogroup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,13 +27,114 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"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.
|
||||
func NE(s string) pgraph.Edge {
|
||||
obj := &Edge{Name: s}
|
||||
obj := &engine.Edge{Name: s}
|
||||
return obj
|
||||
}
|
||||
|
||||
@@ -40,41 +143,96 @@ type testGrouper struct {
|
||||
NonReachabilityGrouper // "inherit" what we want, and reimplement the rest
|
||||
}
|
||||
|
||||
func (ag *testGrouper) name() string {
|
||||
func (obj *testGrouper) Name() string {
|
||||
return "testGrouper"
|
||||
}
|
||||
|
||||
func (ag *testGrouper) vertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
|
||||
if err := VtoR(v1).GroupRes(VtoR(v2)); err != nil { // group them first
|
||||
func (obj *testGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
|
||||
// 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
|
||||
}
|
||||
// HACK: update the name so it matches full list of self+grouped
|
||||
obj := VtoR(v1)
|
||||
names := strings.Split(obj.GetName(), ",") // load in stored names
|
||||
for _, n := range obj.GetGroup() {
|
||||
names = append(names, n.GetName()) // add my contents
|
||||
res := v1.(engine.GroupableRes)
|
||||
names := strings.Split(res.Name(), ",") // load in stored names
|
||||
for _, n := range res.GetGroup() {
|
||||
names = append(names, n.Name()) // add my contents
|
||||
}
|
||||
names = util.StrRemoveDuplicatesInList(names) // remove duplicates
|
||||
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!
|
||||
}
|
||||
|
||||
func (ag *testGrouper) edgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
edge1 := e1.(*Edge) // panic if wrong
|
||||
edge2 := e2.(*Edge) // panic if wrong
|
||||
func (obj *testGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
|
||||
edge1 := e1.(*engine.Edge) // panic if wrong
|
||||
edge2 := e2.(*engine.Edge) // panic if wrong
|
||||
// HACK: update the name so it makes a union of both names
|
||||
n1 := strings.Split(edge1.Name, ",") // load
|
||||
n2 := strings.Split(edge2.Name, ",") // load
|
||||
names := append(n1, n2...)
|
||||
names = util.StrRemoveDuplicatesInList(names) // remove duplicates
|
||||
sort.Strings(names)
|
||||
return &Edge{Name: strings.Join(names, ",")}
|
||||
return &engine.Edge{Name: strings.Join(names, ",")}
|
||||
}
|
||||
|
||||
// helper function
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
// equal. It also compares if grouped element groups are identical.
|
||||
// TODO: port this to use the pgraph.GraphCmp function instead.
|
||||
@@ -133,20 +257,20 @@ func GraphCmp(g1, g2 *pgraph.Graph) error {
|
||||
Loop:
|
||||
// check vertices
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
|
||||
l1 := strings.Split(VtoR(v1).GetName(), ",") // make list of everyone's names...
|
||||
for _, x1 := range VtoR(v1).GetGroup() {
|
||||
l1 = append(l1, x1.GetName()) // add my contents
|
||||
r1 := v1.(engine.GroupableRes)
|
||||
l1 := strings.Split(r1.Name(), ",") // make list of everyone's names...
|
||||
for _, x1 := range r1.GetGroup() {
|
||||
l1 = append(l1, x1.Name()) // add my contents
|
||||
}
|
||||
l1 = util.StrRemoveDuplicatesInList(l1) // remove duplicates
|
||||
sort.Strings(l1)
|
||||
|
||||
// inner loop
|
||||
for v2 := range g2.Adjacency() { // does it match in g2 ?
|
||||
|
||||
l2 := strings.Split(VtoR(v2).GetName(), ",")
|
||||
for _, x2 := range VtoR(v2).GetGroup() {
|
||||
l2 = append(l2, x2.GetName())
|
||||
r2 := v2.(engine.GroupableRes)
|
||||
l2 := strings.Split(r2.Name(), ",")
|
||||
for _, x2 := range r2.GetGroup() {
|
||||
l2 = append(l2, x2.Name())
|
||||
}
|
||||
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
|
||||
sort.Strings(l2)
|
||||
@@ -157,7 +281,7 @@ 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 :)
|
||||
|
||||
@@ -166,35 +290,40 @@ Loop:
|
||||
v2 := m[v1] // lookup in map to get correspondance
|
||||
// g1.Adjacency()[v1] corresponds to g2.Adjacency()[v2]
|
||||
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] {
|
||||
vv2 := m[vv1]
|
||||
ee1 := ee1.(*Edge)
|
||||
ee2 := g2.Adjacency()[v2][vv2].(*Edge)
|
||||
ee1 := ee1.(*engine.Edge)
|
||||
ee2 := g2.Adjacency()[v2][vv2].(*engine.Edge)
|
||||
|
||||
// these are edges from v1 -> vv1 via ee1 (graph 1)
|
||||
// to cmp to edges from v2 -> vv2 via ee2 (graph 2)
|
||||
|
||||
// check: (1) vv1 == vv2 ? (we've already checked this!)
|
||||
l1 := strings.Split(VtoR(vv1).GetName(), ",") // make list of everyone's names...
|
||||
for _, x1 := range VtoR(vv1).GetGroup() {
|
||||
l1 = append(l1, x1.GetName()) // add my contents
|
||||
rr1 := vv1.(engine.GroupableRes)
|
||||
rr2 := vv2.(engine.GroupableRes)
|
||||
|
||||
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
|
||||
sort.Strings(l1)
|
||||
|
||||
l2 := strings.Split(VtoR(vv2).GetName(), ",")
|
||||
for _, x2 := range VtoR(vv2).GetGroup() {
|
||||
l2 = append(l2, x2.GetName())
|
||||
l2 := strings.Split(rr2.Name(), ",")
|
||||
for _, x2 := range rr2.GetGroup() {
|
||||
l2 = append(l2, x2.Name())
|
||||
}
|
||||
l2 = util.StrRemoveDuplicatesInList(l2) // remove duplicates
|
||||
sort.Strings(l2)
|
||||
|
||||
// does l1 match l2 ?
|
||||
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
|
||||
@@ -207,11 +336,13 @@ Loop:
|
||||
// check meta parameters
|
||||
for v1 := range g1.Adjacency() { // for each vertex in g1
|
||||
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(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) {
|
||||
str += "\n"
|
||||
for v := range g.Adjacency() {
|
||||
if semas := VtoR(v).Meta().Sema; len(semas) > 0 {
|
||||
str += fmt.Sprintf("* v: %v; sema: %v\n", VtoR(v).GetName(), semas)
|
||||
r := v.(engine.Res)
|
||||
if semas := r.MetaParams().Sema; len(semas) > 0 {
|
||||
str += fmt.Sprintf("* v: %v; sema: %v\n", r.Name(), semas)
|
||||
} else {
|
||||
str += fmt.Sprintf("* v: %v\n", VtoR(v).GetName())
|
||||
str += fmt.Sprintf("* v: %v\n", r.Name())
|
||||
}
|
||||
// TODO: add explicit grouping data?
|
||||
}
|
||||
for v1 := range g.Adjacency() {
|
||||
for v2, e := range g.Adjacency()[v1] {
|
||||
edge := e.(*Edge)
|
||||
str += fmt.Sprintf("* e: %v -> %v # %v\n", VtoR(v1).GetName(), VtoR(v2).GetName(), edge.Name)
|
||||
r1 := v1.(engine.Res)
|
||||
r2 := v2.(engine.Res)
|
||||
edge := e.(*engine.Edge)
|
||||
str += fmt.Sprintf("* e: %v -> %v # %v\n", r1.Name(), r2.Name(), edge.Name)
|
||||
}
|
||||
}
|
||||
return
|
||||
@@ -731,3 +865,57 @@ func TestPgraphGroupingConnected1(t *testing.T) {
|
||||
}
|
||||
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)
|
||||
}
|
||||
127
engine/graph/autogroup/base.go
Normal file
127
engine/graph/autogroup/base.go
Normal 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
|
||||
}
|
||||
73
engine/graph/autogroup/nonreachability.go
Normal file
73
engine/graph/autogroup/nonreachability.go
Normal 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...
|
||||
}
|
||||
}
|
||||
127
engine/graph/autogroup/util.go
Normal file
127
engine/graph/autogroup/util.go
Normal 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
336
engine/graph/engine.go
Normal 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
59
engine/graph/refresh.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -31,8 +31,8 @@ import (
|
||||
// SemaSep is the trailing separator to split the semaphore id from the size.
|
||||
const SemaSep = ":"
|
||||
|
||||
// SemaLock acquires the list of semaphores in the graph.
|
||||
func (obj *MGraph) SemaLock(semas []string) error {
|
||||
// semaLock acquires the list of semaphores in the graph.
|
||||
func (obj *Engine) semaLock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // very important to avoid deadlock in the dag!
|
||||
|
||||
@@ -53,8 +53,8 @@ func (obj *MGraph) SemaLock(semas []string) error {
|
||||
return reterr
|
||||
}
|
||||
|
||||
// SemaUnlock releases the list of semaphores in the graph.
|
||||
func (obj *MGraph) SemaUnlock(semas []string) error {
|
||||
// semaUnlock releases the list of semaphores in the graph.
|
||||
func (obj *Engine) semaUnlock(semas []string) error {
|
||||
var reterr error
|
||||
sort.Strings(semas) // unlock in the same order to remove partial locks
|
||||
|
||||
@@ -15,11 +15,23 @@
|
||||
// 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 novirt
|
||||
// +build !root
|
||||
|
||||
package resources
|
||||
package graph
|
||||
|
||||
// VirtRes represents the fields of the Virt resource. Since this file is
|
||||
// only invoked with the tag "novirt", we do not need any fields here.
|
||||
type VirtRes struct {
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
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
118
engine/graph/sendrecv.go
Normal 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
436
engine/graph/state.go
Normal 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
169
engine/metaparams.go
Normal 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
42
engine/metaparams_test.go
Normal 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
32
engine/refresh.go
Normal 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
271
engine/resources.go
Normal 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
|
||||
}
|
||||
@@ -21,10 +21,11 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
@@ -39,13 +40,15 @@ const (
|
||||
)
|
||||
|
||||
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.
|
||||
// Currently only allows you to change simple files (e.g sshd_config).
|
||||
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 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
|
||||
// ["path", "value"]. mgmt will run augeas.Get() before augeas.Set(), to
|
||||
// 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
|
||||
}
|
||||
@@ -68,13 +71,31 @@ type AugeasSet struct {
|
||||
Value string `yaml:"value"` // The value to be set on the given Path.
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *AugeasRes) Default() Res {
|
||||
return &AugeasRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
// Cmp compares this set with another one.
|
||||
func (obj *AugeasSet) Cmp(set *AugeasSet) error {
|
||||
if obj == nil && set == nil {
|
||||
return nil
|
||||
}
|
||||
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.
|
||||
@@ -88,12 +109,19 @@ func (obj *AugeasRes) Validate() error {
|
||||
if (obj.Lens == "") != (obj.File == "") {
|
||||
return fmt.Errorf("the File and Lens params must be specified together")
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initiates the resource.
|
||||
func (obj *AugeasRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
// Init initializes the resource.
|
||||
func (obj *AugeasRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
@@ -108,16 +136,14 @@ func (obj *AugeasRes) Watch() error {
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching: %s", obj, obj.File) // attempting to watch...
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", obj.File) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -128,29 +154,33 @@ func (obj *AugeasRes) Watch() error {
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.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)
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
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
|
||||
if send {
|
||||
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
|
||||
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)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
opts := augeas.None
|
||||
if obj.Lens != "" {
|
||||
@@ -224,7 +254,7 @@ func (obj *AugeasRes) CheckApply(apply bool) (bool, error) {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
log.Printf("%s: changes needed, saving", obj)
|
||||
obj.init.Logf("changes needed, saving")
|
||||
if err = ag.Save(); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
type AugeasUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// 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{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []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
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
@@ -25,7 +25,6 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
@@ -33,6 +32,9 @@ import (
|
||||
"sync"
|
||||
"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/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
@@ -45,7 +47,7 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("aws:ec2", func() Res { return &AwsEc2Res{} })
|
||||
engine.RegisterResource("aws:ec2", func() engine.Res { return &AwsEc2Res{} })
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -145,7 +147,10 @@ var AwsRegions = []string{
|
||||
// AWS credentials must be present in ~/.aws - For detailed instructions see
|
||||
// http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
|
||||
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
|
||||
Region string `yaml:"region"` // region must match an element of AwsRegions
|
||||
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.
|
||||
func (obj *AwsEc2Res) Default() Res {
|
||||
return &AwsEc2Res{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *AwsEc2Res) Default() engine.Res {
|
||||
return &AwsEc2Res{}
|
||||
}
|
||||
|
||||
// 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 obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
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.
|
||||
@@ -394,12 +420,11 @@ func (obj *AwsEc2Res) Watch() error {
|
||||
// longpollWatch uses the ec2 api's built in methods to watch ec2 resource state.
|
||||
func (obj *AwsEc2Res) longpollWatch() error {
|
||||
send := false
|
||||
var exit *error
|
||||
|
||||
// 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.
|
||||
if err := obj.Running(); err != nil {
|
||||
return err
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
// cancellable context used for exiting cleanly
|
||||
@@ -463,10 +488,14 @@ func (obj *AwsEc2Res) longpollWatch() error {
|
||||
// process events from the goroutine
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case msg, ok := <-obj.awsChan:
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -479,105 +508,20 @@ func (obj *AwsEc2Res) longpollWatch() error {
|
||||
case "", ec2.InstanceStateNamePending, ec2.InstanceStateNameStopping:
|
||||
continue
|
||||
default:
|
||||
log.Printf("%s: State: %v", obj, msg.state)
|
||||
obj.StateOK(false)
|
||||
obj.init.Logf("State: %v", msg.state)
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
}
|
||||
if send {
|
||||
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-
|
||||
// change notifications pushed to the http endpoint (snsServer) set up below.
|
||||
// In Init() a CloudWatch rule is created along with a corresponding SNS topic
|
||||
@@ -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.
|
||||
func (obj *AwsEc2Res) snsWatch() error {
|
||||
send := false
|
||||
var exit *error
|
||||
defer obj.wg.Wait()
|
||||
// create the sns listener
|
||||
// closing is handled by http.Server.Shutdown in the defer func below
|
||||
@@ -603,10 +546,10 @@ func (obj *AwsEc2Res) snsWatch() error {
|
||||
defer cancel()
|
||||
if err := snsServer.Shutdown(ctx); err != nil {
|
||||
if err != context.Canceled {
|
||||
log.Printf("%s: error stopping sns endpoint: %s", obj, err)
|
||||
obj.init.Logf("error stopping sns endpoint: %s", err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s: sns server shutdown cancelled", obj)
|
||||
obj.init.Logf("sns server shutdown cancelled")
|
||||
}
|
||||
}()
|
||||
defer close(obj.closeChan)
|
||||
@@ -618,7 +561,7 @@ func (obj *AwsEc2Res) snsWatch() error {
|
||||
if err := snsServer.Serve(listener); err != nil {
|
||||
// when we shut down
|
||||
if err == http.ErrServerClosed {
|
||||
log.Printf("%s: Stopped SNS Endpoint", obj)
|
||||
obj.init.Logf("Stopped SNS Endpoint")
|
||||
return
|
||||
}
|
||||
// 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
|
||||
// the http server, so that the server can process the subscription
|
||||
// confirmation. We won't drop incoming connections from aws by this
|
||||
@@ -644,10 +587,14 @@ func (obj *AwsEc2Res) snsWatch() error {
|
||||
// process events
|
||||
for {
|
||||
select {
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case msg, ok := <-obj.awsChan:
|
||||
if !ok {
|
||||
return nil
|
||||
@@ -660,25 +607,27 @@ func (obj *AwsEc2Res) snsWatch() error {
|
||||
// is confirmed, we are ready to receive events, so we
|
||||
// can notify the engine that we're running.
|
||||
if msg.event == awsEc2EventWatchReady {
|
||||
if err := obj.Running(); err != nil {
|
||||
return err
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
continue
|
||||
}
|
||||
log.Printf("%s: State: %v", obj, msg.event)
|
||||
obj.StateOK(false)
|
||||
obj.init.Logf("State: %v", msg.event)
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for AwsEc2 resource.
|
||||
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
|
||||
instance, err := describeInstanceByName(obj.client, obj.prependName())
|
||||
@@ -822,44 +771,22 @@ func (obj *AwsEc2Res) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AwsEc2UID is the UID struct for AwsEc2Res.
|
||||
type AwsEc2UID struct {
|
||||
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() []ResUID {
|
||||
x := &AwsEc2UID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *AwsEc2Res) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*AwsEc2Res)
|
||||
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
|
||||
}
|
||||
@@ -887,6 +814,27 @@ func (obj *AwsEc2Res) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -907,90 +855,6 @@ func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
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.
|
||||
func (obj *AwsEc2Res) snsListener(listenAddr string) (net.Listener, error) {
|
||||
addr := listenAddr
|
||||
@@ -1017,7 +881,7 @@ func (obj *AwsEc2Res) snsPostHandler(w http.ResponseWriter, req *http.Request) {
|
||||
decoder := json.NewDecoder(req.Body)
|
||||
var post postData
|
||||
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)
|
||||
if obj.ErrorOnMalformedPost {
|
||||
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
|
||||
// signature, we print the error, ignore the event and return.
|
||||
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)
|
||||
return
|
||||
}
|
||||
@@ -1181,7 +1045,7 @@ func (obj *AwsEc2Res) snsMakeTopic() (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
log.Printf("%s: Created SNS Topic", obj)
|
||||
obj.init.Logf("Created SNS Topic")
|
||||
if topic.TopicArn == 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 {
|
||||
return err
|
||||
}
|
||||
log.Printf("%s: Deleted SNS Topic", obj)
|
||||
obj.init.Logf("Deleted SNS Topic")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1214,7 +1078,7 @@ func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("%s: Created Subscription", obj)
|
||||
obj.init.Logf("Created Subscription")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1230,7 +1094,7 @@ func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) erro
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("%s: Subscription Confirmed", obj)
|
||||
obj.init.Logf("Subscription Confirmed")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1306,7 +1170,7 @@ func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
|
||||
for _, statement := range policy.Statement {
|
||||
if statement == permission {
|
||||
// if it's already there, we're done
|
||||
log.Printf("%s: Target Already Authorized", obj)
|
||||
obj.init.Logf("Target Already Authorized")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1328,7 +1192,7 @@ func (obj *AwsEc2Res) snsAuthorizeCloudWatch(topicArn string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("%s: Authorized Target", obj)
|
||||
obj.init.Logf("Authorized Target")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1358,7 +1222,7 @@ func (obj *AwsEc2Res) cweMakeRule(name, eventPattern string) error {
|
||||
if _, err := obj.cweClient.PutRule(putRuleInput); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("%s: Created CloudWatch Rule", obj)
|
||||
obj.init.Logf("Created CloudWatch Rule")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1368,7 +1232,7 @@ func (obj *AwsEc2Res) cweDeleteRule(name string) error {
|
||||
drInput := &cwe.DeleteRuleInput{
|
||||
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 {
|
||||
return errwrap.Wrapf(err, "error deleting cloudwatch rule")
|
||||
}
|
||||
@@ -1391,7 +1255,7 @@ func (obj *AwsEc2Res) cweTargetRule(topicArn, targetID, inputPath, ruleName stri
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "error putting cloudwatch target")
|
||||
}
|
||||
log.Printf("%s: Targeted SNS Topic", obj)
|
||||
obj.init.Logf("Targeted SNS Topic")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1402,34 +1266,176 @@ func (obj *AwsEc2Res) cweRemoveTarget(targetID, ruleName string) error {
|
||||
Ids: []*string{aws.String(targetID)},
|
||||
Rule: aws.String(ruleName),
|
||||
}
|
||||
log.Printf("%s: Removing Target", obj)
|
||||
obj.init.Logf("Removing Target")
|
||||
if _, err := obj.cweClient.RemoveTargets(rtInput); err != nil {
|
||||
return errwrap.Wrapf(err, "error removing cloudwatch target")
|
||||
}
|
||||
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)
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
if err := obj.BaseRes.Close(); err != nil {
|
||||
errList = multierr.Append(errList, err) // list of errors
|
||||
// error if we didn't find one
|
||||
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
|
||||
}
|
||||
441
engine/resources/docker_container.go
Normal file
441
engine/resources/docker_container.go
Normal 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
|
||||
}
|
||||
201
engine/resources/docker_container_test.go
Normal file
201
engine/resources/docker_container_test.go
Normal 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
|
||||
}
|
||||
@@ -21,25 +21,31 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("exec", func() Res { return &ExecRes{} })
|
||||
engine.RegisterResource("exec", func() engine.Res { return &ExecRes{} })
|
||||
}
|
||||
|
||||
// ExecRes is an exec resource for running commands.
|
||||
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
|
||||
Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd
|
||||
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!
|
||||
Stdout *string // the cmd stdout, 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.
|
||||
func (obj *ExecRes) Default() Res {
|
||||
return &ExecRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *ExecRes) Default() engine.Res {
|
||||
return &ExecRes{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *ExecRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *ExecRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
obj.wg = &sync.WaitGroup{}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BufioChanScanner wraps the scanner output in a channel.
|
||||
func (obj *ExecRes) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan error) {
|
||||
ch, errch := make(chan string), make(chan error)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
ch <- scanner.Text() // blocks here ?
|
||||
if e := scanner.Err(); e != nil {
|
||||
errch <- e // send any misc errors we encounter
|
||||
//break // TODO: ?
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
errch <- scanner.Err() // eof or some err
|
||||
close(errch)
|
||||
}()
|
||||
return ch, errch
|
||||
// Close is run by the engine to clean up after the resource is done.
|
||||
func (obj *ExecRes) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *ExecRes) Watch() error {
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
bufioch, errch := make(chan string), make(chan error)
|
||||
ioChan := make(chan *bufioOutput)
|
||||
defer obj.wg.Wait()
|
||||
|
||||
if obj.WatchCmd != "" {
|
||||
var cmdName string
|
||||
@@ -157,43 +151,50 @@ func (obj *ExecRes) Watch() error {
|
||||
return errwrap.Wrapf(err, "error starting Cmd")
|
||||
}
|
||||
|
||||
bufioch, errch = obj.BufioChanScanner(scanner)
|
||||
ioChan = obj.bufioChanScanner(scanner)
|
||||
}
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case text := <-bufioch:
|
||||
// each time we get a line of output, we loop!
|
||||
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
|
||||
case data, ok := <-ioChan:
|
||||
if !ok { // EOF
|
||||
// FIXME: add an "if watch command ends/crashes"
|
||||
// restart or generate error option
|
||||
return fmt.Errorf("reached EOF")
|
||||
}
|
||||
// error reading input?
|
||||
return errwrap.Wrapf(err, "unknown error")
|
||||
if err := data.err; err != nil {
|
||||
// error reading input?
|
||||
return errwrap.Wrapf(err, "unknown error")
|
||||
}
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
// each time we get a line of output, we loop!
|
||||
obj.init.Logf("watch output: %s", data.text)
|
||||
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
|
||||
if send {
|
||||
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
|
||||
log.Printf("%s: Apply", obj)
|
||||
obj.init.Logf("Apply")
|
||||
var cmdName string
|
||||
var cmdArgs []string
|
||||
if obj.Shell == "" {
|
||||
@@ -326,25 +327,23 @@ func (obj *ExecRes) CheckApply(apply bool) (bool, error) {
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
e := errwrap.Wrapf(err, "error running cmd")
|
||||
return false, e
|
||||
return false, errwrap.Wrapf(err, "error running cmd")
|
||||
}
|
||||
return false, fmt.Errorf("cmd error, exit status: %d", wStatus.ExitStatus())
|
||||
|
||||
} else if err != nil {
|
||||
e := errwrap.Wrapf(err, "general cmd error")
|
||||
return false, e
|
||||
return false, errwrap.Wrapf(err, "general cmd error")
|
||||
}
|
||||
|
||||
// TODO: if we printed the stdout while the command is running, this
|
||||
// would be nice, but it would require terminal log output that doesn't
|
||||
// interleave all the parallel parts which would mix it all up...
|
||||
if s := out.String(); s == "" {
|
||||
log.Printf("%s: Command output is empty!", obj)
|
||||
obj.init.Logf("Command output is empty!")
|
||||
|
||||
} else {
|
||||
log.Printf("%s: Command output is:", obj)
|
||||
log.Printf(out.String())
|
||||
obj.init.Logf("Command output is:")
|
||||
obj.init.Logf(out.String())
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// ExecUID is the UID struct for ExecRes.
|
||||
type ExecUID struct {
|
||||
BaseUID
|
||||
Cmd string
|
||||
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
|
||||
})
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *ExecRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
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() []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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*ExecRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Cmd != res.Cmd {
|
||||
return false
|
||||
@@ -464,6 +401,61 @@ func (obj *ExecRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *ExecRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -499,14 +491,14 @@ func (obj *ExecRes) getCredential() (*syscall.Credential, error) {
|
||||
}
|
||||
|
||||
if obj.Group != "" {
|
||||
gid, err = GetGID(obj.Group)
|
||||
gid, err = engineUtil.GetGID(obj.Group)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "error looking up gid for %s", obj.Group)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.User != "" {
|
||||
uid, err = GetUID(obj.User)
|
||||
uid, err = engineUtil.GetUID(obj.User)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *ExecRes) cmdFiles() []string {
|
||||
var paths []string
|
||||
@@ -603,3 +527,95 @@ func (obj *ExecRes) cmdFiles() []string {
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -15,24 +15,36 @@
|
||||
// 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 (
|
||||
"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) {
|
||||
r1 := &ExecRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "exec1",
|
||||
Kind: "exec",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Cmd: "echo hello world",
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
@@ -41,7 +53,7 @@ func TestExecSendRecv1(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
@@ -73,16 +85,10 @@ func TestExecSendRecv1(t *testing.T) {
|
||||
|
||||
func TestExecSendRecv2(t *testing.T) {
|
||||
r1 := &ExecRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "exec1",
|
||||
Kind: "exec",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Cmd: "echo hello world 1>&2", // to stderr
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
@@ -91,7 +97,7 @@ func TestExecSendRecv2(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
@@ -123,16 +129,10 @@ func TestExecSendRecv2(t *testing.T) {
|
||||
|
||||
func TestExecSendRecv3(t *testing.T) {
|
||||
r1 := &ExecRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "exec1",
|
||||
Kind: "exec",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
@@ -141,7 +141,7 @@ func TestExecSendRecv3(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
// run artificially without the entire engine
|
||||
File diff suppressed because it is too large
Load Diff
148
engine/resources/file_test.go
Normal file
148
engine/resources/file_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -20,39 +20,39 @@ package resources
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("group", func() Res { return &GroupRes{} })
|
||||
engine.RegisterResource("group", func() engine.Res { return &GroupRes{} })
|
||||
}
|
||||
|
||||
const groupFile = "/etc/group"
|
||||
|
||||
// GroupRes is a user group resource.
|
||||
type GroupRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"` // state: exists, absent
|
||||
GID *uint32 `yaml:"gid"` // the group's gid
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
State string `yaml:"state"` // state: exists, absent
|
||||
GID *uint32 `yaml:"gid"` // the group's gid
|
||||
|
||||
recWatcher *recwatch.RecWatcher
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *GroupRes) Default() Res {
|
||||
return &GroupRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *GroupRes) Default() engine.Res {
|
||||
return &GroupRes{}
|
||||
}
|
||||
|
||||
// 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" {
|
||||
return fmt.Errorf("State must be 'exists' or 'absent'")
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init initializes the resource.
|
||||
func (obj *GroupRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *GroupRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
@@ -78,16 +85,14 @@ func (obj *GroupRes) Watch() error {
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching: %s", obj, groupFile) // attempting to watch...
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", groupFile) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -98,34 +103,38 @@ func (obj *GroupRes) Watch() error {
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.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)
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
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
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Group resource.
|
||||
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
|
||||
exists := true
|
||||
group, err := user.LookupGroup(obj.GetName())
|
||||
group, err := user.LookupGroup(obj.Name())
|
||||
if err != nil {
|
||||
if _, ok := err.(user.UnknownGroupError); !ok {
|
||||
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")
|
||||
}
|
||||
}
|
||||
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")
|
||||
}
|
||||
// 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
|
||||
// if it is wrong groupmod will change it to the desired value
|
||||
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 obj.State == "exists" && *obj.GID == uint32(existingGID) {
|
||||
@@ -172,14 +181,14 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
var cmdName string
|
||||
args := []string{obj.GetName()}
|
||||
args := []string{obj.Name()}
|
||||
|
||||
if obj.State == "exists" {
|
||||
if exists {
|
||||
log.Printf("%s: Modifying group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Modifying group: %s", obj.Name())
|
||||
cmdName = "groupmod"
|
||||
} else {
|
||||
log.Printf("%s: Adding group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Adding group: %s", obj.Name())
|
||||
cmdName = "groupadd"
|
||||
}
|
||||
if obj.GID != nil {
|
||||
@@ -187,7 +196,7 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
}
|
||||
if obj.State == "absent" && exists {
|
||||
log.Printf("%s: Deleting group: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Deleting group: %s", obj.Name())
|
||||
cmdName = "groupdel"
|
||||
}
|
||||
|
||||
@@ -220,15 +229,45 @@ func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
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.
|
||||
type GroupUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string
|
||||
gid *uint32
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !ok {
|
||||
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.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *GroupRes) UIDs() []ResUID {
|
||||
func (obj *GroupRes) UIDs() []engine.ResUID {
|
||||
x := &GroupUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
gid: obj.GID,
|
||||
}
|
||||
return []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
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
@@ -20,29 +20,30 @@ package resources
|
||||
import (
|
||||
"errors"
|
||||
"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/godbus/dbus"
|
||||
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() {
|
||||
RegisterResource("hostname", func() Res { return &HostnameRes{} })
|
||||
engine.RegisterResource("hostname", func() engine.Res { return &HostnameRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
hostname1Path = "/org/freedesktop/hostname1"
|
||||
hostname1Iface = "org.freedesktop.hostname1"
|
||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
||||
hostname1Path = "/org/freedesktop/hostname1"
|
||||
hostname1Iface = "org.freedesktop.hostname1"
|
||||
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.
|
||||
//
|
||||
// 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
|
||||
// specified, it will set all 3 fields to this value.
|
||||
type HostnameRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Hostname string `yaml:"hostname"`
|
||||
PrettyHostname string `yaml:"pretty_hostname"`
|
||||
StaticHostname string `yaml:"static_hostname"`
|
||||
@@ -68,12 +72,8 @@ type HostnameRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *HostnameRes) Default() Res {
|
||||
return &HostnameRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *HostnameRes) Default() engine.Res {
|
||||
return &HostnameRes{}
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
return ErrResourceInsufficientParameters
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
obj.PrettyHostname = obj.Hostname
|
||||
}
|
||||
@@ -95,7 +97,12 @@ func (obj *HostnameRes) Init() error {
|
||||
if obj.TransientHostname == "" {
|
||||
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.
|
||||
@@ -106,47 +113,52 @@ func (obj *HostnameRes) Watch() error {
|
||||
return errwrap.Wrap(err, "Failed to connect to bus")
|
||||
}
|
||||
defer bus.Close()
|
||||
callResult := bus.BusObject().Call(
|
||||
"org.freedesktop.DBus.AddMatch", 0,
|
||||
fmt.Sprintf("type='signal',path='%s',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'", hostname1Path))
|
||||
if callResult.Err != nil {
|
||||
return errwrap.Wrap(callResult.Err, "Failed to subscribe to DBus events for hostname1")
|
||||
// watch the PropertiesChanged signal on the hostname1 dbus path
|
||||
args := fmt.Sprintf(
|
||||
"type='signal', path='%s', interface='%s', member='PropertiesChanged'",
|
||||
hostname1Path,
|
||||
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
|
||||
bus.Signal(signals)
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-signals:
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, _ := obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
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
|
||||
if send {
|
||||
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)
|
||||
if err != nil {
|
||||
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
|
||||
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 {
|
||||
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
|
||||
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 {
|
||||
return false, err
|
||||
}
|
||||
checkOK = checkOK && propertyCheckOK
|
||||
}
|
||||
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 {
|
||||
return false, err
|
||||
}
|
||||
checkOK = checkOK && propertyCheckOK
|
||||
}
|
||||
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 {
|
||||
return false, err
|
||||
}
|
||||
@@ -216,46 +228,21 @@ func (obj *HostnameRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return checkOK, nil
|
||||
}
|
||||
|
||||
// HostnameUID is the UID struct for HostnameRes.
|
||||
type HostnameUID struct {
|
||||
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() []ResUID {
|
||||
x := &HostnameUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
prettyHostname: obj.PrettyHostname,
|
||||
staticHostname: obj.StaticHostname,
|
||||
transientHostname: obj.TransientHostname,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *HostnameRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *HostnameRes) GroupCmp(r Res) bool {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*HostnameRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.PrettyHostname != res.PrettyHostname {
|
||||
return false
|
||||
@@ -270,6 +257,29 @@ func (obj *HostnameRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *HostnameRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -19,14 +19,16 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
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.
|
||||
@@ -47,7 +49,13 @@ const (
|
||||
// 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.
|
||||
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?
|
||||
Key string `yaml:"key"` // key to set
|
||||
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.
|
||||
func (obj *KVRes) Default() Res {
|
||||
return &KVRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *KVRes) Default() engine.Res {
|
||||
return &KVRes{}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if obj.Key == "" {
|
||||
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.
|
||||
func (obj *KVRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *KVRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
func (obj *KVRes) Watch() error {
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
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 exit *error
|
||||
for {
|
||||
select {
|
||||
// NOTE: this part is very similar to the file resource code
|
||||
@@ -113,36 +121,39 @@ func (obj *KVRes) Watch() error {
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.Data().Debug {
|
||||
log.Printf("%s: Event!", obj)
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event!")
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
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
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lessThanCheck checks for less than validity.
|
||||
func (obj *KVRes) lessThanCheck(value string) (checkOK bool, err error) {
|
||||
|
||||
v := *obj.Value
|
||||
if value == v { // redundant check for safety
|
||||
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
|
||||
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!
|
||||
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.
|
||||
log.Printf("CheckApply: `Value` was updated!")
|
||||
obj.init.Logf("CheckApply: `Value` was updated!")
|
||||
}
|
||||
|
||||
hostname := obj.Data().Hostname // me
|
||||
keyMap, err := obj.Data().World.StrMapGet(obj.Key)
|
||||
hostname := obj.init.Hostname // me
|
||||
keyMap, err := obj.init.World.StrMapGet(obj.Key)
|
||||
if err != nil {
|
||||
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!
|
||||
|
||||
} 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")
|
||||
}
|
||||
|
||||
@@ -211,49 +222,28 @@ func (obj *KVRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
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, nil
|
||||
}
|
||||
|
||||
// KVUID is the UID struct for KVRes.
|
||||
type KVUID struct {
|
||||
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() []ResUID {
|
||||
x := &KVUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *KVRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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!
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*KVRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Key != res.Key {
|
||||
return false
|
||||
@@ -276,6 +266,22 @@ func (obj *KVRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *KVRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
732
engine/resources/mount.go
Normal file
732
engine/resources/mount.go
Normal 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
|
||||
}
|
||||
343
engine/resources/mount_test.go
Normal file
343
engine/resources/mount_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,20 +19,26 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
|
||||
"github.com/coreos/go-systemd/journal"
|
||||
)
|
||||
|
||||
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.
|
||||
type MsgRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
traits.Base // add the base methods without re-implementation
|
||||
traits.Refreshable
|
||||
|
||||
init *engine.Init
|
||||
|
||||
Body string `yaml:"body"`
|
||||
Priority string `yaml:"priority"`
|
||||
Fields map[string]string `yaml:"fields"`
|
||||
@@ -43,19 +49,9 @@ type MsgRes struct {
|
||||
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.
|
||||
func (obj *MsgRes) Default() Res {
|
||||
return &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *MsgRes) Default() engine.Res {
|
||||
return &MsgRes{}
|
||||
}
|
||||
|
||||
// Validate the params that are passed to MsgRes.
|
||||
@@ -81,15 +77,52 @@ func (obj *MsgRes) Validate() error {
|
||||
default:
|
||||
return fmt.Errorf("invalid Priority '%s'", obj.Priority)
|
||||
}
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *MsgRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overrriding
|
||||
func (obj *MsgRes) Init(init *engine.Init) error {
|
||||
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 {
|
||||
if obj.Journal && !obj.journalStateOK {
|
||||
return false
|
||||
@@ -102,7 +135,10 @@ func (obj *MsgRes) isAllStateOK() bool {
|
||||
|
||||
// updateStateOK sets the global state so it can be read by the engine.
|
||||
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.
|
||||
@@ -128,42 +164,15 @@ func (obj *MsgRes) journalPriority() journal.Priority {
|
||||
return journal.PriNotice
|
||||
}
|
||||
|
||||
// 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.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.
|
||||
// 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) {
|
||||
|
||||
// isStateOK() done by engine, so we updateStateOK() to pass in value
|
||||
//if obj.isAllStateOK() {
|
||||
// return true, nil
|
||||
//}
|
||||
|
||||
if obj.Refresh() { // if we were notified...
|
||||
if obj.init.Refresh() { // if we were notified...
|
||||
// invalidate cached state...
|
||||
obj.logStateOK = false
|
||||
if obj.Journal {
|
||||
@@ -176,7 +185,7 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||
}
|
||||
|
||||
if !obj.logStateOK {
|
||||
log.Printf("%s: Body: %s", obj, obj.Body)
|
||||
obj.init.Logf("Body: %s", obj.Body)
|
||||
obj.logStateOK = true
|
||||
obj.updateStateOK()
|
||||
}
|
||||
@@ -199,29 +208,21 @@ func (obj *MsgRes) CheckApply(apply bool) (bool, error) {
|
||||
return 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 *MsgRes) UIDs() []ResUID {
|
||||
x := &MsgUID{
|
||||
BaseUID: BaseUID{
|
||||
Name: obj.GetName(),
|
||||
Kind: obj.GetKind(),
|
||||
},
|
||||
body: obj.Body,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *MsgRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*MsgRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Body != res.Body {
|
||||
return false
|
||||
@@ -241,6 +242,23 @@ func (obj *MsgRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *MsgRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -15,6 +15,8 @@
|
||||
// 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 (
|
||||
@@ -23,15 +25,9 @@ import (
|
||||
|
||||
func TestMsgValidate1(t *testing.T) {
|
||||
r1 := &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "msg1",
|
||||
Kind: "msg",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Priority: "Debug",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err != nil {
|
||||
t.Errorf("validate failed with: %v", err)
|
||||
}
|
||||
@@ -39,15 +35,9 @@ func TestMsgValidate1(t *testing.T) {
|
||||
|
||||
func TestMsgValidate2(t *testing.T) {
|
||||
r1 := &MsgRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: "msg1",
|
||||
Kind: "msg",
|
||||
MetaParams: DefaultMetaParams,
|
||||
},
|
||||
Priority: "UnrealPriority",
|
||||
}
|
||||
|
||||
r1.Setup(nil, r1, r1)
|
||||
if err := r1.Validate(); err == nil {
|
||||
t.Errorf("validation error is nil")
|
||||
}
|
||||
888
engine/resources/net.go
Normal file
888
engine/resources/net.go
Normal 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
|
||||
}
|
||||
@@ -19,117 +19,127 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
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.
|
||||
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
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *NoopRes) Default() Res {
|
||||
return &NoopRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *NoopRes) Default() engine.Res {
|
||||
return &NoopRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *NoopRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *NoopRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *NoopRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
func (obj *NoopRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Noop resource. Does nothing, returns happy!
|
||||
func (obj *NoopRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if obj.Refresh() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
if obj.init.Refresh() {
|
||||
obj.init.Logf("received a notification!")
|
||||
}
|
||||
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.
|
||||
type NoopUID struct {
|
||||
BaseUID
|
||||
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 *NoopRes) UIDs() []ResUID {
|
||||
func (obj *NoopRes) UIDs() []engine.ResUID {
|
||||
x := &NoopUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
}
|
||||
return []ResUID{x}
|
||||
return []engine.ResUID{x}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !ok {
|
||||
// NOTE: technically we could group a noop into any other
|
||||
// resource, if that resource knew how to handle it, although,
|
||||
// since the mechanics of inter-kind resource grouping are
|
||||
// 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!
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil // noop resources can always be grouped together!
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
104
engine/resources/noop_test.go
Normal file
104
engine/resources/noop_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -20,10 +20,12 @@ package resources
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
"unicode"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
systemdDbus "github.com/coreos/go-systemd/dbus"
|
||||
@@ -36,20 +38,24 @@ import (
|
||||
const (
|
||||
running = "running"
|
||||
stopped = "stopped"
|
||||
dbusInterface = "org.freedesktop.machine1.Manager"
|
||||
machineNew = "org.freedesktop.machine1.Manager.MachineNew"
|
||||
machineRemoved = "org.freedesktop.machine1.Manager.MachineRemoved"
|
||||
dbusMachine1Iface = "org.freedesktop.machine1.Manager"
|
||||
machineNew = dbusMachine1Iface + ".MachineNew"
|
||||
machineRemoved = dbusMachine1Iface + ".MachineRemoved"
|
||||
nspawnServiceTmpl = "systemd-nspawn@%s"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("nspawn", func() Res { return &NspawnRes{} })
|
||||
engine.RegisterResource("nspawn", func() engine.Res { return &NspawnRes{} })
|
||||
}
|
||||
|
||||
// NspawnRes is an nspawn container resource.
|
||||
type NspawnRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"`
|
||||
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"`
|
||||
// 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
|
||||
// would have two watches potentially racing each other and producing
|
||||
@@ -59,11 +65,8 @@ type NspawnRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *NspawnRes) Default() Res {
|
||||
func (obj *NspawnRes) Default() engine.Res {
|
||||
return &NspawnRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
State: running,
|
||||
}
|
||||
}
|
||||
@@ -71,7 +74,7 @@ func (obj *NspawnRes) Default() Res {
|
||||
// makeComposite creates a pointer to a SvcRes. The pointer is used to
|
||||
// validate and initialize the nested svc.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
@@ -80,6 +83,274 @@ func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
|
||||
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.
|
||||
func systemdVersion() (uint16, error) {
|
||||
// check if systemd is running
|
||||
@@ -108,266 +379,3 @@ func systemdVersion() (uint16, error) {
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -22,18 +22,19 @@ package packagekit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
"github.com/godbus/dbus"
|
||||
multierr "github.com/hashicorp/go-multierror"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// global tweaks of verbosity and code path
|
||||
const (
|
||||
Debug = false
|
||||
Paranoid = false // enable if you see any ghosts
|
||||
)
|
||||
|
||||
@@ -47,7 +48,6 @@ const (
|
||||
PkPath = "/org/freedesktop/PackageKit"
|
||||
PkIface = "org.freedesktop.PackageKit"
|
||||
PkIfaceTransaction = PkIface + ".Transaction"
|
||||
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -57,6 +57,7 @@ var (
|
||||
// TODO: add more values
|
||||
// noarch
|
||||
"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)
|
||||
// fedora
|
||||
"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.
|
||||
type Conn struct {
|
||||
conn *dbus.Conn
|
||||
|
||||
Debug bool
|
||||
Logf func(format string, v ...interface{})
|
||||
}
|
||||
|
||||
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in the map values.
|
||||
@@ -173,58 +177,75 @@ func NewBus() *Conn {
|
||||
}
|
||||
|
||||
// GetBus gets the dbus connection object.
|
||||
func (bus *Conn) GetBus() *dbus.Conn {
|
||||
return bus.conn
|
||||
func (obj *Conn) GetBus() *dbus.Conn {
|
||||
return obj.conn
|
||||
}
|
||||
|
||||
// Close closes the dbus connection object.
|
||||
func (bus *Conn) Close() error {
|
||||
return bus.conn.Close()
|
||||
func (obj *Conn) Close() error {
|
||||
return obj.conn.Close()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if Debug {
|
||||
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals)
|
||||
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
|
||||
if obj.Debug {
|
||||
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>
|
||||
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
|
||||
// that many times... Maybe this should be an object singleton?
|
||||
obj := bus.GetBus().BusObject()
|
||||
var call *dbus.Call
|
||||
pathStr := fmt.Sprintf("%s", path)
|
||||
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 {
|
||||
for _, signal := range signals {
|
||||
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'")
|
||||
if call.Err != nil {
|
||||
break
|
||||
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member'%s'", pathStr, iface, signal)
|
||||
argsList = append(argsList, args)
|
||||
if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
|
||||
break // fail if any one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
// 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!
|
||||
bus.GetBus().Signal(ch)
|
||||
return nil
|
||||
obj.GetBus().Signal(ch)
|
||||
return removeSignals, nil
|
||||
}
|
||||
|
||||
// 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)
|
||||
// NOTE: the TransactionListChanged signal fires much more frequently,
|
||||
// but with much less specificity. If we're missing events, report the
|
||||
// issue upstream! The UpdatesChanged signal is what hughsie suggested
|
||||
var signal = "UpdatesChanged"
|
||||
err := bus.matchSignal(ch, PkPath, PkIface, []string{signal})
|
||||
removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if Paranoid { // TODO: this filtering might not be necessary anymore...
|
||||
defer removeSignals() // ignore the error
|
||||
if Paranoid { // TODO: this filtering might not be necessary anymore...
|
||||
// try to handle the filtering inside this function!
|
||||
rch := make(chan *dbus.Signal)
|
||||
go func() {
|
||||
@@ -236,13 +257,13 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
// zero value immediately": if i get nil here,
|
||||
// it means the channel was closed by someone!!
|
||||
if event == nil { // shared bus issue?
|
||||
log.Println("PackageKit: Hrm, channel was closed!")
|
||||
obj.Logf("Hrm, channel was closed!")
|
||||
break loop // TODO: continue?
|
||||
}
|
||||
// i think this was caused by using the shared
|
||||
// bus, but we might as well leave it in for now
|
||||
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
|
||||
}
|
||||
rch <- event // forward...
|
||||
@@ -256,41 +277,45 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
|
||||
}
|
||||
|
||||
// CreateTransaction creates and returns a transaction path.
|
||||
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
||||
if Debug {
|
||||
log.Println("PackageKit: CreateTransaction()")
|
||||
func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) {
|
||||
if obj.Debug {
|
||||
obj.Logf("CreateTransaction()")
|
||||
}
|
||||
var interfacePath dbus.ObjectPath
|
||||
obj := bus.GetBus().Object(PkIface, PkPath)
|
||||
call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
||||
bus := obj.GetBus().Object(PkIface, PkPath)
|
||||
call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
|
||||
if call != nil {
|
||||
return "", call
|
||||
}
|
||||
if Debug {
|
||||
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath)
|
||||
if obj.Debug {
|
||||
obj.Logf("CreateTransaction(): %v", interfacePath)
|
||||
}
|
||||
return interfacePath, nil
|
||||
}
|
||||
|
||||
// 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{}
|
||||
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 {
|
||||
return []string{}, err
|
||||
}
|
||||
|
||||
// add signal matches for Package and Finished which will always be last
|
||||
var signals = []string{"Package", "Finished", "Error", "Destroy"}
|
||||
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if Debug {
|
||||
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath)
|
||||
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
|
||||
if Debug {
|
||||
log.Println("PackageKit: ResolvePackages(): Call: Success!")
|
||||
defer removeSignals()
|
||||
if obj.Debug {
|
||||
obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath)
|
||||
}
|
||||
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 {
|
||||
return []string{}, call.Err
|
||||
@@ -300,11 +325,11 @@ loop:
|
||||
// FIXME: add a timeout option to error in case signals are dropped!
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if Debug {
|
||||
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal)
|
||||
if obj.Debug {
|
||||
obj.Logf("ResolvePackages(): Signal: %+v", signal)
|
||||
}
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -337,10 +362,10 @@ loop:
|
||||
}
|
||||
|
||||
// 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
|
||||
filter += PkFilterEnumArch // always search in our arch
|
||||
packageIDs, e := bus.ResolvePackages(packages, filter)
|
||||
packageIDs, e := obj.ResolvePackages(packages, filter)
|
||||
if e != nil {
|
||||
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.
|
||||
// TODO: this could be optimized by making the resolve call directly
|
||||
func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
p, e := bus.IsInstalledList([]string{pkg})
|
||||
func (obj *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
p, e := obj.IsInstalledList([]string{pkg})
|
||||
if len(p) != 1 {
|
||||
return false, e
|
||||
}
|
||||
@@ -384,23 +409,27 @@ func (bus *Conn) IsInstalled(pkg string) (bool, error) {
|
||||
}
|
||||
|
||||
// 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 :(
|
||||
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
|
||||
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
call := obj.Call(FmtTransactionMethod("RefreshCache"), 0, false)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
call = obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
||||
call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -411,7 +440,7 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -431,30 +460,34 @@ loop:
|
||||
}
|
||||
case <-util.TimeAfterOrBlock(timeout):
|
||||
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 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.
|
||||
func (bus *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
|
||||
|
||||
var allowDeps = true // TODO: configurable
|
||||
var autoremove = false // unsupported on GNU/Linux
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
call := obj.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -464,7 +497,7 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -488,18 +521,22 @@ loop:
|
||||
}
|
||||
|
||||
// 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 :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
call := obj.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
|
||||
if call.Err != nil {
|
||||
return call.Err
|
||||
}
|
||||
@@ -509,7 +546,7 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -531,20 +568,24 @@ loop:
|
||||
}
|
||||
|
||||
// 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
|
||||
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
call := obj.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
|
||||
if call.Err != nil {
|
||||
err = call.Err
|
||||
return
|
||||
@@ -557,7 +598,7 @@ loop:
|
||||
case signal := <-ch:
|
||||
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -596,22 +637,26 @@ loop:
|
||||
}
|
||||
|
||||
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
|
||||
func (bus *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||
if Debug {
|
||||
log.Println("PackageKit: GetUpdates()")
|
||||
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
|
||||
if obj.Debug {
|
||||
obj.Logf("GetUpdates()")
|
||||
}
|
||||
packageIDs := []string{}
|
||||
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
|
||||
interfacePath, err := bus.CreateTransaction()
|
||||
interfacePath, err := obj.CreateTransaction()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
call := obj.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
||||
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
|
||||
call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
|
||||
if call.Err != nil {
|
||||
return nil, call.Err
|
||||
}
|
||||
@@ -621,7 +666,7 @@ loop:
|
||||
select {
|
||||
case signal := <-ch:
|
||||
if signal.Path != interfacePath {
|
||||
log.Printf("PackageKit: Woops: Signal.Path: %+v", signal.Path)
|
||||
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
|
||||
continue loop
|
||||
}
|
||||
|
||||
@@ -660,7 +705,7 @@ loop:
|
||||
// outside mgmt. The packageMap input has the package names as keys and
|
||||
// requested states as values. These states can be: installed, uninstalled,
|
||||
// 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
|
||||
packages := make([]string, len(packageMap))
|
||||
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
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Printf("PackageKit: PackagesToPackageIDs(): %v", strings.Join(packages, ", "))
|
||||
if obj.Debug {
|
||||
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
|
||||
}
|
||||
resolved, e := bus.ResolvePackages(packages, filter)
|
||||
resolved, e := obj.ResolvePackages(packages, filter)
|
||||
if e != nil {
|
||||
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 {
|
||||
index = -1
|
||||
//log.Printf("* %v", packageID)
|
||||
//obj.Logf("* %v", packageID)
|
||||
// format is: name;version;arch;data
|
||||
s := strings.Split(packageID, ";")
|
||||
//if len(s) != 4 { continue } // this would be a bug!
|
||||
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
|
||||
if !IsMyArch(arch) {
|
||||
b, err := IsMyArch(arch)
|
||||
if err != nil {
|
||||
return nil, errwrap.Wrapf(err, "arch error")
|
||||
} else if !b {
|
||||
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!
|
||||
// if something isn't installed, we can't verify it with this method
|
||||
// FIXME: https://github.com/hughsie/PackageKit/issues/116
|
||||
updates, e := bus.GetUpdates(filter)
|
||||
updates, e := obj.GetUpdates(filter)
|
||||
if e != nil {
|
||||
return nil, fmt.Errorf("Updates error: %v", e)
|
||||
}
|
||||
for _, packageID := range updates {
|
||||
//log.Printf("* %v", packageID)
|
||||
//obj.Logf("* %v", packageID)
|
||||
// format is: name;version;arch;data
|
||||
s := strings.Split(packageID, ";")
|
||||
//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...
|
||||
//resolved, e := bus.ResolvePackages(..., filter+PkFilterEnumNewest)
|
||||
//resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest)
|
||||
// but that's basically what recursion here could do too!
|
||||
if len(checkPackages) > 0 {
|
||||
if Debug {
|
||||
log.Printf("PackageKit: PackagesToPackageIDs(): Recurse: %v", strings.Join(checkPackages, ", "))
|
||||
if obj.Debug {
|
||||
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 {
|
||||
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) {
|
||||
result := []string{}
|
||||
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!
|
||||
if !ok || !obj.Found || obj.PackageID == "" {
|
||||
if !ok || !p.Found || p.PackageID == "" {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
result = append(result, obj.PackageID)
|
||||
result = append(result, p.PackageID)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -849,18 +897,18 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
||||
result = make(map[string]bool)
|
||||
pkgs := []string{} // bad pkgs that don't have a bool state
|
||||
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!
|
||||
if !ok || !obj.Found {
|
||||
if !ok || !p.Found {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
var b bool
|
||||
if state == "installed" {
|
||||
b = obj.Installed
|
||||
b = p.Installed
|
||||
} else if state == "uninstalled" {
|
||||
b = !obj.Installed
|
||||
b = !p.Installed
|
||||
} else if state == "newest" {
|
||||
b = obj.Newest
|
||||
b = p.Newest
|
||||
} else {
|
||||
// we can't filter "version" state in this function
|
||||
pkgs = append(pkgs, k)
|
||||
@@ -869,7 +917,7 @@ func FilterState(m map[string]*PkPackageIDActionData, packages []string, state s
|
||||
result[k] = b // save
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -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) {
|
||||
result = []string{}
|
||||
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!
|
||||
if !ok || !obj.Found {
|
||||
if !ok || !p.Found {
|
||||
return nil, fmt.Errorf("can't find package named '%s'", k)
|
||||
}
|
||||
b := false
|
||||
if state == "installed" && obj.Installed {
|
||||
if state == "installed" && p.Installed {
|
||||
b = true
|
||||
} else if state == "uninstalled" && !obj.Installed {
|
||||
} else if state == "uninstalled" && !p.Installed {
|
||||
b = true
|
||||
} else if state == "newest" && obj.Newest {
|
||||
} else if state == "newest" && p.Newest {
|
||||
b = true
|
||||
} else if state == obj.Version {
|
||||
} else if state == p.Version {
|
||||
b = true
|
||||
}
|
||||
if b {
|
||||
@@ -917,14 +965,14 @@ func FmtTransactionMethod(method string) string {
|
||||
}
|
||||
|
||||
// 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]
|
||||
if !ok {
|
||||
// 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
|
||||
return true
|
||||
return true, nil
|
||||
}
|
||||
return goarch == runtime.GOARCH
|
||||
return goarch == runtime.GOARCH, nil
|
||||
}
|
||||
@@ -21,19 +21,20 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("password", func() Res { return &PasswordRes{} })
|
||||
engine.RegisterResource("password", func() engine.Res { return &PasswordRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -43,43 +44,54 @@ const (
|
||||
|
||||
// PasswordRes is a no-op resource that returns a random password string.
|
||||
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?
|
||||
Length uint16 `yaml:"length"` // number of characters to return
|
||||
Saved bool // this caches the password in the clear locally
|
||||
CheckRecovery bool // recovery from integrity checks by re-generating
|
||||
Password *string // the generated password, read only, do not set!
|
||||
Length uint16 `yaml:"length"` // number of characters to return
|
||||
Saved bool // this caches the password in the clear locally
|
||||
CheckRecovery bool // recovery from integrity checks by re-generating
|
||||
|
||||
path string // the path to local storage
|
||||
recWatcher *recwatch.RecWatcher
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *PasswordRes) Default() Res {
|
||||
func (obj *PasswordRes) Default() engine.Res {
|
||||
return &PasswordRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
Length: 64, // safe default
|
||||
}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
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
|
||||
// will save this into a local file. It will load it back in from previous runs.
|
||||
func (obj *PasswordRes) Init() error {
|
||||
// Init runs some startup code for this resource. It generates a new password
|
||||
// for this resource if one was not provided. It will save this into a local
|
||||
// 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 {
|
||||
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||
}
|
||||
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) {
|
||||
@@ -171,12 +183,11 @@ func (obj *PasswordRes) Watch() error {
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
for {
|
||||
select {
|
||||
// NOTE: this part is very similar to the file resource code
|
||||
@@ -188,30 +199,33 @@ func (obj *PasswordRes) Watch() error {
|
||||
return errwrap.Wrapf(err, "unknown %s watcher error", obj)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
// we avoid sending events on unpause
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
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
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Password resource. Does nothing, returns happy!
|
||||
func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
|
||||
var refresh = obj.Refresh() // do we have a pending reload to apply?
|
||||
var exists = true // does the file (aka the token) exist?
|
||||
var generate bool // do we need to generate a new password?
|
||||
var write bool // do we need to write out to disk?
|
||||
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||
var exists = true // does the file (aka the token) exist?
|
||||
var generate bool // do we need to generate a new password?
|
||||
var write bool // do we need to write out to disk?
|
||||
|
||||
password, err := obj.read() // password might be empty if just a token
|
||||
if err != nil {
|
||||
@@ -226,7 +240,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if !obj.CheckRecovery {
|
||||
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
|
||||
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
|
||||
if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
||||
write = true
|
||||
}
|
||||
//if p := obj.Password; obj.Saved && (p != nil && *p != password) {
|
||||
// write = true
|
||||
//}
|
||||
|
||||
if !refresh && exists && !generate && !write { // nothing to do, done!
|
||||
return true, nil
|
||||
@@ -260,13 +274,18 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
// generate the actual password
|
||||
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!
|
||||
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
|
||||
|
||||
@@ -277,7 +296,7 @@ func (obj *PasswordRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
output = 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// PasswordUID is the UID struct for PasswordRes.
|
||||
type PasswordUID struct {
|
||||
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() []ResUID {
|
||||
x := &PasswordUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PasswordRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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!
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*PasswordRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Length != res.Length {
|
||||
return false
|
||||
@@ -342,6 +336,37 @@ func (obj *PasswordRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *PasswordRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -19,23 +19,29 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"path"
|
||||
"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"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("pkg", func() Res { return &PkgRes{} })
|
||||
engine.RegisterResource("pkg", func() engine.Res { return &PkgRes{} })
|
||||
}
|
||||
|
||||
// PkgRes is a package resource for packagekit.
|
||||
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>
|
||||
AllowUntrusted bool `yaml:"allowuntrusted"` // allow untrusted packages to be installed?
|
||||
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.
|
||||
func (obj *PkgRes) Default() Res {
|
||||
func (obj *PkgRes) Default() engine.Res {
|
||||
return &PkgRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
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 obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *PkgRes) Init() error {
|
||||
if err := obj.BaseRes.Init(); err != nil { // call base init, b/c we're overriding
|
||||
return err
|
||||
}
|
||||
func (obj *PkgRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
if obj.fileList == nil {
|
||||
if err := obj.populateFileList(); err != nil {
|
||||
@@ -78,6 +79,11 @@ func (obj *PkgRes) Init() error {
|
||||
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.
|
||||
// It uses the PackageKit UpdatesChanged signal to watch for changes.
|
||||
// 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")
|
||||
}
|
||||
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()
|
||||
if err != nil {
|
||||
@@ -95,23 +105,21 @@ func (obj *PkgRes) Watch() error {
|
||||
}
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching...", obj.fmtNames(obj.getNames()))
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("%s: Watching...", obj.fmtNames(obj.getNames()))
|
||||
}
|
||||
|
||||
select {
|
||||
case event := <-ch:
|
||||
// FIXME: ask packagekit for info on what packages changed
|
||||
if obj.debug {
|
||||
log.Printf("%s: Event: %v", obj.fmtNames(obj.getNames()), event.Name)
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Event(%s): %s", event.Name, obj.fmtNames(obj.getNames()))
|
||||
}
|
||||
|
||||
// since the chan is buffered, remove any supplemental
|
||||
@@ -121,20 +129,20 @@ func (obj *PkgRes) Watch() error {
|
||||
}
|
||||
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event := <-obj.init.Events:
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
//obj.StateOK(false) // these events don't invalidate state
|
||||
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
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
|
||||
func (obj *PkgRes) getNames() []string {
|
||||
if g := obj.GetGroup(); len(g) > 0 { // grouped elements
|
||||
names := []string{obj.GetName()}
|
||||
names := []string{obj.Name()}
|
||||
for _, x := range g {
|
||||
pkg, ok := x.(*PkgRes) // convert from Res
|
||||
if ok {
|
||||
names = append(names, pkg.Name)
|
||||
names = append(names, pkg.Name())
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
return []string{obj.GetName()}
|
||||
return []string{obj.Name()}
|
||||
}
|
||||
|
||||
// pretty print for header values
|
||||
func (obj *PkgRes) fmtNames(names []string) string {
|
||||
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()
|
||||
}
|
||||
@@ -168,9 +176,9 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
|
||||
for _, x := range g {
|
||||
pkg, ok := x.(*PkgRes) // convert from Res
|
||||
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
|
||||
@@ -178,7 +186,7 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
|
||||
|
||||
func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) {
|
||||
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
|
||||
filter += packagekit.PkFilterEnumArch // always search in our arch (optional!)
|
||||
// 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")
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
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!
|
||||
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
|
||||
@@ -237,13 +251,17 @@ func (obj *PkgRes) populateFileList() error {
|
||||
// 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.
|
||||
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()
|
||||
if bus == nil {
|
||||
return false, fmt.Errorf("can't connect to PackageKit bus")
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
@@ -251,7 +269,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
|
||||
packageMap := obj.groupMappingHelper() // map[string]string
|
||||
packageList := []string{obj.Name}
|
||||
packageList := []string{obj.Name()}
|
||||
packageList = append(packageList, util.StrMapKeys(packageMap)...)
|
||||
//stateList := []string{obj.State}
|
||||
//stateList = append(stateList, util.StrMapValues(packageMap)...)
|
||||
@@ -262,7 +280,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if err != nil {
|
||||
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))
|
||||
|
||||
// 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
|
||||
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)
|
||||
if err != nil {
|
||||
return false, err // fail
|
||||
@@ -301,7 +319,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted
|
||||
}
|
||||
// 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 {
|
||||
case "uninstalled": // run remove
|
||||
// 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 {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.
|
||||
type PkgUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
name string // pkg name
|
||||
state string // pkg state or "version"
|
||||
}
|
||||
|
||||
// PkgFileUID is the UID struct for PkgRes files.
|
||||
type PkgFileUID struct {
|
||||
BaseUID
|
||||
engine.BaseUID
|
||||
path string // path of the file
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !ok {
|
||||
return false
|
||||
@@ -349,16 +403,16 @@ func (obj *PkgUID) IFF(uid ResUID) bool {
|
||||
// PkgResAutoEdges holds the state of the auto edge generator.
|
||||
type PkgResAutoEdges struct {
|
||||
fileList []string
|
||||
svcUIDs []ResUID
|
||||
svcUIDs []engine.ResUID
|
||||
testIsNext bool // safety
|
||||
name string // saved data from PkgRes obj
|
||||
kind string
|
||||
}
|
||||
|
||||
// Next returns the next automatic edge.
|
||||
func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
func (obj *PkgResAutoEdges) Next() []engine.ResUID {
|
||||
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
|
||||
|
||||
@@ -367,12 +421,12 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
return x
|
||||
}
|
||||
|
||||
var result []ResUID
|
||||
var result []engine.ResUID
|
||||
// return UID's for whatever is in obj.fileList
|
||||
for _, x := range obj.fileList {
|
||||
var reversed = false // cheat by passing a pointer
|
||||
result = append(result, &FileUID{
|
||||
BaseUID: BaseUID{
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.name,
|
||||
Kind: obj.kind,
|
||||
Reversed: &reversed,
|
||||
@@ -386,22 +440,22 @@ func (obj *PkgResAutoEdges) Next() []ResUID {
|
||||
// Test gets results of the earlier Next() call, & returns if we should continue!
|
||||
func (obj *PkgResAutoEdges) Test(input []bool) bool {
|
||||
if !obj.testIsNext {
|
||||
log.Fatal("expecting a call to Next()")
|
||||
panic("expecting a call to Next()")
|
||||
}
|
||||
|
||||
// ack the svcUID's...
|
||||
if x := obj.svcUIDs; len(x) > 0 {
|
||||
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
|
||||
return true
|
||||
}
|
||||
|
||||
count := len(obj.fileList)
|
||||
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
|
||||
|
||||
@@ -436,7 +490,7 @@ func (obj *PkgResAutoEdges) Test(input []bool) bool {
|
||||
|
||||
// AutoEdges produces an object which generates a minimal pkg file optimization
|
||||
// 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
|
||||
// more of the mechanics, most of the AutoEdge mechanics for the PkgRes
|
||||
// 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!
|
||||
var svcUIDs []ResUID
|
||||
var svcUIDs []engine.ResUID
|
||||
for _, x := range ReturnSvcInFileList(obj.fileList) {
|
||||
var reversed = false
|
||||
svcUIDs = append(svcUIDs, &SvcUID{
|
||||
BaseUID: BaseUID{
|
||||
Name: obj.GetName(),
|
||||
Kind: obj.GetKind(),
|
||||
BaseUID: engine.BaseUID{
|
||||
Name: obj.Name(),
|
||||
Kind: obj.Kind(),
|
||||
Reversed: &reversed,
|
||||
},
|
||||
name: x, // the svc name itself in the SvcUID object!
|
||||
@@ -464,25 +518,25 @@ func (obj *PkgRes) AutoEdges() (AutoEdge, error) {
|
||||
return &PkgResAutoEdges{
|
||||
fileList: util.RemoveCommonFilePrefixes(obj.fileList), // clean start!
|
||||
svcUIDs: svcUIDs,
|
||||
testIsNext: false, // start with Next() call
|
||||
name: obj.GetName(), // save data for PkgResAutoEdges obj
|
||||
kind: obj.GetKind(),
|
||||
testIsNext: false, // start with Next() call
|
||||
name: obj.Name(), // save data for PkgResAutoEdges obj
|
||||
kind: obj.Kind(),
|
||||
}, 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 *PkgRes) UIDs() []ResUID {
|
||||
func (obj *PkgRes) UIDs() []engine.ResUID {
|
||||
x := &PkgUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
name: obj.Name(),
|
||||
state: obj.State,
|
||||
}
|
||||
result := []ResUID{x}
|
||||
result := []engine.ResUID{x}
|
||||
|
||||
for _, y := range obj.fileList {
|
||||
y := &PkgFileUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
|
||||
path: 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.
|
||||
// can these two resources be merged ?
|
||||
// (aka does this resource support doing so?)
|
||||
// will resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *PkgRes) GroupCmp(r Res) bool {
|
||||
// Can these two resources be merged, aka, does this resource support doing so?
|
||||
// Will resource allow itself to be grouped _into_ this obj?
|
||||
func (obj *PkgRes) GroupCmp(r engine.GroupableRes) error {
|
||||
res, ok := r.(*PkgRes)
|
||||
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
|
||||
resStateIsVersion := (res.State != "installed" && res.State != "uninstalled" && res.State != "newest") // must be a ver. string
|
||||
if objStateIsVersion || resStateIsVersion {
|
||||
// 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
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
return fmt.Errorf("resource is of a different state")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct.
|
||||
@@ -15,6 +15,8 @@
|
||||
// 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 (
|
||||
@@ -19,130 +19,119 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
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.
|
||||
// It will also display a message when it receives a notification. It supports
|
||||
// automatic grouping.
|
||||
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
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *PrintRes) Default() Res {
|
||||
return &PrintRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *PrintRes) Default() engine.Res {
|
||||
return &PrintRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
func (obj *PrintRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *PrintRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *PrintRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
func (obj *PrintRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Print resource. Does nothing, returns happy!
|
||||
func (obj *PrintRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply: %t", obj, apply)
|
||||
if val, exists := obj.Recv["Msg"]; exists && val.Changed {
|
||||
obj.init.Logf("CheckApply: %t", apply)
|
||||
if val, exists := obj.init.Recv()["Msg"]; exists && val.Changed {
|
||||
// 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() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
if obj.init.Refresh() {
|
||||
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
|
||||
for _, x := range g {
|
||||
print, ok := x.(*PrintRes) // convert from Res
|
||||
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
|
||||
}
|
||||
|
||||
// PrintUID is the UID struct for PrintRes.
|
||||
type PrintUID struct {
|
||||
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() []ResUID {
|
||||
x := &PrintUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *PrintRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*PrintRes)
|
||||
if !ok {
|
||||
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 {
|
||||
return false
|
||||
@@ -150,6 +139,31 @@ func (obj *PrintRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *PrintRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -21,8 +21,9 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
systemd "github.com/coreos/go-systemd/dbus" // change namespace
|
||||
@@ -32,24 +33,26 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("svc", func() Res { return &SvcRes{} })
|
||||
engine.RegisterResource("svc", func() engine.Res { return &SvcRes{} })
|
||||
}
|
||||
|
||||
// SvcRes is a service resource for systemd units.
|
||||
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
|
||||
Startup string `yaml:"startup"` // enabled, disabled, undefined
|
||||
Session bool `yaml:"session"` // user session (true) or system?
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *SvcRes) Default() Res {
|
||||
return &SvcRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *SvcRes) Default() engine.Res {
|
||||
return &SvcRes{}
|
||||
}
|
||||
|
||||
// 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 != "" {
|
||||
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.
|
||||
func (obj *SvcRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *SvcRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
@@ -101,16 +111,15 @@ func (obj *SvcRes) Watch() error {
|
||||
bus.Signal(buschan)
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var svc = fmt.Sprintf("%s.service", obj.Name) // systemd name
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
var invalid = false // does the svc exist or not?
|
||||
var previous bool // previous invalid value
|
||||
set := conn.NewSubscriptionSet() // no error should be returned
|
||||
var svc = fmt.Sprintf("%s.service", obj.Name()) // systemd name
|
||||
var send = false // send event?
|
||||
var invalid = false // does the svc exist or not?
|
||||
var previous bool // previous invalid value
|
||||
set := conn.NewSubscriptionSet() // no error should be returned
|
||||
subChannel, subErrors := set.Subscribe()
|
||||
var activeSet = false
|
||||
|
||||
@@ -124,25 +133,25 @@ func (obj *SvcRes) Watch() error {
|
||||
// firstly, does svc even exist or not?
|
||||
loadstate, err := conn.GetUnitProperty(svc, "LoadState")
|
||||
if err != nil {
|
||||
log.Printf("Failed to get property: %v", err)
|
||||
obj.init.Logf("failed to get property: %+v", err)
|
||||
invalid = true
|
||||
}
|
||||
|
||||
if !invalid {
|
||||
var notFound = (loadstate.Value == dbus.MakeVariant("not-found"))
|
||||
if notFound { // XXX: in the loop we'll handle changes better...
|
||||
log.Printf("Failed to find svc: %s", svc)
|
||||
obj.init.Logf("failed to find svc")
|
||||
invalid = true // XXX: ?
|
||||
}
|
||||
}
|
||||
|
||||
if previous != invalid { // if invalid changed, send signal
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
}
|
||||
|
||||
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 {
|
||||
activeSet = false
|
||||
set.Remove(svc) // no return value should ever occur
|
||||
@@ -151,11 +160,11 @@ func (obj *SvcRes) Watch() error {
|
||||
select {
|
||||
case <-buschan: // XXX: wait for new units event to unstick
|
||||
// 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():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event := <-obj.init.Events:
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -164,47 +173,53 @@ func (obj *SvcRes) Watch() error {
|
||||
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 {
|
||||
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...
|
||||
if event[svc] != nil {
|
||||
// event[svc].ActiveState is not nil
|
||||
|
||||
switch event[svc].ActiveState {
|
||||
case "active":
|
||||
log.Printf("Svc[%s]->Started", svc)
|
||||
obj.init.Logf("started")
|
||||
case "inactive":
|
||||
log.Printf("Svc[%s]->Stopped", svc)
|
||||
obj.init.Logf("stopped")
|
||||
case "reloading":
|
||||
log.Printf("Svc[%s]->Reloading", svc)
|
||||
obj.init.Logf("reloading")
|
||||
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:
|
||||
log.Fatalf("Unknown svc state: %s", event[svc].ActiveState)
|
||||
return fmt.Errorf("unknown svc state: %s", event[svc].ActiveState)
|
||||
}
|
||||
} else {
|
||||
// svc stopped (and ActiveState is nil...)
|
||||
log.Printf("Svc[%s]->Stopped", svc)
|
||||
obj.init.Logf("stopped")
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case err := <-subErrors:
|
||||
return errwrap.Wrapf(err, "unknown %s error", obj)
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event := <-obj.init.Events:
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if send {
|
||||
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()
|
||||
|
||||
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")
|
||||
if err != nil {
|
||||
@@ -251,8 +266,8 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
|
||||
var running = (activestate.Value == dbus.MakeVariant("active"))
|
||||
var stateOK = ((obj.State == "") || (obj.State == "running" && running) || (obj.State == "stopped" && !running))
|
||||
var startupOK = true // XXX: DETECT AND SET
|
||||
var refresh = obj.Refresh() // do we have a pending reload to apply?
|
||||
var startupOK = true // XXX: DETECT AND SET
|
||||
var refresh = obj.init.Refresh() // do we have a pending reload to apply?
|
||||
|
||||
if stateOK && startupOK && !refresh {
|
||||
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
|
||||
log.Printf("%s: Apply", obj)
|
||||
obj.init.Logf("Apply")
|
||||
var files = []string{svc} // the svc represented in a list
|
||||
if obj.Startup == "enabled" {
|
||||
_, _, 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")
|
||||
}
|
||||
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
|
||||
} 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")
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -310,7 +325,7 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
|
||||
if refresh { // we need to reload the service
|
||||
// XXX: run a svc reload here!
|
||||
log.Printf("%s: Reloading...", obj)
|
||||
obj.init.Logf("Reloading...")
|
||||
}
|
||||
|
||||
// XXX: also set enabled on boot
|
||||
@@ -318,124 +333,21 @@ func (obj *SvcRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil // success
|
||||
}
|
||||
|
||||
// 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.
|
||||
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
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *SvcRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*SvcRes)
|
||||
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
|
||||
@@ -450,6 +362,111 @@ func (obj *SvcRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *SvcRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -19,17 +19,25 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
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.
|
||||
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"`
|
||||
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
|
||||
AlwaysGroup bool `lang:"alwaysgroup" yaml:"always_group"` // set to true to cause auto grouping
|
||||
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!
|
||||
|
||||
@@ -87,12 +96,8 @@ type TestRes struct {
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *TestRes) Default() Res {
|
||||
return &TestRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *TestRes) Default() engine.Res {
|
||||
return &TestRes{}
|
||||
}
|
||||
|
||||
// Validate if the params passed in are valid data.
|
||||
@@ -103,128 +108,129 @@ func (obj *TestRes) Validate() error {
|
||||
if s := obj.ValidateError; 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.
|
||||
func (obj *TestRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
func (obj *TestRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
func (obj *TestRes) Watch() error {
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for Test resource. Does nothing, returns happy!
|
||||
func (obj *TestRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
log.Printf("%s: CheckApply: %t", obj, apply)
|
||||
if obj.Refresh() {
|
||||
log.Printf("%s: Received a notification!", obj)
|
||||
for key, val := range obj.init.Recv() {
|
||||
obj.init.Logf("CheckApply: Received `%s`, changed: %t", key, val.Changed)
|
||||
}
|
||||
|
||||
log.Printf("%s: Bool: %v", obj, obj.Bool)
|
||||
log.Printf("%s: Str: %v", obj, obj.Str)
|
||||
if obj.init.Refresh() {
|
||||
obj.init.Logf("Received a notification!")
|
||||
}
|
||||
|
||||
log.Printf("%s: Int: %v", obj, obj.Int)
|
||||
log.Printf("%s: Int8: %v", obj, obj.Int8)
|
||||
log.Printf("%s: Int16: %v", obj, obj.Int16)
|
||||
log.Printf("%s: Int32: %v", obj, obj.Int32)
|
||||
log.Printf("%s: Int64: %v", obj, obj.Int64)
|
||||
obj.init.Logf("%s: Bool: %v", obj, obj.Bool)
|
||||
obj.init.Logf("%s: Str: %v", obj, obj.Str)
|
||||
|
||||
log.Printf("%s: Uint: %v", obj, obj.Uint)
|
||||
log.Printf("%s: Uint8: %v", obj, obj.Uint)
|
||||
log.Printf("%s: Uint16: %v", obj, obj.Uint)
|
||||
log.Printf("%s: Uint32: %v", obj, obj.Uint)
|
||||
log.Printf("%s: Uint64: %v", obj, obj.Uint)
|
||||
obj.init.Logf("%s: Int: %v", obj, obj.Int)
|
||||
obj.init.Logf("%s: Int8: %v", obj, obj.Int8)
|
||||
obj.init.Logf("%s: Int16: %v", obj, obj.Int16)
|
||||
obj.init.Logf("%s: Int32: %v", obj, obj.Int32)
|
||||
obj.init.Logf("%s: Int64: %v", obj, obj.Int64)
|
||||
|
||||
//log.Printf("%s: Uintptr: %v", obj, obj.Uintptr)
|
||||
log.Printf("%s: Byte: %v", obj, obj.Byte)
|
||||
log.Printf("%s: Rune: %v", obj, obj.Rune)
|
||||
obj.init.Logf("%s: Uint: %v", obj, obj.Uint)
|
||||
obj.init.Logf("%s: Uint8: %v", obj, obj.Uint)
|
||||
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)
|
||||
log.Printf("%s: Float64: %v", obj, obj.Float64)
|
||||
log.Printf("%s: Complex64: %v", obj, obj.Complex64)
|
||||
log.Printf("%s: Complex128: %v", obj, obj.Complex128)
|
||||
//obj.init.Logf("%s: Uintptr: %v", obj, obj.Uintptr)
|
||||
obj.init.Logf("%s: Byte: %v", obj, obj.Byte)
|
||||
obj.init.Logf("%s: Rune: %v", obj, obj.Rune)
|
||||
|
||||
log.Printf("%s: BoolPtr: %v", obj, obj.BoolPtr)
|
||||
log.Printf("%s: StringPtr: %v", obj, obj.StringPtr)
|
||||
log.Printf("%s: Int64Ptr: %v", obj, obj.Int64Ptr)
|
||||
log.Printf("%s: Int8Ptr: %v", obj, obj.Int8Ptr)
|
||||
log.Printf("%s: Uint8Ptr: %v", obj, obj.Uint8Ptr)
|
||||
obj.init.Logf("%s: Float32: %v", obj, obj.Float32)
|
||||
obj.init.Logf("%s: Float64: %v", obj, obj.Float64)
|
||||
obj.init.Logf("%s: Complex64: %v", obj, obj.Complex64)
|
||||
obj.init.Logf("%s: Complex128: %v", obj, obj.Complex128)
|
||||
|
||||
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)
|
||||
log.Printf("%s: MapIntFloat: %v", obj, obj.MapIntFloat)
|
||||
log.Printf("%s: MixedStruct: %v", obj, obj.MixedStruct)
|
||||
log.Printf("%s: Interface: %v", obj, obj.Interface)
|
||||
obj.init.Logf("%s: Int8PtrPtrPtr: %v", obj, obj.Int8PtrPtrPtr)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// TestUID is the UID struct for TestRes.
|
||||
type TestUID struct {
|
||||
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() []ResUID {
|
||||
x := &TestUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *TestRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*TestRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// calling base Compare is probably unneeded for the test res, but do it
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
//if obj.Name != res.Name {
|
||||
// return false
|
||||
//}
|
||||
|
||||
if obj.CompareFail || res.CompareFail {
|
||||
return false
|
||||
@@ -368,6 +374,9 @@ func (obj *TestRes) Compare(r Res) bool {
|
||||
if obj.AlwaysGroup != res.AlwaysGroup {
|
||||
return false
|
||||
}
|
||||
if obj.SendValue != res.SendValue {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Comment != res.Comment {
|
||||
return false
|
||||
@@ -376,6 +385,50 @@ func (obj *TestRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *TestRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
177
engine/resources/test_test.go
Normal file
177
engine/resources/test_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -19,46 +19,49 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
)
|
||||
|
||||
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
|
||||
// interval seconds.
|
||||
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
|
||||
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
// TimerUID is the UID struct for TimerRes.
|
||||
type TimerUID struct {
|
||||
BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *TimerRes) Default() Res {
|
||||
return &TimerRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *TimerRes) Default() engine.Res {
|
||||
return &TimerRes{}
|
||||
}
|
||||
|
||||
// Validate the params that are passed to TimerRes.
|
||||
func (obj *TimerRes) Validate() error {
|
||||
return obj.BaseRes.Validate()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *TimerRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overrriding
|
||||
func (obj *TimerRes) Init(init *engine.Init) error {
|
||||
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
|
||||
@@ -73,27 +76,31 @@ func (obj *TimerRes) Watch() error {
|
||||
defer obj.ticker.Stop()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case <-obj.ticker.C: // received the timer event
|
||||
send = true
|
||||
log.Printf("%s: received tick", obj)
|
||||
obj.init.Logf("received tick")
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, _ := obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := obj.init.Read(event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if send {
|
||||
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) {
|
||||
// because there are no checks to run, this resource has a less
|
||||
// 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
|
||||
} else if !apply { // we had a refresh to do
|
||||
return false, nil // therefore state is wrong
|
||||
@@ -114,32 +121,21 @@ func (obj *TimerRes) CheckApply(apply bool) (bool, error) {
|
||||
return 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 *TimerRes) UIDs() []ResUID {
|
||||
x := &TimerUID{
|
||||
BaseUID: BaseUID{
|
||||
Name: obj.GetName(),
|
||||
Kind: obj.GetKind(),
|
||||
},
|
||||
name: obj.Name,
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *TimerRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*TimerRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) {
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.Interval != res.Interval {
|
||||
return false
|
||||
@@ -148,6 +144,23 @@ func (obj *TimerRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *TimerRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -20,7 +20,6 @@ package resources
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"sort"
|
||||
@@ -28,20 +27,26 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("user", func() Res { return &UserRes{} })
|
||||
engine.RegisterResource("user", func() engine.Res { return &UserRes{} })
|
||||
}
|
||||
|
||||
const passwdFile = "/etc/passwd"
|
||||
|
||||
// UserRes is a user account resource.
|
||||
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
|
||||
UID *uint32 `yaml:"uid"` // uid must be unique unless AllowDuplicateUID is true
|
||||
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.
|
||||
func (obj *UserRes) Default() Res {
|
||||
return &UserRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
}
|
||||
func (obj *UserRes) Default() engine.Res {
|
||||
return &UserRes{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (obj *UserRes) Init() error {
|
||||
return obj.BaseRes.Init() // call base init, b/c we're overriding
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *UserRes) Init(init *engine.Init) error {
|
||||
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.
|
||||
@@ -112,16 +120,14 @@ func (obj *UserRes) Watch() error {
|
||||
defer obj.recWatcher.Close()
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit *error
|
||||
|
||||
for {
|
||||
if obj.debug {
|
||||
log.Printf("%s: Watching: %s", obj, passwdFile) // attempting to watch...
|
||||
if obj.init.Debug {
|
||||
obj.init.Logf("Watching: %s", passwdFile) // attempting to watch...
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -132,33 +138,37 @@ func (obj *UserRes) Watch() error {
|
||||
if err := event.Error; err != nil {
|
||||
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
||||
}
|
||||
if obj.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)
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
case event, ok := <-obj.init.Events:
|
||||
if !ok {
|
||||
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
|
||||
if send {
|
||||
send = false
|
||||
obj.Event()
|
||||
if err := obj.init.Event(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply method for User resource.
|
||||
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
|
||||
usr, err := user.Lookup(obj.GetName())
|
||||
usr, err := user.Lookup(obj.Name())
|
||||
if err != nil {
|
||||
if _, ok := err.(user.UnknownUserError); !ok {
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -213,10 +223,10 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
if obj.State == "exists" {
|
||||
if exists {
|
||||
cmdName = "usermod"
|
||||
log.Printf("%s: Modifying user: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Modifying user: %s", obj.Name())
|
||||
} else {
|
||||
cmdName = "useradd"
|
||||
log.Printf("%s: Adding user: %s", obj, obj.GetName())
|
||||
obj.init.Logf("Adding user: %s", obj.Name())
|
||||
}
|
||||
if obj.AllowDuplicateUID {
|
||||
args = append(args, "--non-unique")
|
||||
@@ -239,10 +249,10 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
}
|
||||
if obj.State == "absent" {
|
||||
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.SysProcAttr = &syscall.SysProcAttr{
|
||||
@@ -273,113 +283,22 @@ func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// UserUID is the UID struct for UserRes.
|
||||
type UserUID struct {
|
||||
BaseUID
|
||||
name string
|
||||
}
|
||||
|
||||
// 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
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *UserRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*UserRes)
|
||||
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
|
||||
}
|
||||
@@ -430,6 +349,91 @@ func (obj *UserRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *UserRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -21,26 +21,23 @@ package resources
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os/user"
|
||||
"path"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
multierr "github.com/hashicorp/go-multierror"
|
||||
"github.com/libvirt/libvirt-go"
|
||||
libvirtxml "github.com/libvirt/libvirt-go-xml"
|
||||
errwrap "github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterResource("virt", func() Res { return &VirtRes{} })
|
||||
engine.RegisterResource("virt", func() engine.Res { return &VirtRes{} })
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -65,17 +62,15 @@ const (
|
||||
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
|
||||
// set to `shutoff` is one which does not exist. The parallel equivalent is a
|
||||
// file resource which removes a particular path.
|
||||
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
|
||||
State string `yaml:"state"` // running, paused, shutoff
|
||||
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
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *VirtRes) Default() Res {
|
||||
return &VirtRes{
|
||||
BaseRes: BaseRes{
|
||||
MetaParams: DefaultMetaParams, // force a default
|
||||
},
|
||||
// VirtAuth is used to pass credentials to libvirt.
|
||||
type VirtAuth struct {
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *VirtRes) Default() engine.Res {
|
||||
return &VirtRes{
|
||||
MaxCPUs: DefaultMaxCPUs,
|
||||
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 {
|
||||
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.
|
||||
func (obj *VirtRes) Init() error {
|
||||
func (obj *VirtRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
if !libvirtInitialized {
|
||||
if err := libvirt.EventRegisterDefaultImpl(); err != nil {
|
||||
return errwrap.Wrapf(err, "method EventRegisterDefaultImpl failed")
|
||||
@@ -154,7 +153,7 @@ func (obj *VirtRes) Init() error {
|
||||
}
|
||||
|
||||
// check for hard to change properties
|
||||
dom, err := obj.conn.LookupDomainByName(obj.GetName())
|
||||
dom, err := obj.conn.LookupDomainByName(obj.Name())
|
||||
if err == nil {
|
||||
defer dom.Free()
|
||||
} else if !isNotFound(err) {
|
||||
@@ -194,7 +193,7 @@ func (obj *VirtRes) Init() error {
|
||||
}
|
||||
}
|
||||
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.
|
||||
@@ -209,12 +208,6 @@ func (obj *VirtRes) Close() error {
|
||||
_, 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!
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -279,7 +272,7 @@ func (obj *VirtRes) Watch() error {
|
||||
go func() {
|
||||
defer obj.wg.Done()
|
||||
defer wg.Done()
|
||||
defer log.Printf("EventRunDefaultImpl exited!")
|
||||
defer obj.init.Logf("EventRunDefaultImpl exited!")
|
||||
for {
|
||||
// TODO: can we merge this into our main for loop below?
|
||||
select {
|
||||
@@ -287,7 +280,7 @@ func (obj *VirtRes) Watch() error {
|
||||
return
|
||||
default:
|
||||
}
|
||||
//log.Printf("EventRunDefaultImpl started!")
|
||||
//obj.init.Logf("EventRunDefaultImpl started!")
|
||||
if err := libvirt.EventRunDefaultImpl(); err != nil {
|
||||
select {
|
||||
case errorChan <- errwrap.Wrapf(err, "EventRunDefaultImpl failed"):
|
||||
@@ -296,14 +289,14 @@ func (obj *VirtRes) Watch() error {
|
||||
}
|
||||
return
|
||||
}
|
||||
//log.Printf("EventRunDefaultImpl looped!")
|
||||
//obj.init.Logf("EventRunDefaultImpl looped!")
|
||||
}
|
||||
}()
|
||||
|
||||
// domain events callback
|
||||
domCallback := func(c *libvirt.Connect, d *libvirt.Domain, ev *libvirt.DomainEventLifecycle) {
|
||||
domName, _ := d.GetName()
|
||||
if domName == obj.GetName() {
|
||||
if domName == obj.Name() {
|
||||
select {
|
||||
case domChan <- ev.Event: // send
|
||||
case <-exitChan:
|
||||
@@ -320,7 +313,7 @@ func (obj *VirtRes) Watch() error {
|
||||
// guest agent events callback
|
||||
gaCallback := func(c *libvirt.Connect, d *libvirt.Domain, eva *libvirt.DomainEventAgentLifecycle) {
|
||||
domName, _ := d.GetName()
|
||||
if domName == obj.GetName() {
|
||||
if domName == obj.Name() {
|
||||
select {
|
||||
case gaChan <- eva: // send
|
||||
case <-exitChan:
|
||||
@@ -334,13 +327,11 @@ func (obj *VirtRes) Watch() error {
|
||||
defer obj.conn.DomainEventDeregister(gaCallbackID)
|
||||
|
||||
// notify engine that we're running
|
||||
if err := obj.Running(); err != nil {
|
||||
return err // bubble up a NACK...
|
||||
if err := obj.init.Running(); err != nil {
|
||||
return err // exit if requested
|
||||
}
|
||||
|
||||
var send = false
|
||||
var exit *error // if ptr exists, that is the exit error to return
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
processExited := false // did the process exit fully (shutdown)?
|
||||
select {
|
||||
@@ -349,31 +340,31 @@ func (obj *VirtRes) Watch() error {
|
||||
switch event {
|
||||
case libvirt.DOMAIN_EVENT_DEFINED:
|
||||
if obj.Transient {
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
case libvirt.DOMAIN_EVENT_UNDEFINED:
|
||||
if !obj.Transient {
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
case libvirt.DOMAIN_EVENT_STARTED:
|
||||
fallthrough
|
||||
case libvirt.DOMAIN_EVENT_RESUMED:
|
||||
if obj.State != "running" {
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
case libvirt.DOMAIN_EVENT_SUSPENDED:
|
||||
if obj.State != "paused" {
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
case libvirt.DOMAIN_EVENT_STOPPED:
|
||||
fallthrough
|
||||
case libvirt.DOMAIN_EVENT_SHUTDOWN:
|
||||
if obj.State != "shutoff" {
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
}
|
||||
processExited = true
|
||||
@@ -384,7 +375,7 @@ func (obj *VirtRes) Watch() error {
|
||||
// verify, detect and patch appropriately!
|
||||
fallthrough
|
||||
case libvirt.DOMAIN_EVENT_CRASHED:
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
send = true
|
||||
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 {
|
||||
obj.guestAgentConnected = true
|
||||
obj.StateOK(false) // dirty
|
||||
obj.init.Dirty() // dirty
|
||||
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 {
|
||||
obj.guestAgentConnected = false
|
||||
// ignore CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_DOMAIN_STARTED
|
||||
// events because they just tell you that guest agent channel was added
|
||||
if reason == libvirt.CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_CHANNEL {
|
||||
log.Printf("%s: Guest agent disconnected", obj)
|
||||
obj.init.Logf("Guest agent disconnected")
|
||||
}
|
||||
|
||||
} else {
|
||||
@@ -418,15 +409,21 @@ func (obj *VirtRes) Watch() error {
|
||||
case err := <-errorChan:
|
||||
return fmt.Errorf("unknown %s libvirt error: %s", obj, err)
|
||||
|
||||
case event := <-obj.Events():
|
||||
if exit, send = obj.ReadEvent(event); exit != nil {
|
||||
return *exit // exit
|
||||
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
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -462,20 +459,20 @@ func (obj *VirtRes) domainCreate() (*libvirt.Domain, bool, error) {
|
||||
if err != nil {
|
||||
return dom, false, err // returned dom is invalid
|
||||
}
|
||||
log.Printf("%s: Domain defined", obj)
|
||||
obj.init.Logf("Domain defined")
|
||||
|
||||
if obj.State == "running" {
|
||||
if err := dom.Create(); err != nil {
|
||||
return dom, false, err
|
||||
}
|
||||
log.Printf("%s: Domain started", obj)
|
||||
obj.init.Logf("Domain started")
|
||||
}
|
||||
|
||||
if obj.State == "paused" {
|
||||
if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil {
|
||||
return dom, false, err
|
||||
}
|
||||
log.Printf("%s: Domain created paused", obj)
|
||||
obj.init.Logf("Domain created paused")
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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 {
|
||||
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")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: Domain resumed", obj)
|
||||
obj.init.Logf("Domain resumed")
|
||||
break
|
||||
}
|
||||
if err := dom.Create(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "domain.Create failed")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: Domain created", obj)
|
||||
obj.init.Logf("Domain created")
|
||||
|
||||
case "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")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: Domain paused", obj)
|
||||
obj.init.Logf("Domain paused")
|
||||
break
|
||||
}
|
||||
if err := dom.CreateWithFlags(libvirt.DOMAIN_START_PAUSED); err != nil {
|
||||
return false, errwrap.Wrapf(err, "domain.CreateWithFlags failed")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: Domain created paused", obj)
|
||||
obj.init.Logf("Domain created paused")
|
||||
|
||||
case "shutoff":
|
||||
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")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: Domain destroyed", obj)
|
||||
obj.init.Logf("Domain destroyed")
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
@@ -620,7 +617,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
|
||||
return false, errwrap.Wrapf(err, "domain.SetVcpus failed")
|
||||
}
|
||||
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:
|
||||
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")
|
||||
}
|
||||
checkOK = false
|
||||
log.Printf("%s: CPUs (cold) changed to %d", obj, obj.CPUs)
|
||||
obj.init.Logf("CPUs (cold) changed to %d", obj.CPUs)
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -663,7 +660,7 @@ func (obj *VirtRes) attrCheckApply(apply bool, dom *libvirt.Domain) (bool, error
|
||||
return false, errwrap.Wrapf(err, "domain.SetVcpus failed")
|
||||
}
|
||||
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")
|
||||
}
|
||||
if domInfo.State == libvirt.DOMAIN_SHUTOFF || domInfo.State == libvirt.DOMAIN_SHUTDOWN {
|
||||
log.Printf("%s: Shutdown", obj)
|
||||
obj.init.Logf("Shutdown")
|
||||
break
|
||||
}
|
||||
|
||||
@@ -699,7 +696,7 @@ func (obj *VirtRes) domainShutdownSync(apply bool, dom *libvirt.Domain) (bool, e
|
||||
obj.processExitChan = make(chan struct{})
|
||||
// if machine shuts down before we call this, we 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 {
|
||||
// FIXME: if machine is already shutdown completely, return early
|
||||
return false, errwrap.Wrapf(err, "domain.Shutdown failed")
|
||||
@@ -734,8 +731,8 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
|
||||
panic("virt: CheckApply is being called with nil connection")
|
||||
}
|
||||
// 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?
|
||||
if obj.RestartOnRefresh && obj.Refresh() { // a refresh is a restart ask
|
||||
var restart bool // do we need to do a restart?
|
||||
if obj.RestartOnRefresh && obj.init.Refresh() { // a refresh is a restart ask
|
||||
restart = true
|
||||
}
|
||||
|
||||
@@ -750,7 +747,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
|
||||
|
||||
var checkOK = true
|
||||
|
||||
dom, err := obj.conn.LookupDomainByName(obj.GetName())
|
||||
dom, err := obj.conn.LookupDomainByName(obj.Name())
|
||||
if err == nil {
|
||||
// pass
|
||||
} else if isNotFound(err) {
|
||||
@@ -792,7 +789,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
|
||||
if err := dom.Undefine(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "domain.Undefine failed")
|
||||
}
|
||||
log.Printf("%s: Domain undefined", obj)
|
||||
obj.init.Logf("Domain undefined")
|
||||
} else {
|
||||
domXML, err := dom.GetXMLDesc(libvirt.DOMAIN_XML_INACTIVE)
|
||||
if err != nil {
|
||||
@@ -801,7 +798,7 @@ func (obj *VirtRes) CheckApply(apply bool) (bool, error) {
|
||||
if _, err = obj.conn.DomainDefineXML(domXML); err != nil {
|
||||
return false, errwrap.Wrapf(err, "conn.DomainDefineXML failed")
|
||||
}
|
||||
log.Printf("%s: Domain defined", obj)
|
||||
obj.init.Logf("Domain defined")
|
||||
}
|
||||
checkOK = false
|
||||
}
|
||||
@@ -890,7 +887,7 @@ func (obj *VirtRes) getDomainXML() string {
|
||||
var b string
|
||||
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)
|
||||
|
||||
if obj.HotCPUs {
|
||||
@@ -996,7 +993,7 @@ type filesystemDevice struct {
|
||||
}
|
||||
|
||||
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
|
||||
b += "<disk type='file' device='disk'>"
|
||||
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 {
|
||||
source, _ := expandHome(d.Source) // TODO: should we handle errors?
|
||||
source, _ := util.ExpandHome(d.Source) // TODO: should we handle errors?
|
||||
var b string
|
||||
b += "<disk type='file' device='cdrom'>"
|
||||
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 {
|
||||
source, _ := expandHome(d.Source) // TODO: should we handle errors?
|
||||
source, _ := util.ExpandHome(d.Source) // TODO: should we handle errors?
|
||||
var b string
|
||||
b += "<filesystem" // open
|
||||
if d.Access != "" {
|
||||
@@ -1047,43 +1044,21 @@ func (d *filesystemDevice) GetXML(idx int) string {
|
||||
return b
|
||||
}
|
||||
|
||||
// VirtUID is the UID struct for FileRes.
|
||||
type VirtUID struct {
|
||||
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() []ResUID {
|
||||
x := &VirtUID{
|
||||
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
|
||||
// TODO: add more properties here so we can link to vm dependencies
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *VirtRes) Cmp(r engine.Res) error {
|
||||
if !obj.Compare(r) {
|
||||
return fmt.Errorf("did not compare")
|
||||
}
|
||||
return []ResUID{x}
|
||||
}
|
||||
|
||||
// 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
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
res, ok := r.(*VirtRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
|
||||
if obj.URI != res.URI {
|
||||
return false
|
||||
@@ -1131,6 +1106,21 @@ func (obj *VirtRes) Compare(r Res) bool {
|
||||
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.
|
||||
// It is primarily useful for setting the defaults.
|
||||
func (obj *VirtRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
@@ -1171,30 +1161,3 @@ func isNotFound(err error) bool {
|
||||
}
|
||||
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
67
engine/sendrecv.go
Normal 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
42
engine/traits/autoedge.go
Normal 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
|
||||
}
|
||||
89
engine/traits/autogroup.go
Normal file
89
engine/traits/autogroup.go
Normal 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
36
engine/traits/base.go
Normal 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
39
engine/traits/kind.go
Normal 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
40
engine/traits/meta.go
Normal 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
40
engine/traits/named.go
Normal 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
40
engine/traits/refresh.go
Normal 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
75
engine/traits/sendrecv.go
Normal 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
41
engine/util.go
Normal 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
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
// 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
|
||||
package util
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -24,10 +24,10 @@ import (
|
||||
"fmt"
|
||||
"os/user"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/lang/types"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
@@ -36,29 +36,18 @@ import (
|
||||
const (
|
||||
// StructTag is the key we use in struct field names for key mapping.
|
||||
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).
|
||||
func ResToB64(res Res) (string, error) {
|
||||
func ResToB64(res engine.Res) (string, error) {
|
||||
b := bytes.Buffer{}
|
||||
e := gob.NewEncoder(&b)
|
||||
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).
|
||||
func B64ToRes(str string) (Res, error) {
|
||||
func B64ToRes(str string) (engine.Res, error) {
|
||||
var output interface{}
|
||||
bb, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
@@ -80,7 +69,7 @@ func B64ToRes(str string) (Res, error) {
|
||||
if err := d.Decode(&output); err != nil { // pass with &
|
||||
return nil, errwrap.Wrapf(err, "gob failed to decode")
|
||||
}
|
||||
res, ok := output.(Res)
|
||||
res, ok := output.(engine.Res)
|
||||
if !ok {
|
||||
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
|
||||
// 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
|
||||
result := make(map[string]string) // `lang` field tag -> field name
|
||||
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
|
||||
// of each field name to the actual field name. It only returns public fields.
|
||||
// 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
|
||||
st := reflect.TypeOf(res).Elem() // elem for ptr to res
|
||||
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.
|
||||
// TODO: should this behaviour be changed?
|
||||
func LangFieldNameToStructFieldName(kind string) (map[string]string, error) {
|
||||
res, err := NewResource(kind)
|
||||
res, err := engine.NewResource(kind)
|
||||
if err != nil {
|
||||
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
|
||||
// in the lang type system.
|
||||
func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) {
|
||||
res, err := NewResource(kind)
|
||||
res, err := engine.NewResource(kind)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -180,7 +169,7 @@ func StructKindToFieldNameTypeMap(kind string) (map[string]*types.Type, error) {
|
||||
field := st.Field(i)
|
||||
name := field.Name
|
||||
// TODO: in future, skip over fields that don't have a `lang` tag
|
||||
//if name == "BaseRes" { // TODO: hack!!!
|
||||
//if name == "Base" { // TODO: hack!!!
|
||||
// continue
|
||||
//}
|
||||
|
||||
107
engine/util/util_test.go
Normal file
107
engine/util/util_test.go
Normal 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
48
engine/world.go
Normal 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)
|
||||
}
|
||||
31
etcd/etcd.go
31
etcd/etcd.go
@@ -32,7 +32,7 @@
|
||||
// * 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.
|
||||
// * 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.
|
||||
//
|
||||
// Smoke testing:
|
||||
@@ -64,7 +64,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/converger"
|
||||
"github.com/purpleidea/mgmt/event"
|
||||
"github.com/purpleidea/mgmt/etcd/event"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
|
||||
@@ -87,8 +87,10 @@ const (
|
||||
selfRemoveTimeout = 3 // give unnominated members a chance to self exit
|
||||
exitDelay = 3 // number of sec of inactivity after exit to clean up
|
||||
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
|
||||
// single etcd transaction. If you exceed this limit, it is possible
|
||||
// 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
|
||||
// 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
|
||||
u := url.URL{Scheme: "http", Host: DefaultClientURL} // default
|
||||
obj.clientURLs = append([]url.URL{u}, obj.clientURLs...) // prepend
|
||||
u, err := url.Parse(DefaultClientURL)
|
||||
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!
|
||||
@@ -271,8 +276,18 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient
|
||||
if len(obj.endpoints) > 0 {
|
||||
obj.noServer = true // we didn't have enough to be a server
|
||||
}
|
||||
u := url.URL{Scheme: "http", Host: DefaultServerURL} // default
|
||||
obj.serverURLs = []url.URL{u}
|
||||
u, err := url.Parse(DefaultServerURL) // default
|
||||
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
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
// 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 etcd
|
||||
|
||||
import (
|
||||
|
||||
@@ -22,54 +22,10 @@ import (
|
||||
"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,
|
||||
// and a non-nil represents a NACK (false). This also lets us use custom errors.
|
||||
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.
|
||||
func NewResp() Resp {
|
||||
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
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
// 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 fs_test // named this way to make it easier for examples
|
||||
|
||||
import (
|
||||
|
||||
@@ -22,7 +22,8 @@ import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
engineUtil "github.com/purpleidea/mgmt/engine/util"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
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.
|
||||
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
|
||||
|
||||
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
|
||||
ops := []etcd.Op{} // list of ops in this transaction
|
||||
for _, res := range resourceList {
|
||||
if res.GetKind() == "" {
|
||||
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
|
||||
if res.Kind() == "" {
|
||||
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)
|
||||
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
|
||||
ops = append(ops, etcd.OpPut(path, data))
|
||||
} 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 {
|
||||
if res.GetKind() == x.GetKind() && res.GetName() == x.GetName() {
|
||||
if res.Kind() == x.Kind() && res.Name() == x.Name() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -104,10 +105,10 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
|
||||
hasDeletes := false
|
||||
// delete old, now unused resources here...
|
||||
for _, res := range originals {
|
||||
if res.GetKind() == "" {
|
||||
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
|
||||
if res.Kind() == "" {
|
||||
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)
|
||||
|
||||
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
|
||||
// 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.
|
||||
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
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get resources: %v", err)
|
||||
@@ -170,7 +171,7 @@ func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resourc
|
||||
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)
|
||||
resourceList = append(resourceList, obj)
|
||||
} else {
|
||||
|
||||
@@ -22,18 +22,18 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
etcdfs "github.com/purpleidea/mgmt/etcd/fs"
|
||||
"github.com/purpleidea/mgmt/etcd/scheduler"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
)
|
||||
|
||||
// World is an etcd backed implementation of the World interface.
|
||||
type World struct {
|
||||
Hostname string // uuid for the consumer of these
|
||||
EmbdEtcd *EmbdEtcd
|
||||
MetadataPrefix string // expected metadata prefix
|
||||
StoragePrefix string // storage prefix for etcdfs storage
|
||||
StandaloneFs resources.Fs // store an fs here for local usage
|
||||
MetadataPrefix string // expected metadata prefix
|
||||
StoragePrefix string // storage prefix for etcdfs storage
|
||||
StandaloneFs engine.Fs // store an fs here for local usage
|
||||
Debug bool
|
||||
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.
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ResCollect gets the collection of exported resources which match the filter.
|
||||
// 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
|
||||
// exported with a tag that allows or restricts our hostname? We could
|
||||
// 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
|
||||
// actually be a local or memory backed file system, so actually only
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
6
examples/lang/docker_container0.mcl
Normal file
6
examples/lang/docker_container0.mcl
Normal 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
5
examples/lang/mount0.mcl
Normal file
@@ -0,0 +1,5 @@
|
||||
mount "/mnt/foo" {
|
||||
state => "exists",
|
||||
device => "/dev/sdb1",
|
||||
type => "ext4",
|
||||
}
|
||||
5
examples/lang/mount1.mcl
Normal file
5
examples/lang/mount1.mcl
Normal 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
13
examples/lang/mount2.mcl
Normal 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
5
examples/lang/net1.mcl
Normal 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
3
examples/lang/net2.mcl
Normal file
@@ -0,0 +1,3 @@
|
||||
net "eth0" {
|
||||
state => "up",
|
||||
}
|
||||
3
examples/lang/net3.mcl
Normal file
3
examples/lang/net3.mcl
Normal file
@@ -0,0 +1,3 @@
|
||||
net "eth0" {
|
||||
state => "down",
|
||||
}
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/resources"
|
||||
"github.com/purpleidea/mgmt/gapi"
|
||||
mgmt "github.com/purpleidea/mgmt/lib"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
|
||||
"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
|
||||
// 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) {
|
||||
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 != "" {
|
||||
return nil, fmt.Errorf("input is not empty")
|
||||
@@ -103,66 +104,46 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaparams := resources.DefaultMetaParams
|
||||
|
||||
exec1 := &resources.ExecRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "exec1",
|
||||
Kind: "exec",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
|
||||
Shell: "/bin/bash",
|
||||
}
|
||||
g.AddVertex(exec1)
|
||||
|
||||
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",
|
||||
State: "present",
|
||||
}
|
||||
// XXX: add send->recv!
|
||||
//Recv: map[string]*engine.Send{
|
||||
// "Content": {Res: exec1, Key: "Output"},
|
||||
//},
|
||||
|
||||
g.AddVertex(output)
|
||||
g.AddEdge(exec1, output, &resources.Edge{Name: "e0"})
|
||||
g.AddEdge(exec1, output, &engine.Edge{Name: "e0"})
|
||||
|
||||
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",
|
||||
State: "present",
|
||||
}
|
||||
// XXX: add send->recv!
|
||||
//Recv: map[string]*engine.Send{
|
||||
// "Content": {Res: exec1, Key: "Stdout"},
|
||||
//},
|
||||
g.AddVertex(stdout)
|
||||
g.AddEdge(exec1, stdout, &resources.Edge{Name: "e1"})
|
||||
g.AddEdge(exec1, stdout, &engine.Edge{Name: "e1"})
|
||||
|
||||
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",
|
||||
State: "present",
|
||||
}
|
||||
// XXX: add send->recv!
|
||||
//Recv: map[string]*engine.Send{
|
||||
// "Content": {Res: exec1, Key: "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)
|
||||
return g, nil
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/resources"
|
||||
"github.com/purpleidea/mgmt/gapi"
|
||||
mgmt "github.com/purpleidea/mgmt/lib"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
|
||||
errwrap "github.com/pkg/errors"
|
||||
"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
|
||||
// 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) {
|
||||
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 != "" {
|
||||
return nil, fmt.Errorf("input is not empty")
|
||||
@@ -103,27 +104,13 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaparams := resources.DefaultMetaParams
|
||||
|
||||
f1 := &resources.FileRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "file1",
|
||||
Kind: "file",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Path: "/tmp/mgmt/sub1",
|
||||
|
||||
Path: "/tmp/mgmt/sub1",
|
||||
State: "present",
|
||||
}
|
||||
g.AddVertex(f1)
|
||||
|
||||
n1 := &resources.NoopRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "noop1",
|
||||
Kind: "noop",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
}
|
||||
n1 := &resources.NoopRes{}
|
||||
g.AddVertex(n1)
|
||||
|
||||
return g, nil
|
||||
@@ -140,14 +127,8 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaparams := resources.DefaultMetaParams
|
||||
|
||||
content := "I created a subgraph!\n"
|
||||
f0 := &resources.FileRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "README",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Path: "/tmp/mgmt/README",
|
||||
Content: &content,
|
||||
State: "present",
|
||||
@@ -160,7 +141,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||
}
|
||||
|
||||
edgeGenFn := func(v1, v2 pgraph.Vertex) pgraph.Edge {
|
||||
edge := &resources.Edge{
|
||||
edge := &engine.Edge{
|
||||
Name: fmt.Sprintf("edge: %s->%s", v1, v2),
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/resources"
|
||||
"github.com/purpleidea/mgmt/gapi"
|
||||
mgmt "github.com/purpleidea/mgmt/lib"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
|
||||
"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
|
||||
// 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) {
|
||||
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 != "" {
|
||||
return nil, fmt.Errorf("input is not empty")
|
||||
@@ -103,15 +104,8 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
metaparams := resources.DefaultMetaParams
|
||||
|
||||
content := "I created a subgraph!\n"
|
||||
f0 := &resources.FileRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "README",
|
||||
Kind: "file",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Path: "/tmp/mgmt/README",
|
||||
Content: &content,
|
||||
State: "present",
|
||||
@@ -126,40 +120,24 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
|
||||
|
||||
// add elements into the sub graph
|
||||
f1 := &resources.FileRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "file1",
|
||||
Kind: "file",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Path: "/tmp/mgmt/sub1",
|
||||
|
||||
State: "present",
|
||||
}
|
||||
subGraph.AddVertex(f1)
|
||||
|
||||
n1 := &resources.NoopRes{
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "noop1",
|
||||
Kind: "noop",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
}
|
||||
n1 := &resources.NoopRes{}
|
||||
subGraph.AddVertex(n1)
|
||||
|
||||
e0 := &resources.Edge{Name: "e0"}
|
||||
e0 := &engine.Edge{Name: "e0"}
|
||||
e0.Notify = true // send a notification from v0 to v1
|
||||
subGraph.AddEdge(f1, n1, e0)
|
||||
|
||||
// create the actual resource to hold the sub graph
|
||||
subGraphRes0 := &resources.GraphRes{ // TODO: should we name this SubGraphRes ?
|
||||
BaseRes: resources.BaseRes{
|
||||
Name: "subgraph1",
|
||||
Kind: "graph",
|
||||
MetaParams: metaparams,
|
||||
},
|
||||
Graph: subGraph,
|
||||
}
|
||||
g.AddVertex(subGraphRes0) // add it to the main graph
|
||||
//subGraphRes0 := &resources.GraphRes{ // TODO: should we name this SubGraphRes ?
|
||||
// Graph: subGraph,
|
||||
//}
|
||||
//g.AddVertex(subGraphRes0) // add it to the main graph
|
||||
|
||||
//g, err := config.NewGraphFromConfig(obj.data.Hostname, obj.data.World, obj.data.Noop)
|
||||
return g, nil
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user