140 Commits

Author SHA1 Message Date
James Shubin
3ad7097c8a lang: Add internal, resource specific edges
This adds the ability to specify internal, resource specific edges, with
and without notifications. We use the special words: "Notify", "Before",
"Listen", and "Depend". They must have the first character capitalized.
They also support the "elvis" operator.
2018-02-27 23:26:25 -05:00
James Shubin
8e01b6db48 lang: Add a resource-specific elvis operator
This allows you to omit a resource parameter programmatically, and
avoids the need of an `undef` or `nil` in our language, which would
contribute to programming errors, crashes, and overall reduced safety.
2018-02-27 17:29:49 -05:00
James Shubin
67607eba8b travis: Fix the OSX builds
I don't use OSX, but here's a bit of sympathy for the poor travis OSX
builder that can't understand apt ;)
2018-02-27 17:28:09 -05:00
James Shubin
6e7a71d01a travis: Attempt to cut down on flaky failures
Travis has been spuriously failing a LOT. Hopefully this reduces some of
those failures.
2018-02-27 17:17:29 -05:00
James Shubin
ff69a82b57 lang: unification: Fix panic in struct/func cmp of partials
This was discovered by user aequitas. I modified his patch slightly, and
added some comments and a test.
2018-02-27 16:39:10 -05:00
James Shubin
df1e50e599 lang: funcs: Add math pow function and a few examples
Just a few small things I think should be committed.
2018-02-25 19:48:25 -05:00
James Shubin
6370f0cb95 lang: Add edges to lexer and parser
This adds some initial syntax for external edges to the language.

There are still improvements which are necessary for send/recv.
2018-02-25 19:29:27 -05:00
James Shubin
80784bb8f1 lang: types, funcs: Add simple polymorphic function API
This adds a simple API for adding static, polymorphic, pure functions.
This lets you define a list of type signatures and the associated
implementations to overload a particular function name. The internals of
this API then do all of the hard work of matching the available
signatures to what statically type checks, and then calling the
appropriate implementation.

While this seems as if this would only work for function polymorphism
with a finite number of possible types, while this is mostly true, it
also allows you to add the `variant` "wildcard" type into your
signatures which will allow you to match a wider set of signatures.

A canonical use case for this is the len function which can determine
the length of both lists and maps with any contained type. (Either the
type of the list elements, or the types of the map keys and values.)

When using this functionality, you must be careful to ensure that there
is only a single mapping from possible type to signature so that the
"dynamic dispatch" of the function is unique.

It is worth noting that this API won't cover functions which support an
arbitrary number of input arguments. The well-known case of this,
printf, is implemented with the more general function API which is more
complicated.

This patch also adds some necessary library improvements for comparing
types to partial types, and to types containing variants.

Lastly, this fixes a bug in the `NewType` parser which parsed certain
complex function types wrong.
2018-02-25 02:17:13 -05:00
James Shubin
40dcd6ec99 all: Misc fixes and test fixes 2018-02-25 02:13:51 -05:00
karimb
06f2d65500 docs: Add docs for docker usage 2018-02-24 12:04:23 +01:00
Johan Bloemberg
98d3c299ff project: Add me 2018-02-23 19:59:55 -05:00
James Shubin
46da3a34a0 docs: Add two new faq entries 2018-02-23 19:44:54 -05:00
Johan Bloemberg
f33f84d2f2 lang: Add getenv function
$x = getenv("NAME")
    $y = defaultenv("NOTEXIST", "defaultvalue")
    $z = hasenv("NAME")
    $a = env()
    $b = maplookup($a, "NAME", "defaultvalue")
2018-02-23 20:02:13 +01:00
James Shubin
a785a43ef3 travis: Attempt to workaround the constant travis failures
I'm beginning to think we need a more reliable CI...
2018-02-22 20:26:52 -05:00
James Shubin
b0911c6d70 lang: funcs: simple: Don't block on simple, pure, static functions
I forgot to handle the special case of a function using this API that
received no inputs. It was waiting for the first input to come in, and
as a result was never producing any output.

Remember that functions like this should *almost* be thought of as
constants of the system. You would expect their output to never change
during the lifetime of a particular program invocation.
2018-02-22 19:26:18 -05:00
James Shubin
81a0e9e8c7 build: Relocate time command to the front for readability
This makes the output more readable in my terminal.
2018-02-22 17:49:33 -05:00
Johan Bloemberg
06d33a45f5 docs, misc: Add tool references, .editorconfig for mcl 2018-02-22 17:23:11 -05:00
Jonathan Gold
cfb8deac56 project: Add Jonathan Gold to AUTHORS 2018-02-22 17:19:58 -05:00
Johan Bloemberg
9544ab2e02 recwatch: Fix watching newly created files on macOS
Fixes: https://github.com/purpleidea/mgmt/issues/33
2018-02-22 16:52:26 -05:00
Oliver Frommel
318fe4a5dc misc: Small fixes for makedeps script
- install Go distribution package only if no go binary found
2018-02-22 16:50:13 -05:00
James Shubin
5597183391 docs: Add two faq entries about the type system 2018-02-22 16:45:54 -05:00
James Shubin
05c60d9a59 test, docs: Restrict long lines in markdown linter
It's getting out of hand...
2018-02-22 16:19:23 -05:00
Peter Oliver
f01eea33e9 emacs: Bundle an Emacs major mode, mgmtconfig-mode
This provides syntax highlighting, commenting, and rudimentary indentation of the mgmt language.
2018-02-22 15:57:05 -05:00
James Shubin
9992c367bf misc: Update golint to new location
Somehow this got changed...
2018-02-22 02:01:14 -05:00
James Shubin
d275a23a81 misc: Add dependency on time package
Some environments apparently don't have this installed. We have it in
certain places where we like to time things.
2018-02-21 22:52:41 -05:00
James Shubin
14ddd7c196 golint: Fix ineffassign mistakes 2018-02-21 22:52:41 -05:00
James Shubin
0815b20b76 lang: funcs: Fix up some old comments
Woops, bad copy-paste issues.
2018-02-21 22:52:41 -05:00
James Shubin
cffdb06181 test, docs: Add a linter for testing markdown, and fix up our docs
While writing docs, I couldn't remember what the correct style was
supposed to be, and I remember someone complaining about this
previously, so I decided to add a linter! I excluded a bunch of annoying
style rules, but if we find more we can add those to the list too.

Hopefully this gives us a more consistent feel throughout.
2018-02-21 22:52:41 -05:00
James Shubin
837388ae4e lang: types, funcs: Add simple function API
This patch adds a simple function API for writing simple, pure
functions. This should reduce the amount of boilerplate required for
most functions, and make growing a stdlib significantly easier. If you
need to build more complex, event-generating functions, or statically
polymorphic functions, then you'll still need to use the normal API for
now.

This also makes all of these pure functions available automatically
within templates. It might make sense to group these functions into
packages to make their logical organization easier, but this is a good
enough start for now.

Lastly, this added some missing pieces to our types library. You can now
use `ValueOf` to convert from a `reflect.Value` to the corresponding
`Value` in our type system, if an equivalent exists.

Unfortunately, we're severely lacking in tests for these new types
library additions, but look forward to growing some in the future!
2018-02-21 21:32:31 -05:00
Johan Bloemberg
cbd2bdd4c5 travis: Retry flaky apt update at build start 2018-02-20 21:41:08 +01:00
Johan Bloemberg
f34ca3a5ca travis: Improve travis speed by only building 1 go version for osx 2018-02-20 21:41:03 +01:00
James Shubin
4898297cce travis: Avoid notification noise from forks
Encrypt name of IRC channel to workaround forks spamming us with their
testing messages.

Docs: https://docs.travis-ci.com/user/environment-variables/#Defining-encrypted-variables-in-.travis.yml
2018-02-20 14:12:09 -05:00
Johan Bloemberg
ffcc2aa2af lib: Provide detailed feedback about invalid URLs 2018-02-20 10:29:19 -05:00
Johan Bloemberg
158fb8d31c etcd: Warn about invalid configuration, clarify --no-server 2018-02-20 10:29:19 -05:00
Johan Bloemberg
07714c67cb cli: Log errors return by Run functions
Turns

```
$ ./mgmt run
00:44:15 hello.go:46: This is: mgmt, version: 0.0.14-30-ge3a2648
00:44:15 hello.go:47: Main: Start: 1518738255855525279
$
```

Into

```
$ ./mgmt run
01:07:02 hello.go:46: This is: mgmt, version: 0.0.14-30-ge3a2648-dirty
01:07:02 hello.go:47: Main: Start: 1518739622517652739
01:07:02 cli.go:167: Main: Error: can't create prefix: mkdir /var/lib/mgmt/: permission denied
$
```
2018-02-20 10:29:19 -05:00
James Shubin
f12e502c61 lang: funcs: Rename things for consistency
Also fix a few copy-pasta issues in the documentation.
2018-02-18 19:47:14 -05:00
Toshaan Bharvani
2fdf8d5dc3 lang: Interface sorting order
as golang does not loop over the same map/list always the same
we use a helper list to sort it

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>
2018-02-18 18:32:15 -05:00
James Shubin
28ec7a1e54 etcd: scheduler: Remove etcd 3.2 specific hacks
Now that we're using etcd 3.3, we can simplify our code now that our
patches are in a release.
2018-02-18 18:28:45 -05:00
James Shubin
24cb2e6450 etcd: Increase the default max txn op count
The default of 128 is fairly low for large code bases. Please let us
know if you hit the new limit of 512.
2018-02-18 18:28:00 -05:00
James Shubin
915b022901 test: Show test output as it happens 2018-02-18 17:31:45 -05:00
James Shubin
4a623c1891 docs: Add an entry to the faq about converged timeouts 2018-02-18 16:28:50 -05:00
Johan Bloemberg
7508161c39 test: Exclude generated files from golint 2018-02-18 15:07:54 -05:00
Johan Bloemberg
d33861ccb4 test: Fix augeas test for macOS, improve test debuggability
- resolve a discrepancy in augeas behaviour on macOS
- on macOS `sed` requires an argument for `-i`.
- made the test fail as early as it can
- provide information about why the test is failing
2018-02-18 14:36:59 -05:00
Johan Bloemberg
572b2575c5 test: Export the mgmt command to be used during test 2018-02-18 14:19:05 -05:00
James Shubin
d99190b166 travis: Add golang 1.10.x to builds 2018-02-18 13:58:11 -05:00
James Shubin
bc91b03276 docs: Add faq entry about production readiness 2018-02-18 13:34:21 -05:00
James Shubin
9ba893c06c etcd: Bump to etcd v3.3 and golang 1.9
This moves us to etcd v3.3 (a new major release) which has some useful
features but that requires version 1.9 of golang.
2018-02-15 18:47:55 -05:00
James Shubin
27e51f1bcb authors: Clarify wording in AUTHORS file 2018-02-15 18:45:32 -05:00
James Shubin
e3a26483e8 test: Improve gometalinter test so that it skips generated files
This should improve things significantly, and avoid the failures now
that we're testing after the files have already been built.
2018-02-15 16:26:06 -05:00
James Shubin
33a4fd6fbe build: Add -i flag to go build
It got accidentally dropped, but is crucial for happiness.
See: https://purpleidea.com/blog/2017/02/26/faster-golang-builds/
2018-02-15 12:24:56 -05:00
Johan Bloemberg
b34b359860 test: Streamline test suite a little
This change aims to streamline the integrationtest suite and reduce friction when running (parts of) test suites.

Changes:
- add `test-testname` to makefile to easily run one suite
- made skipping tests first class citizen in test.sh (all available testsuites and the reasons they are skipped are now better exposed and discovered)
- suppress some output of gotest unless there is an error
- no longer build binary for examples and gotest suites
- removed .SILENT from makefile as it being applied to only some targets makes it feel weird (I just learned about this option btw, feel free to comment on this change)
- move individual tests out of `test.sh` and into `test-misc.sh`
- introduced the concept of testsuites to `test.sh`
2018-02-15 17:21:49 +01:00
James Shubin
8b9491823d etcd: Fix golint issue in test
Found with new gometalinter version.
2018-02-14 18:53:10 -05:00
James Shubin
b8b6e5266f build: Improve speed of make
Generating a huge amount of unnecessary targets caused "noop" make runs
to take seven seconds on my machine. This limits the list of these
drastically and now "noop" make's are now < 1s on my machine.

Issue discussed in:
https://github.com/purpleidea/mgmt/issues/331
2018-02-14 18:42:12 -05:00
James Shubin
5f80c1ac2a resources: nspawn: Don't panic if one svc is nil
Not sure why one of them was nil, but this prevents the panic.
2018-02-14 16:44:21 -05:00
James Shubin
3af7e815d0 docs: Add newly recorded talks and blog posts about mgmt 2018-02-14 15:45:40 -05:00
James Shubin
714afe35a1 test: Fix broken gometalinter test
The test for gometalinter got silently broken in an earlier commit.
Look for the missing space that was added back in this commit to see
why! In any case, this now fixes some of the things that weren't
previously caught by this change.

If anyone knows how to run these sorts of tests properly so that entire
packages are tested and so that we can enable additional tests, please
let me know!

It's also unclear why goreportcard catches a few additional problems
which aren't found by running this ourselves.

See:
https://goreportcard.com/report/github.com/purpleidea/mgmt
for more information.
2018-02-14 14:34:36 -05:00
James Shubin
b0a8f585c3 readme: Fix broken link 2018-02-14 14:03:07 -05:00
James Shubin
1a2918082d docs: Add FAQ entry about vendoring dependencies 2018-02-14 14:01:10 -05:00
Johan Bloemberg
22e4dfa534 build: Unify build/crossbuild
Changes:

- allows explicit crossbuild targets (eg: `make mgmt-darwin-amd64`)
- adds darwin/amd64 to default crossbuild targets
- gitignore only build artifacts (eg: not all files starting with `mgmt-`)
- `build` and `crossbuild` target now utilize the same build function (`build` still generates only a `mgmt` binary for the current os/arch)
- test crossbuilding
- allow specifying custom GOOSARCHES envvar to override defaults
- crossbuild artifacts go into `build/` now
- add `build-debug` which includes symbol tables and debug info
- the build function now has `-s -w` linker arguments which discards some debug info afaict, to build a debug release use `make build-debug`

On my mac crossbuilding won't work unless I disable augeas and libvirt:

```
~/.g/s/g/p/mgmt (build|●1✚8…3) $ make build
Generating: bindata...
Generating: lang...
/Applications/Xcode.app/Contents/Developer/usr/bin/make --quiet -C lang
Building: mgmt, os/arch: darwin-amd64, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=darwin GOARCH=amd64 time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-darwin-amd64 ;
        7.14 real        10.36 user         1.73 sys
mv mgmt-darwin-amd64 mgmt
```

```
~/.g/s/g/p/mgmt (build|●1✚8…3) $ time env GOTAGS='noaugeas novirt' make crossbuild
Generating: bindata...
Generating: lang...
/Applications/Xcode.app/Contents/Developer/usr/bin/make --quiet -C lang
Building: mgmt, os/arch: linux-amd64, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=linux GOARCH=amd64 time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-linux-amd64 -tags 'noaugeas novirt';
       18.48 real        50.02 user         5.83 sys
Building: mgmt, os/arch: linux-ppc64, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=linux GOARCH=ppc64 time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-linux-ppc64 -tags 'noaugeas novirt';
       29.83 real        85.09 user        11.54 sys
Building: mgmt, os/arch: linux-ppc64le, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=linux GOARCH=ppc64le time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-linux-ppc64le -tags 'noaugeas novirt';
       29.74 real        85.84 user        11.76 sys
Building: mgmt, os/arch: linux-arm64, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=linux GOARCH=arm64 time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-linux-arm64 -tags 'noaugeas novirt';
       28.33 real        83.24 user        11.40 sys
Building: mgmt, os/arch: darwin-amd64, version: 0.0.14-12-g94c8bc1-dirty...
env GOOS=darwin GOARCH=amd64 time go build -ldflags "-X main.program=mgmt -X main.version=0.0.14-12-g94c8bc1-dirty -s -w" -o mgmt-darwin-amd64 -tags 'noaugeas novirt';
        7.16 real        10.15 user         1.74 sys
      114.71 real       315.26 user        42.44 sys
```
2018-02-14 12:37:49 -05:00
Johan Bloemberg
41eb850b3d debian: Add graphviz and packagekit runtime dependencies 2018-02-12 15:11:13 -05:00
Wim
3a50171d19 build: Add gcc,pkg-config deps 2018-02-12 15:10:19 -05:00
Wim
6c9e0ff974 docs: Add GOPATH/bin to PATH 2018-02-12 15:09:32 -05:00
James Shubin
644e5164b1 test: Increase timeouts for when travis is slow
Hopefully this cuts down on spurious failures.
2018-02-12 15:08:47 -05:00
James Shubin
4fefa9f2f0 travis: Disable fast finish for now
This causes a notification for each entry in the matrix which is now too
many emails. When travis adds and option to send just one notification,
but to still allow you to fast finish, then please lmk :)
2018-02-10 18:49:12 -05:00
Johan Bloemberg
4c793e0ee6 misc: Fix graphviz output for hostnames with dot in them 2018-02-11 00:02:52 +01:00
James Shubin
68a7de41ae etcd: Update broken link 2018-02-10 10:38:54 -05:00
dsx
94c8bc1de9 debian: Add packaging 2018-02-10 05:12:31 -05:00
Johan Bloemberg
8fb0373f82 resources: Do not return GID for UID lookup
On linux it is convention for users to have a group with the same GID as the users UID. On macOS this is not the case. This broke the test which lead to discovering this bug.
2018-02-10 05:01:12 -05:00
Johan Bloemberg
d567dc3769 lang: Use universal way to retrieve load
Sysinfo is not supported on macOS and results in a build error.
2018-02-10 05:01:12 -05:00
Johan Bloemberg
ba21554c5f build, docs: Improve macOS building
- New docker command for quickly running tasks in a Linux environment.
- Updated docs with macOS specific details.
- Fixed some test issues.
- Add (fallible) macOS test target for Travis.
2018-02-10 05:01:12 -05:00
James Shubin
e37bb3ac8a test: Add new test for language prefix 2018-02-07 21:52:06 -05:00
Carsten Thiel
79845f0dfd test: Refactor unification_test to subtests
Testsuite for unification now uses subtests feature.
2018-02-07 16:13:18 +01:00
Carsten Thiel
eb33a5a5df docs: Improve file resource documentation
Info on how to create a directory.
Explain more parameter options.
2018-02-07 14:19:14 +01:00
jonathangold
adbe9c7be1 misc: Replace missing go-bindata dependency 2018-02-07 06:15:03 -05:00
James Shubin
b19583e7d3 lang: Initial implementation of the mgmt language
This is an initial implementation of the mgmt language. It is a
declarative (immutable) functional, reactive, domain specific
programming language. It is intended to be a language that is:

* safe
* powerful
* easy to reason about

With these properties, we hope this language, and the mgmt engine will
allow you to model the real-time systems that you'd like to automate.

This also includes a number of other associated changes. Sorry for the
large size of this patch.
2018-01-20 08:09:29 -05:00
Joe Julian
1c8c0b2915 misc: Don't install packages that are already installed
pacman needs `--needed` to prevent reinstalling packages that are
already installed. Additionally I added `--asdeps` to allow later
cleanup of unneeded dependencies.
2018-02-03 17:17:23 -05:00
Joe Julian
bee1aa00f1 misc: Use bash's command instead of which
Bash has a built-in command, `command`, that will search the path and
return the full path to a command if it exists (or an exit code of 1 if
it does not), preventing the requirement of the `which` package.
2018-02-03 11:23:52 -05:00
Toshaan Bharvani
077b6e540a build: Add cross building option
Added a cross build option using a buildrelease function

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>

build: Add gitignore entry for mgmt-* binaries

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>

build: Update makefile based upon feed back

* rename cross to crossbuild
* added crossbuild to PHONY

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>

build: Change the order of .PHONY as per the rest of the file

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>
2018-02-03 16:43:40 +01:00
James Shubin
e8b03545bb test: Don't fail on tag builds
This seems to be causing our failures with:

$ git fetch --unshallow
fatal: Couldn't find remote ref refs/heads/0.0.x

where x is some tag.

Hopefully this doesn't break the other use case we added this patch for!
2018-01-11 18:03:56 -05:00
James Shubin
70c59eab4a misc: Don't display script name in output 2018-01-11 18:03:04 -05:00
jonathangold
3c677543e0 resources: aws: ec2: Fix closed channel handling
If awschan closes, longpollWatch and snsWatch return nil
instead of an error. This will prevent the engine from
shutting down in case we choose to close the channel
early or from other struct methods.
2018-01-06 15:15:30 -05:00
jonathangold
c455ef2c62 resources: aws: ec2: Send IP addresses and InstanceID 2018-01-03 21:34:28 -05:00
Jonathan Gold
032d0992d6 resources: aws: ec2: Refactor CheckApply
CheckApply was rewritten, using the new describe methods to improve
readability and maintainability.
2018-01-03 21:34:28 -05:00
jonathangold
67837a47ac resources: aws: ec2: Refactor longpollWatch
Complete rewrite of longpollWatch() for correctness and maintanability.
2018-01-03 21:34:28 -05:00
Jonathan Gold
32e3c4e029 resources: aws: ec2: Refactor longpollWatch
This patch simplifies longpollwatch by getting rid of some unnecessary
api calls and breaking the waiters out into their own functions.
2018-01-03 21:34:28 -05:00
Jonathan Gold
76fcb7a06e resources: aws: ec2: Wait for stop and terminate concurrently
In longpollWatch it was no longer sufficient to use only
WaitUntilInstanceStopped as it would block if the instance was
terminated. This patch launches two goroutines in its place, one
waits until the instance stops and the other waits until it
terminates. When either one returns, it cancels their context,
and execution continues.
2018-01-03 21:34:28 -05:00
Jonathan Gold
149a2188e2 resources: aws: ec2: Retry on exceeded wait attempts error
The waiters now return the AwsErr error "ResourceNotReady: exceeded wait
attempts" when the instance state does not converge after 40 retries.
During longpollWatch() we need to detect this error and continue to
the top of the loop so we can restart the waiters and keep watching for
events.
2018-01-03 21:34:28 -05:00
Jonathan Gold
08e7caea6b resources: aws: ec2: CheckApply fix pending and stopping cases
If CheckApply was called when the instance was pending or stopping, it
would return an error. This patch supresses these errors and tells the
engine that the state can't yet be changed.
2018-01-03 21:34:28 -05:00
Jonathan Gold
e330ebc8c9 resources: aws: ec2: Verify SNS message signatures 2018-01-03 21:34:28 -05:00
Jonathan Gold
388a08e13a resources: aws: ec2: Check that policy.Statement != nil 2018-01-03 21:34:28 -05:00
Jonathan Gold
9ba9ef1cbf resources: aws: ec2: Close closeChan before server shutdown
This patch makes sure that closeChan is closed as soon as the main loop
returns, so any channel operations are unblocked before we run shutdown.
This ensures that the server's goroutine can return before shutdown
completes and we don't panic by trying to serve the client after
shutdown returns.
2018-01-03 21:34:27 -05:00
Jonathan Gold
fac004b774 resources: aws: ec2: Update postHandler to process messages 2018-01-03 21:34:27 -05:00
Jonathan Gold
8cd3f28734 resources: aws: ec2: Authorize CloudWatch to publish to sns 2018-01-03 21:34:27 -05:00
Jonathan Gold
dcd23fcf75 resources: aws: ec2: Add CloudWatch rule and target SNS
This patch creates the cloudwatch rule that detects ec2 instance
state changes, and targets the rule to publish on our sns topic
which, in turn, pushes those event notifications to our endpoint.
2018-01-03 21:34:27 -05:00
Jonathan Gold
1162485c2c resources: aws: ec2: Subscribe SNS endpoint to topic
This patch adds methods to subscribe and confirm the subscription
to the sns topic.
2018-01-03 21:34:27 -05:00
Jonathan Gold
966172eac6 resources: aws: ec2: Use custom listener for snsServer
This patch replaces the call to Server.ListenAndServe() with
Server.Serve(listener) in order to make sure the listener is up
and running before we subscribe to the topic in a future patch.
2018-01-03 21:34:27 -05:00
James Shubin
12fce52cd7 legal: Happy 2018 everyone...
Done with:

ack '2017+' -l | xargs sed -i -e 's/2017+/2018+/g'

Checked manually with:

git add -p

Hello to future James from 2019, and Happy Hacking!
2018-01-03 21:22:07 -05:00
Felix Frank
5ca1e2a23f puppet: Avoid empty parameters to puppet mgmtgraph
This solves an issue first observed with golang 1.8.

Creating an exec.Command with an empty string parameter (when no puppet.conf
file is specified) would lead to an error from Puppet, stating that an
unexpected argument was passed to "puppet mgmtgraph print".

The workaround is to not include *any* positional argument (not even the
empty string) when --puppet-conf is not used.
2017-12-26 00:18:46 +01:00
Paul Morgan
98f8a61e83 git: Configure editorconfig to indent with tabs in bash scripts
This follows `test/test-bashfmt.sh` style check(s).
2017-12-20 21:09:15 +00:00
Paul Morgan
2e86d7c5ab git: Ensure the tagging script is idempotent 2017-12-20 21:04:57 +00:00
Jonathan Gold
62ca12608d cli: Add license flag
This patch adds the option to print the license with a cli flag. It
uses go-bindata to store the license file. The file is generated by
running `make bindata` and the result is stored in the bindata
directory.
2017-12-08 00:57:58 -05:00
Jonathan Gold
406aa55667 resources: virt: Update libvirt-xml target
Builds started failing due to go-libvirt-xml 6d97448. In that patch,
the DomainChannelTarget struct was changed from having a single type
field, to having an individual field for each virtualization type.

This patch updates the connection check in Init to reflect the changes
to go-libvirt-xml, so that builds no longer fail.
2017-11-29 19:03:56 -05:00
James Shubin
a76dce8b15 docs: Add missing blog post about augeas resource 2017-11-26 17:15:49 -05:00
James Shubin
b01d453ae3 docs: Refresh documentation to provide a better new user experience
This does some cleanups and moves some things around for a better
experience. If you're an expert in this area, or are a new user who has
some feedback about their first impressions and experiences, please let
us know!
2017-11-25 20:45:57 -05:00
Guillaume Herail
ac629404f4 test: Switch to goimports instead of gofmt
see https://github.com/purpleidea/mgmt/pull/256#issuecomment-346360414
2017-11-25 06:49:00 -05:00
Guillaume Herail
3575d597f7 resources: Add User/Group to ExecRes 2017-11-24 10:38:16 -05:00
Toshaan Bharvani
2affcba3b4 build: Added build option to strip binary
This is a build option in Golang that will strip the binary.
The binary becomes about 50% smaller.

Signed-off-by: Toshaan Bharvani <toshaan@vantosh.com>
2017-11-24 10:26:48 -05:00
James Shubin
846c5f8762 test: Add another check for off-by-one-error commit tags 2017-11-24 09:46:32 -05:00
Julien Pivotto
086af712d2 example: Remove content out of directory definition
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-24 14:26:20 +01:00
Julien Pivotto
2b6e39f283 build: Remove go 1.3 and 1.4 support
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-24 05:35:09 -05:00
Julien Pivotto
472663193a prometheus: Initialize all metrics
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-24 11:02:36 +01:00
James Shubin
879ff838ae resources: Replace golang 1.6 specific code with newer 1.7 version
We now require at least 1.8 so we might as well fix this up.
2017-11-23 10:57:11 -05:00
Julien Pivotto
5e9a085e39 exec: Add autoEdges between ExecRes and PkgRes
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-23 16:30:22 +01:00
Julien Pivotto
c2b5729ebd build: Build mgmt on any go file change
Prior to this commit, running make would only rebuild mgmt when
main.go was changed. It means that make clean build was needed.

With this commit, any go file change in this directory will
trigger a new compilation.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-23 09:32:02 -05:00
Julien Pivotto
fdce9d6a6a prometheus: Initialize mgmt_checkapply_total metrics
It is recommended by Prometheus to initialize metrics:

https://prometheus.io/docs/practices/instrumentation/#avoid-missing-metrics

This commits initialize the mgmt_checkapply_total metric
for each registered resource.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-23 15:23:41 +01:00
Guillaume Herail
bfc2549289 resources: Move FileRes.uid()/.gid() to util.go 2017-11-23 08:34:38 -05:00
James Shubin
52fd1ae73e test: Add check for common doc vs docs ambiguity 2017-11-23 08:20:44 -05:00
Julien Pivotto
23e167616f doc: Fix link to the prometheus wiki
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
2017-11-23 09:52:28 +01:00
James Shubin
51ce83f20b test: Add extra commit message tests for some common mistakes
Feel free to add more if we identify them.
2017-11-21 11:05:20 -05:00
Jonathan Gold
5e5bbf4b39 travis: Allow travis builds to access target branches
Because travis builds only fetch a single branch (master) by default,
test-commit-message.sh only had access to commits in the master branch.
In order to fetch the correct branch for our build, we need to run
'git config remote.origin.fetch..' with the target branch's information
before executing git fetch on the repo in before_install.

Now git will always fetch the appropriate branch.
2017-11-18 21:04:12 -05:00
Guillaume Herail
cbc3a691b9 docker: Bump to golang 1.8 2017-11-16 17:36:35 +01:00
Jonathan Gold
a5247d6e69 resources: aws: ec2: Change event messages to iota consts 2017-11-14 16:48:51 -05:00
Jonathan Gold
d698b82a83 resources: aws: ec2: Start and stop SNS endpoint in snsWatch
This patch adds snsWatch which launches the HTTP server and listens
for messages on awsChan to forward as events to the mgmt engine.
2017-11-11 23:07:12 -05:00
Jonathan Gold
91eff75288 resources: aws: ec2: Add method to make sns topic 2017-11-10 17:31:19 -05:00
James Shubin
91a9edb322 resources: aws: ec2: Fix deadlock on rare error scenarios
If we get an error in the Watch loop, it will send this on awsChan,
which will cause Watch to loop. However, in this scenario it will never
cause closeChan to close, and we will deadlock because we have a
waitGroup in a helper goroutine which is waiting on this channel to
close the context.

Normally this wouldn't be an issue, but since we have more than one
goroutine (with associated waitGroup) it is. It's also good practice to
close all the channels to help avoid this kind of bug.

This patch also moves the waitGroup Wait into a more logical place for
visibility.
2017-11-10 14:17:54 -05:00
Jonathan Gold
c8ddbeaa5c resource: aws: ec2: Add http server 2017-11-09 13:13:42 -05:00
Jonathan Gold
3634b3450d resource: aws: ec2: Move waitgroup to resource struct 2017-11-08 16:57:41 -05:00
Jonathan Gold
c2a5e3f5d8 resources: aws: ec2: Move watch channels into struct 2017-11-08 16:16:01 -05:00
Jonathan Gold
db49fe85e4 resources: aws: ec2: Move chanStruct type out of longpollWatch 2017-11-08 16:08:25 -05:00
Jonathan Gold
567a2e9fd8 resources: aws: ec2: Reorganized consts 2017-11-08 16:02:29 -05:00
Jonathan Gold
987de00e17 resources: aws: ec2: Remove extra wait from Watch
There were two calls to WaitUntilInstanceTerminatedWithContext in a row.
There's no reason to make the call twice.
2017-11-08 16:02:24 -05:00
Jonathan Gold
baeafec74a resources: aws: ec2: Move Watch to longpollWatch 2017-11-08 16:02:12 -05:00
James Shubin
9cfa0b14d4 yamlgraph: Improve error output
This makes it easier to know what's missing.
2017-11-02 09:13:27 -04:00
James Shubin
948ded6792 github: This event is over
And it wasn't successful at all.
2017-11-01 07:07:14 -04:00
James Shubin
3c69619fd9 github: Add new label for design discussions and trackers
Open ideas related to designs can be tracked here. We've already got a
few such tickets open.
2017-11-01 07:04:32 -04:00
Jonathan Gold
e7c4bc7f47 resources: Add UserData field to AwsEc2
UserData specifies first-launch bash and cloud-init commands. See
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
for documentation and examples.
2017-10-30 00:22:30 -04:00
Jonathan Gold
277ecc901b etcd: Plumbed in the new cli flags for advertise urls 2017-10-29 17:16:51 -04:00
Jonathan Gold
0f70c31a30 etcd: Add advertise urls to cli
This patch adds the option to specify URLs to advertise for clients and peers.
This will facilitate etcd communication through nat, where we want to listen
on a local IP, but expose a public IP to clients/peers.
2017-10-28 22:42:27 -04:00
James Shubin
9a97a92e31 github: Use third-party settings app to sync github settings
Let's give this a try. One downside is that giving anyone push access
gives them ability to rename repo and do other bad admin type things.
2017-10-26 05:04:41 -04:00
James Shubin
f9d452ad2c examples: Add longpoll server and client
This is an example of a race-free long-poll server and client. It uses a
redirection method to signal that the "Watch" is running.

Other race-free methods exist.
2017-10-24 04:20:19 -04:00
380 changed files with 32450 additions and 1810 deletions

View File

@@ -12,8 +12,14 @@ end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.sh]
indent_style = tab
[*.go]
indent_style = tab
[Makefile]
indent_style = tab
[*.mcl]
indent_style = tab

View File

@@ -1,20 +1,30 @@
## Tips:
* please read the style guide before submitting your patch:
[docs/style-guide.md](../docs/style-guide.md)
* commit message titles must be in the form:
```topic: Capitalized message with no trailing period```
or:
```topic, topic2: Capitalized message with no trailing period```
* golang code must be formatted according to the standard, please run:
```
make gofmt # formats the entire project correctly
```
or format a single golang file correctly:
```
gofmt -w yourcode.go
```
* please rebase your patch against current git master:
```
git checkout master
git pull origin master
@@ -25,6 +35,7 @@ hub pull-request # or submit with the github web ui
```
* after a patch review, please ping @purpleidea so we know to re-review:
```
# make changes based on reviews...
git add -p # add new changes

96
.github/settings.yml vendored Normal file
View File

@@ -0,0 +1,96 @@
# These settings are synced to GitHub by https://probot.github.io/apps/settings/
repository:
# See https://developer.github.com/v3/repos/#edit for all available settings.
# The name of the repository. Changing this will rename the repository
name: mgmt
# A short description of the repository that will show up on GitHub
description: Next generation distributed, event-driven, parallel config management!
# A URL with more information about the repository
homepage: https://purpleidea.com/tags/mgmtconfig/
# A comma-separated list of topics to set on the repository
topics: golang, go, configuration-management, config-management, devops, etcd, distributed-systems, graph-theory, choreography
# Either `true` to make the repository private, or `false` to make it public.
private: false
# Either `true` to enable issues for this repository, `false` to disable them.
has_issues: true
# Either `true` to enable projects for this repository, or `false` to disable them.
# If projects are disabled for the organization, passing `true` will cause an API error.
has_projects: false
# Either `true` to enable the wiki for this repository, `false` to disable it.
has_wiki: false
# Either `true` to enable downloads for this repository, `false` to disable them.
has_downloads: true
# Updates the default branch for this repository.
default_branch: master
# Either `true` to allow squash-merging pull requests, or `false` to prevent
# squash-merging.
allow_squash_merge: false
# Either `true` to allow merging pull requests with a merge commit, or `false`
# to prevent merging pull requests with merge commits.
allow_merge_commit: false
# Either `true` to allow rebase-merging pull requests, or `false` to prevent
# rebase-merging.
allow_rebase_merge: true
# Labels: define labels for Issues and Pull Requests (in alphabetical order)
labels:
- name: bug
color: fc2929
- name: confirmed
color: d93f0b
- name: design
color: 5319e7
- name: duplicate
color: cccccc
- name: enhancement
color: 84b6eb
- name: good first issue
color: 7057ff
- name: help wanted
color: 159818
- name: invalid
color: e6e6e6
- name: mgmtlove
color: e11d21
- name: question
color: cc317c
- name: wontfix
color: ffffff
# - name: first-timers-only
# # include the old name to rename an existing label
# oldname: Help Wanted
# Collaborators: give specific users access to this repository.
#collaborators:
# - username: purpleidea
# # Note: Only valid on organization-owned repositories.
# # The permission to grant the collaborator. Can be one of:
# # * `pull` - can pull, but not push to or administer this repository.
# # * `push` - can pull and push, but not administer this repository.
# # * `admin` - can pull, push and administer this repository.
# permission: push
# - username: hubot
# permission: pull
# NOTE: The APIs needed for teams are not supported yet by GitHub Apps
# https://developer.github.com/v3/apps/available-endpoints/
#teams:
# - name: core
# permission: admin
# - name: docs
# permission: push

4
.gitignore vendored
View File

@@ -6,7 +6,11 @@
old/
tmp/
*_stringer.go
bindata/*.go
mgmt
mgmt.static
# crossbuild artifacts
build/mgmt-*
mgmt.iml
rpmbuild/
*.deb

View File

@@ -1,25 +1,40 @@
language: go
os:
- linux
go:
- 1.8.x
- 1.9.x
- 1.10.x
- tip
go_import_path: github.com/purpleidea/mgmt
sudo: true
dist: trusty
before_install:
- sudo apt update
# 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
# apt update tends to be flaky in travis, retry up to 3 times on failure
# https://docs.travis-ci.com/user/common-build-problems/#travis_retry
- if [[ "$TRAVIS_OS_NAME" != "osx" ]]; then travis_retry travis_retry sudo apt update; fi
- git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*"
- git fetch --unshallow
install: 'make deps'
script: 'make test'
matrix:
fast_finish: true
fast_finish: false
allow_failures:
- go: 1.10.x
- go: tip
- go: 1.9.x
- os: osx
# include only one build for osx for a quicker build as the nr. of these runners are sparse
include:
- os: osx
go: 1.9.x
# the "secure" channel value is the result of running: ./misc/travis-encrypt.sh
# with a value of: irc.freenode.net#mgmtconfig to eliminate noise from forks...
notifications:
irc:
channels:
- "irc.freenode.net#mgmtconfig"
- secure: htcuWAczm3C1zKC9vUfdRzhIXM1vtF+q0cLlQFXK1IQQlk693/pM30Mmf2L/9V2DVDeps+GyLdip0ARXD1DZEJV0lK+Ca1qbHdFP1r4Xv6l5+jaDb5Y88YU5LI8K758QShiZJojuQ1aO2j8xmmt9V0/5y5QwlpPeHbKYBOFPBX3HvlT9DhvwZNKGhBb4qJOEaPVOwq9IkN3DyQ456MHcJ3q3vF9Lb440uTuLsJNof2AbYZH8ZIHCSG2N8tBj2qhJOpWQboYtQJzE2pRaGkGBL4kYcHZSZMXX8sl4cBM1vx/IRUkvBxJUpLJz2gn/eRI+/gr59juZE2K0+FOLlx9dLnX626Y9xSViopBI6JsIoHJDqNC7aGaF2qaYulGYN65VNKVqmghjgt6JLmmiKeH10hYrJMMvt2rms8l4+5iwmCwXvhH/WU9edzk2p5wqERMnostJFEJib0zI3yzLoF0sdJs+veKtagzfayY2d2l7hlmt951IpqqVWldVgWUcQKVvi8gmRarbwFlK+5D7BEnkUDcLNly/cqf7BgEeX6YfF+FiR4pgfOhYvGCD+2q91NgWQXHBCxbyN0be1TVdkXD94f0Lkn94VyEJJ+PkPlG+rPgFwGcjqN4oEGkJeJmES2If05q2Ms1dJLwYQDL3+Py4lNMSdSWj24TzlFVhtwHepuw=
template:
- "%{repository} (%{commit}: %{author}): %{message}"
- "More info : %{build_url}"
@@ -29,6 +44,6 @@ notifications:
skip_join: false
email:
recipients:
- travis-ci@shubin.ca
- secure: qNkgP6QLl6VXpFQIxas2wggxvIiOmm1/hGRXm4BXsSFzHsJPvMamA3E1HEC7H+luiWTny1jtGSGgTJPV9CX1LtQV0g0S4ThaAvWuKvk3rXO8IVd++iA/Lh1s1H6JdKM0dJtLqFICawjeci4tOQzSvrM2eCBWqT0UYsrQsGHB6AF31GNAH0Acqd5cYeL+ZpbCN+hQEznAZQ7546N25TwqieI8Lg7nisA+lwYYwsaC2+f5RIeyvvKjQv3wzEdBAQ9CI9WQiTOUBnUnyYxMrdomQ/XGF66QnZy9vq5nEP83IFtuhPvSamL7ceT+yJW0jDyBi8sYEV7On7eXzjyHbiYpF4YHcJrFnf5RyV4kQGd6/SC8iZwK4Is4eyeAjDFTC+JafLajw9R9x9bK43BwlRAWOZxjFKe0cU/BVAjmlz87vHgUho2P41+0a5XfajfU6VhA5QFPK6rNH7W1CnA7D/0LmS0yaqJM1OCrm6LfoZEMhe0DxTJ9uWJbr0x1sYao6q8H4xYk+fyRgoBAr2TxYU7kXx8ThiRdzuQ8izdbojlzTYLe8liZMIsjL0axLsLK7YBWrjJUcDFDjR/DqmVxPrvbVFbCi9ChmBw0WmbJvDY0FV8T8dO8wCjg9JEmprAmWPyq0g/F87LFK4tAZqQFJGjP1qwsR9jdwdNTKeCdY656f/Y=
on_failure: change
on_success: change

View File

@@ -1,10 +1,12 @@
This is a list of authors/contributors to the mgmt project.
If you're a contributor, please send a patch with your name.
If you're a core contributor, we might ask you to send a patch with your name.
If you appreciate the work of one of the contributors, thank them a beverage!
For a more exhaustive list please run: git log --format='%aN' | sort -u
This list is sorted alphabetically by first name.
Felix Frank
James Shubin
Johan Bloemberg
Jonathan Gold
Julien Pivotto
Paul Morgan

View File

@@ -1,5 +1,5 @@
Mgmt
Copyright (C) 2013-2017+ James Shubin and the project contributors
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

109
Makefile
View File

@@ -1,5 +1,5 @@
# Mgmt
# Copyright (C) 2013-2017+ James Shubin and the project contributors
# 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
@@ -16,13 +16,15 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SHELL = /usr/bin/env bash
.PHONY: all art cleanart version program path deps run race generate build clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
.SILENT: clean
.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
.SILENT: clean bindata
# a large amount of output from this `find`, can cause `make` to be much slower!
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"-")
OLDGOLANG := $(shell go version | grep -E 'go1.3|go1.4')
ifeq ($(VERSION),$(SVERSION))
RELEASE = 1
else
@@ -38,8 +40,12 @@ USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '='
SERVER = 'dl.fedoraproject.org'
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
ifneq ($(GOTAGS),)
BUILD_FLAGS = -tags '$(GOTAGS)'
BUILD_FLAGS = -tags '$(GOTAGS)'
endif
GOOSARCHES ?= linux/amd64 linux/ppc64 linux/ppc64le linux/arm64 darwin/amd64
GOHOSTOS = $(shell go env GOHOSTOS)
GOHOSTARCH = $(shell go env GOHOSTARCH)
default: build
@@ -101,40 +107,74 @@ run:
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:
@echo "Generating: bindata..."
$(MAKE) --quiet -C bindata
generate:
go generate
build: $(PROGRAM)
lang:
@# recursively run make in child dir named lang
@echo "Generating: lang..."
$(MAKE) --quiet -C lang
$(PROGRAM): main.go
@echo "Building: $(PROGRAM), version: $(SVERSION)..."
ifneq ($(OLDGOLANG),)
@# avoid equals sign in old golang versions eg in: -X foo=bar
time go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o $(PROGRAM) $(BUILD_FLAGS);
else
time go build -i -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o $(PROGRAM) $(BUILD_FLAGS);
endif
# build a `mgmt` binary for current host os/arch
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH}
cp $< $@
$(PROGRAM).static: main.go
$(PROGRAM).static: $(GO_FILES)
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
go generate
ifneq ($(OLDGOLANG),)
@# avoid equals sign in old golang versions eg in: -X foo=bar
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program $(PROGRAM) -X main.version $(SVERSION)' -o $(PROGRAM).static $(BUILD_FLAGS);
else
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION)' -o $(PROGRAM).static $(BUILD_FLAGS);
endif
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: $(PROGRAM)
build-debug: LDFLAGS=
build-debug: $(PROGRAM)
# pattern rule target for (cross)building, mgmt-OS-ARCH will be expanded to the correct build
# extract os and arch from target pattern
GOOS=$(firstword $(subst -, ,$*))
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);
# create a list of binary file names to use as make targets
crossbuild_targets = $(addprefix build/mgmt-,$(subst /,-,${GOOSARCHES}))
crossbuild: ${crossbuild_targets}
clean:
$(MAKE) --quiet -C bindata clean
$(MAKE) --quiet -C lang clean
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
rm -f *_stringer.go # generated by `go generate`
rm -f *_mock.go # generated by `go generate`
# crossbuild artifacts
rm -f build/mgmt-*
test:
test: build
./test.sh
# create all test targets for make tab completion (eg: make test-gofmt)
test_suites=$(shell find test/ -maxdepth 1 -name test-* -exec basename {} .sh \;)
# allow to run only one test suite at a time
${test_suites}: test-%: build
./test.sh $*
# targets to run individual shell tests (eg: make test-shell-load0)
test_shell=$(shell find test/shell/ -maxdepth 1 -name "*.sh" -exec basename {} .sh \;)
$(addprefix test-shell-,${test_shell}): test-shell-%: build
./test/test-shell.sh "$*.sh"
gofmt:
find . -maxdepth 3 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
# TODO: remove gofmt once goimports has a -s option
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec gofmt -s -w {} \;
find . -maxdepth 6 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -not -path './vendor/*' -exec goimports -w {} \;
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" \;
@@ -277,4 +317,27 @@ upload-rpms: rpmbuild/RPMS/ rpmbuild/RPMS/SHA256SUMS rpmbuild/RPMS/SHA256SUMS.as
copr: upload-srpms
./misc/copr-build.py https://$(SERVER)/$(REMOTE_PATH)/SRPMS/$(SRPM_BASE)
#
# deb build
#
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/
build_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:
docker rmi purpleidea/mgmt-build
docker rmi purpleidea/mgmt
# vim: ts=8

View File

@@ -9,6 +9,7 @@
[![Jenkins](https://img.shields.io/badge/jenkins-status-brightgreen.svg?style=flat-square)](https://ci.centos.org/job/purpleidea-mgmt/)
## Community:
Come join us in the `mgmt` community!
| Medium | Link |
@@ -18,70 +19,65 @@ Come join us in the `mgmt` community!
| Mailing list | [mgmtconfig-list@redhat.com](https://www.redhat.com/mailman/listinfo/mgmtconfig-list) |
## Status:
Mgmt is a fairly new project.
We're working towards being minimally useful for production environments.
We aren't feature complete for what we'd consider a 1.x release yet.
With your help you'll be able to influence our design and get us there sooner!
Mgmt is a next generation automation tool. It has similarities to other tools in
the configuration management space, but has a fast, modern, distributed systems
approach. The project contains an engine and a language.
[Please have a look at an introductory video or blog post.](docs/on-the-web.md)
Mgmt is a fairly new project. It is usable today, but not yet feature complete.
With your help you'll be able to influence our design and get us to 1.0 sooner!
Interested developers should read the [quick start guide](docs/quick-start-guide.md).
## Documentation:
Please read, enjoy and help improve our documentation!
| Documentation | Additional Notes |
|---|---|
| [general documentation](docs/documentation.md) | for everyone |
| [quick start guide](docs/quick-start-guide.md) | for mgmt developers |
| [frequently asked questions](docs/faq.md) | for everyone |
| [general documentation](docs/documentation.md) | for everyone |
| [language guide](docs/language-guide.md) | for everyone |
| [function guide](docs/function-guide.md) | for mgmt developers |
| [resource guide](docs/resource-guide.md) | for mgmt developers |
| [style guide](docs/style-guide.md) | for mgmt developers |
| [godoc API reference](https://godoc.org/github.com/purpleidea/mgmt) | for mgmt developers |
| [prometheus guide](docs/prometheus.md) | for everyone |
| [puppet guide](docs/puppet-guide.md) | for puppet sysadmins |
| [development](docs/development.md) | for mgmt developers |
## Questions:
Please ask in the [community](#community)!
If you have a well phrased question that might benefit others, consider asking it by sending a patch to the documentation [FAQ](https://github.com/purpleidea/mgmt/blob/master/docs/documentation.md#usage-and-frequently-asked-questions) section. I'll merge your question, and a patch with the answer!
If you have a well phrased question that might benefit others, consider asking
it by sending a patch to the [FAQ](docs/faq.md) section. I'll merge your
question, and a patch with the answer!
## Roadmap:
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove)
issues if you're a first time contributor to the project or if you're unsure
about what to hack on!
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
Please get involved by working on one of these items or by suggesting something else!
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove) issues if you're a first time contributor to the project or if you're unsure about what to hack on!
Please get involved by working on one of these items or by suggesting something
else!
## Bugs:
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go) to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell) or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible test case.
Feel free to read my article on [debugging golang programs](https://ttboj.wordpress.com/2016/02/15/debugging-golang-programs/).
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go)
to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell)
or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible
test case.
Feel free to read my article on [debugging golang programs](https://purpleidea.com/blog/2016/02/15/debugging-golang-programs/).
## Patches:
We'd love to have your patches! Please send them by email, or as a pull request.
## On the web:
| Author | Format | Subject |
|---|---|---|
| James Shubin | blog | [Next generation configuration mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/) |
| James Shubin | video | [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1) |
| James Shubin | video | [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1) |
| Julian Dunn | video | [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1) |
| Walter Heck | slides | [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3) |
| Marco Marongiu | blog | [On mgmt](http://syslog.me/2016/02/15/leap-or-die/) |
| Felix Frank | blog | [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/) |
| James Shubin | blog | [Automatic edges in mgmt (...and the pkg resource)](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/) |
| James Shubin | blog | [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/) |
| John Arundel | tweet | [“Puppets days are numbered.”](https://twitter.com/bitfield/status/732157519142002688) |
| Felix Frank | blog | [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/) |
| Felix Frank | blog | [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/) |
| James Shubin | blog | [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/) |
| James Shubin | video | [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1) |
| James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf)) |
| Felix Frank | blog | [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/) |
| Felix Frank | blog | [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/) |
| James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1) |
| James Shubin | blog | [Remote execution in mgmt](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/) |
| James Shubin | video | [Recording from High Load Strategy 2016](https://vimeo.com/191493409) |
| James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1) |
| James Shubin | blog | [Send/Recv in mgmt](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/) |
| James Shubin | blog | [Metaparameters in mgmt](https://ttboj.wordpress.com/2017/03/01/metaparameters-in-mgmt/) |
| 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) |
##
[Read what people are saying and publishing about mgmt!](docs/on-the-web.md)
Happy hacking!

24
TODO.md
View File

@@ -1,4 +1,5 @@
# TODO
If you're looking for something to do, look here!
Let us know if you're working on one of the items.
If you'd like something to work on, ping @purpleidea and I'll create an issue
@@ -6,63 +7,78 @@ tailored especially for you! Just let me know your approximate golang skill
level and how many hours you'd like to spend on the patch.
## Package resource
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
## File resource [bug](https://github.com/purpleidea/mgmt/issues/64) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
- [ ] recurse limit support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
## Svc resource
- [ ] base resource improvements
## Exec resource
- [ ] base resource improvements
## Timer resource
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## User/Group resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Virt (libvirt) resource
- [ ] base resource improvements [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Net (systemd-networkd) resource
- [ ] base resource
## Nspawn (systemd-nspawn) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Mount (systemd-mount) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Cron (systemd-timer) resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Http resource
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
## Etcd improvements
- [ ] fix embedded etcd master race
## Torrent/dht file transfer
- [ ] base plumbing
## GPG/Auth improvements
- [ ] base plumbing
## Language improvements
- [ ] language design
- [ ] lexer/parser
- [ ] more core functions
- [ ] automatic language formatter, ala `gofmt`
- [ ] gedit/gnome-builder/gtksourceview syntax highlighting
- [ ] vim syntax highlighting
- [ ] emacs syntax highlighting
- [x] emacs syntax highlighting: see `misc/emacs/`
## Other
- [ ] better error/retry handling
- [ ] deb package target in Makefile
- [ ] reproducible builds

38
bindata/Makefile Normal file
View File

@@ -0,0 +1,38 @@
# Mgmt
# Copyright (C) 2013-2018+ James Shubin and the project contributors
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# The bindata target generates go files from any source defined below. To use
# the files, import the "bindata" package and use:
# `bytes, err := bindata.Asset("FILEPATH")`
# where FILEPATH is the path of the original input file relative to `bindata/`.
.PHONY: build clean
default: build
build: bindata.go
# add more input files as dependencies at the end here...
bindata.go: ../COPYING
# go-bindata --pkg bindata -o <OUTPUT> <INPUT>
go-bindata --pkg bindata -o ./$@ $^
# gofmt the output file
gofmt -s -w $@
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && $$ROOT/misc/header.sh '$@'
clean:
# remove generated bindata/*.go
@ROOT=$$(dirname "$${BASH_SOURCE}")/.. && rm -f *.go

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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

7
debian/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
*.debhelper.log
*debhelper
changelog
debhelper-build-stamp
files
mgmt.substvars
mgmt/*

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
9

17
debian/control vendored Normal file
View File

@@ -0,0 +1,17 @@
Source: mgmt
Maintainer: Johan Bloemberg (aequitas) <mgmt@ijohan.nl>
Build-Depends:
debhelper,
devscripts,
dh-golang,
dh-systemd,
golang-go,
Package: mgmt
Architecture: any
Depends: ${shlibs:Depends}, ${misc:Depends}, packagekit
Suggests: graphviz
Description: mgmt: next generation config management!
The mgmt tool is a next generation config management prototype. It's
not yet ready for production, but we hope to get there soon. Get
involved today!

21
debian/copyright vendored Normal file
View File

@@ -0,0 +1,21 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: mgmt
Source: <https://github.com/purpleidea/mgmt>
Files: *
Copyright: Copyright (C) 2013-2018+ James Shubin and the project contributors
License: GPL-3.0
License: GPL-3.0
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 <https://www.gnu.org/licenses/>.

11
debian/mgmt.docs vendored Normal file
View File

@@ -0,0 +1,11 @@
AUTHORS
COPYING
COPYRIGHT
README.md
THANKS
TODO.md
docs
examples
misc/bashrc.sh
misc/delta-cpu.sh
misc/mgmt.service

2
debian/mgmt.install vendored Normal file
View File

@@ -0,0 +1,2 @@
mgmt usr/bin
misc/mgmt.service /lib/systemd/system

15
debian/rules vendored Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/make -f
export DH_OPTIONS
export DH_GOPKG := mgmt
export DH_GOLANG_INSTALL_ALL := 1
unexport GOROOT
override_dh_auto_build:
make build
override_dh_auto_test:
@echo "Tests are disabled for now"
%:
dh $@ --with=systemd

2
doc.go
View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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

View File

@@ -1,10 +1,10 @@
FROM golang:1.6.2
FROM golang:1.9
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
# Set the reset cache variable
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
ENV REFRESHED_AT 2016-05-10
ENV REFRESHED_AT 2017-11-16
# Update the package list to be able to use required packages
RUN apt-get update

12
docker/Dockerfile.build Normal file
View File

@@ -0,0 +1,12 @@
FROM centos:7
MAINTAINER Karim Boumedhel <karimboumedhel@gmail.com>
ENV GOPATH=/root/gopath
ENV PATH=/opt/rh/rh-ruby22/root/usr/bin:/root/gopath/bin:/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/go/bin
ENV LD_LIBRARY_PATH=/opt/rh/rh-ruby22/root/usr/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
ENV PKG_CONFIG_PATH=/opt/rh/rh-ruby22/root/usr/lib64/pkgconfig${PKG_CONFIG_PATH:+:${PKG_CONFIG_PATH}}
RUN yum -y install epel-release wget unzip git make which centos-release-scl gcc && sed -i "s/enabled=0/enabled=1/" /etc/yum.repos.d/epel-testing.repo && yum -y install rh-ruby22 && wget -O /opt/go1.9.1.linux-amd64.tar.gz https://storage.googleapis.com/golang/go1.9.1.linux-amd64.tar.gz && tar -C /usr/local -xzf /opt/go1.9.1.linux-amd64.tar.gz
RUN mkdir -p $GOPATH/src/github.com/purpleidea && cd $GOPATH/src/github.com/purpleidea && git clone --recursive https://github.com/purpleidea/mgmt
RUN go get -u gopkg.in/alecthomas/gometalinter.v1 && cd $GOPATH/src/github.com/purpleidea/mgmt && make deps && make build
CMD ["/bin/bash"]

View File

@@ -1,10 +1,10 @@
FROM golang:1.6.2
FROM golang:1.9
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
# Set the reset cache variable
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
ENV REFRESHED_AT 2016-05-14
ENV REFRESHED_AT 2017-11-16
RUN apt-get update
@@ -27,5 +27,8 @@ WORKDIR /home/$USER_NAME/mgmt
# Install dependencies
RUN make deps
# Chown $GOPATH
RUN chown -R ${USER_ID}:${GROUP_ID} /go
# Change user
USER ${USER_NAME}

9
docker/Dockerfile.static Normal file
View File

@@ -0,0 +1,9 @@
FROM centos:7
MAINTAINER Karim Boumedhel <karimboumedhel@gmail.com>
RUN yum -y install augeas-libs libvirt-libs && yum clean all
ADD mgmt /usr/bin
RUN chmod 700 /usr/bin/mgmt
ENTRYPOINT ["/usr/bin/mgmt"]
CMD ["-h"]

18
docker/scripts/exec-development Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/bash
# runs command provided as argument inside a development (Linux) Docker container
# Stop on any error
set -e
script_directory="$( cd "$( dirname "$0" )" && pwd )"
project_directory=$script_directory/../..
# Specify the Docker image name
image_name='purpleidea/mgmt:development'
# Run container in development mode
docker run --rm --name=mgm_development --user=mgmt \
-v "$project_directory:/go/src/github.com/purpleidea/mgmt/" \
-w /go/src/github.com/purpleidea/mgmt/ \
-it "$image_name" /bin/bash -c "$*"

View File

@@ -51,7 +51,7 @@ master_doc = 'index'
# General information about the project.
project = u'mgmt'
copyright = u'2013-2017+ James Shubin and the project contributors'
copyright = u'2013-2018+ James Shubin and the project contributors'
author = u'James Shubin'
# The version info for the project you're documenting, acts as replacement for

49
docs/development.md Normal file
View File

@@ -0,0 +1,49 @@
# Development
This document contains some additional information and help regarding
developing `mgmt`. Useful tools, conventions, etc.
Be sure to read [quick start guide](docs/quick-start-guide.md) first.
## Testing
This project has both unit tests in the form of golang tests and integration
tests using shell scripting.
Native golang tests are preferred over tests written in our shell testing
framework. Please see [https://golang.org/pkg/testing/](https://golang.org/pkg/testing/)
for more information.
To run all tests:
```
make test
```
There is a library of quick and small integration tests for the language and
YAML related things, check out [`test/shell/`](/test/shell). Adding a test is as
easy as copying one of the files in [`test/shell/`](/test/shell) and adapting
it.
This test suite won't run by default (unless when on CI server) and needs to be
called explictly using:
```
make test-shell
```
Or run an individual shell test using:
```
make test-shell-load0
```
Tip: you can use TAB completion with `make` to quickly get a list of possible
individual tests to run.
## Tools, integrations, IDE's etc
### IDE/Editor support
- Emacs: see `misc/emacs/`
- [Textmate](https://github.com/aequitas/mgmt.tmbundle)

View File

@@ -1,9 +1,4 @@
# mgmt
Available from:
[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/)
This documentation is available in: [Markdown](https://github.com/purpleidea/mgmt/blob/master/docs/documentation.md) or [PDF](https://pdfdoc-purpleidea.rhcloud.com/pdf/https://github.com/purpleidea/mgmt/blob/master/docs/documentation.md) format.
# General documentation
## Overview
@@ -18,24 +13,21 @@ foundation in and for, new and existing software.
For more information, you may like to read some blog posts from the author:
* [Next generation config mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
* [Automatic edges in mgmt](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
* [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
* [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
* [Remote execution in mgmt](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/)
* [Send/Recv in mgmt](https://ttboj.wordpress.com/2016/12/07/sendrecv-in-mgmt/)
* [Metaparameters in mgmt](https://ttboj.wordpress.com/2017/03/01/metaparameters-in-mgmt/)
* [Next generation config mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
* [Automatic edges in mgmt](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/)
* [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/)
* [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/)
* [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/)
* [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/)
* [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/)
There is also an [introductory video](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) available.
Older videos and other material [is available](https://github.com/purpleidea/mgmt/#on-the-web).
There is also an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1)
available. Older videos and other material [is available](on-the-web.md).
## Setup
During this prototype phase, the tool can be run out of the source directory.
You'll probably want to use ```./run.sh run --yaml examples/graph1.yaml``` to
get started. Beware that this _can_ cause data loss. Understand what you're
doing first, or perform these actions in a virtual environment such as the one
provided by [Oh-My-Vagrant](https://github.com/purpleidea/oh-my-vagrant).
You'll probably want to read the [quick start guide](quick-start-guide.md) to
get going.
## Features
@@ -71,7 +63,7 @@ the meta attributes of that resource to `false`.
#### Blog post
You can read the introductory blog post about this topic here:
[https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
[https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/)
### Autogrouping
@@ -90,7 +82,7 @@ the meta attributes of that resource to `false`.
#### Blog post
You can read the introductory blog post about this topic here:
[https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
[https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/)
### Automatic clustering
@@ -106,7 +98,7 @@ with the `--seeds` variable.
#### Blog post
You can read the introductory blog post about this topic here:
[https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
[https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/)
### Remote ("agent-less") mode
@@ -133,7 +125,7 @@ which need to exchange information that is only available at run time.
#### Blog post
You can read the introductory blog post about this topic here:
[https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/](https://ttboj.wordpress.com/2016/10/07/remote-execution-in-mgmt/)
[https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/)
### Puppet support
@@ -145,15 +137,15 @@ Invoke `mgmt` with the `--puppet` switch, which supports 3 variants:
1. Request the configuration from the Puppet Master (like `puppet agent` does)
mgmt run --puppet agent
`mgmt run --puppet agent`
2. Compile a local manifest file (like `puppet apply`)
mgmt run --puppet /path/to/my/manifest.pp
`mgmt run --puppet /path/to/my/manifest.pp`
3. Compile an ad hoc manifest from the commandline (like `puppet apply -e`)
mgmt run --puppet 'file { "/etc/ntp.conf": ensure => file }'
`mgmt run --puppet 'file { "/etc/ntp.conf": ensure => file }'`
For more details and caveats see [Puppet.md](Puppet.md).
@@ -162,283 +154,41 @@ For more details and caveats see [Puppet.md](Puppet.md).
An introductory post on the Puppet support is on
[Felix's blog](http://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/).
## Resources
This section lists all the built-in resources and their properties. The
resource primitives in `mgmt` are typically more powerful than resources in
other configuration management systems because they can be event based which
lets them respond in real-time to converge to the desired state. This property
allows you to build more complex resources that you probably hadn't considered
in the past.
In addition to the resource specific properties, there are resource properties
(otherwise known as parameters) which can apply to every resource. These are
called [meta parameters](#meta-parameters) and are listed 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.
* [Augeas](#Augeas): Manipulate files using augeas.
* [Exec](#Exec): Execute shell commands on the system.
* [File](#File): Manage files and directories.
* [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.
* [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.
* [Svc](#Svc): Manage system systemd services.
* [Timer](#Timer): Manage system systemd services.
* [Virt](#Virt): Manage virtual machines with libvirt.
### Augeas
The augeas resource uses [augeas](http://augeas.net/) commands to manipulate
files.
### Exec
The exec resource can execute commands on your system.
### File
The file resource manages files and directories. In `mgmt`, directories are
identified by a trailing slash in their path name. File have no such slash.
It has the following properties:
- `path`: file path (directories have a trailing slash here)
- `content`: raw file content
- `state`: either `exists` (the default value) or `absent`
- `mode`: octal unix file permissions
- `owner`: username or uid for the file owner
- `group`: group name or gid for the file group
#### Path
The path property specifies the file or directory that we are managing.
#### Content
The content property is a string that specifies the desired file contents.
#### Source
The source property points to a source file or directory path that we wish to
copy over and use as the desired contents for our resource.
#### State
The state property describes the action we'd like to apply for the resource. The
possible values are: `exists` and `absent`.
#### Recurse
The recurse property limits whether file resource operations should recurse into
and monitor directory contents with a depth greater than one.
#### Force
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.
### Hostname
The hostname resource manages static, transient/dynamic and pretty hostnames
on the system and watches them for changes.
#### static_hostname
The static hostname is the one configured in /etc/hostname or a similar
file.
It is chosen by the local user. It is not always in sync with the current
host name as returned by the gethostname() system call.
#### transient_hostname
The transient / dynamic hostname is the one configured via the kernel's
sethostbyname().
It can be different from the static hostname in case DHCP or mDNS have been
configured to change the name based on network information.
#### pretty_hostname
The pretty hostname is a free-form UTF8 host name for presentation to the user.
#### hostname
Hostname is the fallback value for all 3 fields above, if only `hostname` is
specified, it will set all 3 fields to this value.
### KV
The KV resource sets a key and value pair in the global world database. This is
quite useful for setting a flag after a number of resources have run. It will
ignore database updates to the value that are greater in compare order than the
requested key if the `SkipLessThan` parameter is set to true. If we receive a
refresh, then the stored value will be reset to the requested value even if the
stored value is greater.
#### Key
The string key used to store the key.
#### Value
The string value to set. This can also be set via Send/Recv.
#### SkipLessThan
If this parameter is set to `true`, then it will ignore updating the value as
long as the database versions are greater than the requested value. The compare
operation used is based on the `SkipCmpStyle` parameter.
#### SkipCmpStyle
By default this converts the string values to integers and compares them as you
would expect.
### Msg
The msg resource sends messages to the main log, or an external service such
as systemd's journal.
### Noop
The noop resource does absolutely nothing. It does have some utility in testing
`mgmt` and also as a placeholder in the resource graph.
### Nspawn
The nspawn resource is used to manage systemd-machined style containers.
### Password
The password resource can generate a random string to be used as a password. It
will re-generate the password if it receives a refresh notification.
### Pkg
The pkg resource is used to manage system packages. This resource works on many
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.
### Svc
The service resource is still very WIP. Please help us my improving it!
### Timer
This resource needs better documentation. Please help us my improving it!
### Virt
The virt resource can manage virtual machines via libvirt.
## Usage and frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### Why did you start this project?
I wanted a next generation config management solution that didn't have all of
the design flaws or limitations that the current generation of tools do, and no
tool existed!
### Why did you use etcd? What about consul?
Etcd and consul are both written in golang, which made them the top two
contenders for my prototype. Ultimately a choice had to be made, and etcd was
chosen, but it was also somewhat arbitrary. If there is available interest,
good reasoning, *and* patches, then we would consider either switching or
supporting both, but this is not a high priority at this time.
### Can I use an existing etcd cluster instead of the automatic embedded servers?
Yes, it's possible to use an existing etcd cluster instead of the automatic,
elastic embedded etcd servers. To do so, simply point to the cluster with the
`--seeds` variable, the same way you would if you were seeding a new member to
an existing mgmt cluster.
The downside to this approach is that you won't benefit from the automatic
elastic nature of the embedded etcd servers, and that you're responsible if you
accidentally break your etcd cluster, or if you use an unsupported version.
### What does the error message about an inconsistent dataDir mean?
If you get an error message similar to:
```
Etcd: Connect: CtxError...
Etcd: CtxError: Reason: CtxDelayErr(5s): No endpoints available yet!
Etcd: Connect: Endpoints: []
Etcd: The dataDir (/var/lib/mgmt/etcd) might be inconsistent or corrupt.
```
This happens when there are a series of fatal connect errors in a row. This can
happen when you start `mgmt` using a dataDir that doesn't correspond to the
current cluster view. As a result, the embedded etcd server never finishes
starting up, and as a result, a default endpoint never gets added. The solution
is to either reconcile the mistake, and if there is no important data saved, you
can remove the etcd dataDir. This is typically `/var/lib/mgmt/etcd/member/`.
### Why do resources have both a `Compare` method and an `IFF` (on the UID) method?
The `Compare()` methods are for determining if two resources are effectively the
same, which is used to make graph change delta's efficient. This is when we want
to change from the current running graph to a new graph, but preserve the common
vertices. Since we want to make this process efficient, we only update the parts
that are different, and leave everything else alone. This `Compare()` method can
tell us if two resources are the same.
The `IFF()` method is part of the whole UID system, which is for discerning if a
resource meets the requirements another expects for an automatic edge. This is
because the automatic edge system assumes a unified UID pattern to test for
equality. In the future it might be helpful or sane to merge the two similar
comparison functions although for now they are separate because they are
actually answer different questions.
### Did you know that there is a band named `MGMT`?
I didn't realize this when naming the project, and it is accidental. After much
anguishing, I chose the name because it was short and I thought it was
appropriately descriptive. If you need a less ambiguous search term or phrase,
you can try using `mgmtconfig` or `mgmt config`.
### You didn't answer my question, or I have a question!
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
to see if someone can help you. Once we get a big enough community going, we'll
add a mailing list. If you don't get any response from the above, you can
contact me through my [technical blog](https://ttboj.wordpress.com/contact/)
and I'll do my best to help. If you have a good question, please add it as a
patch to this documentation. I'll merge your question, and add a patch with the
answer!
## Reference
Please note that there are a number of undocumented options. For more
information on these options, please view the source at:
[https://github.com/purpleidea/mgmt/](https://github.com/purpleidea/mgmt/).
If you feel that a well used option needs documenting here, please patch it!
### Overview of reference
* [Meta parameters](#meta-parameters): List of available resource meta parameters.
* [Graph definition file](#graph-definition-file): Main graph definition file.
* [Command line](#command-line): Command line parameters.
* [Compilation options](#compilation-options): Compilation options.
### Meta parameters
These meta parameters are special parameters (or properties) which can apply to
any resource. The usefulness of doing so will depend on the particular meta
parameter and resource combination.
#### AutoEdge
Boolean. Should we generate auto edges for this resource?
#### AutoGroup
Boolean. Should we attempt to automatically group this resource with others?
#### Noop
Boolean. Should the Apply portion of the CheckApply method of the resource
make any changes? Noop is a concatenation of no-operation.
#### Retry
Integer. The number of times to retry running the resource on error. Use -1 for
infinite. This currently applies for both the Watch operation (which can fail)
and for the CheckApply operation. While they could have separate values, I've
@@ -446,6 +196,7 @@ decided to use the same ones for both until there's a proper reason to want to
do something differently for the Watch errors.
#### Delay
Integer. Number of milliseconds to wait between retries. The same value is
shared between the Watch and CheckApply retries. This currently applies for both
the Watch operation (which can fail) and for the CheckApply operation. While
@@ -454,6 +205,7 @@ until there's a proper reason to want to do something differently for the Watch
errors.
#### Poll
Integer. Number of seconds to wait between `CheckApply` checks. If this is
greater than zero, then the standard event based `Watch` mechanism for this
resource is replaced with a simple polling mechanism. In general, this is not
@@ -471,6 +223,7 @@ which is another way of saying that if the resource finally settles down to give
the graph enough time, it can probably converge.
#### Limit
Float. Maximum rate of `CheckApply` runs started per second. Useful to limit
an especially _eventful_ process from causing excessive checks to run. This
defaults to `+Infinity` which adds no limiting. If you change this value, you
@@ -478,12 +231,14 @@ will also need to change the `Burst` value to a non-zero value. Please see the
[rate](https://godoc.org/golang.org/x/time/rate) package for more information.
#### Burst
Integer. Burst is the maximum number of runs which can happen without invoking
the rate limiter as designated by the `Limit` value. If the `Limit` is not set
to `+Infinity`, this must be a non-zero value. Please see the
[rate](https://godoc.org/golang.org/x/time/rate) package for more information.
#### Sema
List of string ids. Sema is a P/V style counting semaphore which can be used to
limit parallelism during the CheckApply phase of resource execution. Each
resource can have `N` different semaphores which share a graph global namespace.
@@ -495,30 +250,37 @@ id's include: `some_id`, `hello:42`, `not:smart:4` and `:13`. It is expected
that the last bare example be only used by the engine to add a global semaphore.
### Graph definition file
graph.yaml is the compiled graph definition file. The format is currently
undocumented, but by looking through the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples)
you can probably figure out most of it, as it's fairly intuitive.
### Command line
The main interface to the `mgmt` tool is the command line. For the most recent
documentation, please run `mgmt --help`.
#### `--yaml <graph.yaml>`
Point to a graph file to run.
#### `--converged-timeout <seconds>`
Exit if the machine has converged for approximately this many seconds.
#### `--max-runtime <seconds>`
Exit when the agent has run for approximately this many seconds. This is not
generally recommended, but may be useful for users who know what they're doing.
#### `--noop`
Globally force all resources into no-op mode. This also disables the export to
etcd functionality, but does not disable resource collection, however all
resources that are collected will have their individual noop settings set.
#### `--sema <size>`
Globally add a counting semaphore of this size to each resource in the graph.
The semaphore will get given an id of `:size`. In other words if you specify a
size of 42, you can expect a semaphore if named: `:42`. It is expected that
@@ -528,38 +290,46 @@ than zero at this time. The traditional non-parallel execution found in config
management tools such as `Puppet` can be obtained with `--sema 1`.
#### `--remote <graph.yaml>`
Point to a graph file to run on the remote host specified within. This parameter
can be used multiple times if you'd like to remotely run on multiple hosts in
parallel.
#### `--allow-interactive`
Allow interactive prompting for SSH passwords if there is no authentication
method that works.
#### `--ssh-priv-id-rsa`
Specify the path for finding SSH keys. This defaults to `~/.ssh/id_rsa`. To
never use this method of authentication, set this to the empty string.
#### `--cconns`
The maximum number of concurrent remote ssh connections to run. This defaults
to `0`, which means unlimited.
#### `--no-caching`
Don't allow remote caching of the remote execution binary. This will require
the binary to be copied over for every remote execution, but it limits the
likelihood that there is leftover information from the configuration process.
#### `--prefix <path>`
Specify a path to a custom working directory prefix. This directory will get
created if it does not exist. This usually defaults to `/var/lib/mgmt/`. This
can't be combined with the `--tmp-prefix` option. It can be combined with the
`--allow-tmp-prefix` option.
#### `--tmp-prefix`
If this option is specified, a temporary prefix will be used instead of the
default prefix. This can't be combined with the `--prefix` option.
#### `--allow-tmp-prefix`
If this option is specified, we will attempt to fall back to a temporary prefix
if the primary prefix couldn't be created. This is useful for avoiding failures
in environments where the primary prefix may or may not be available, but you'd
@@ -596,12 +366,14 @@ GOTAGS="noaugeas novirt" make build
```
## Examples
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git
source repository. It is available from:
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples)
directory in the git source repository. It is available from:
[https://github.com/purpleidea/mgmt/tree/master/examples](https://github.com/purpleidea/mgmt/tree/master/examples)
### Systemd:
See [`misc/mgmt.service`](misc/mgmt.service) for a sample systemd unit file.
This unit file is part of the RPM.
@@ -629,13 +401,13 @@ This is a project that I started in my free time in 2013. Development is driven
by all of our collective patches! Dive right in, and start hacking!
Please contact me if you'd like to invite me to speak about this at your event.
You can follow along [on my technical blog](https://ttboj.wordpress.com/).
You can follow along [on my technical blog](https://purpleidea.com/blog/).
To report any bugs, please file a ticket at: [https://github.com/purpleidea/mgmt/issues](https://github.com/purpleidea/mgmt/issues).
## Authors
Copyright (C) 2013-2017+ James Shubin and the project contributors
Copyright (C) 2013-2018+ James Shubin and the project contributors
Please see the
[AUTHORS](https://github.com/purpleidea/mgmt/tree/master/AUTHORS) file
@@ -643,4 +415,4 @@ for more information.
* [github](https://github.com/purpleidea/)
* [&#64;purpleidea](https://twitter.com/#!/purpleidea)
* [https://ttboj.wordpress.com/](https://ttboj.wordpress.com/)
* [https://purpleidea.com/](https://purpleidea.com/)

267
docs/faq.md Normal file
View File

@@ -0,0 +1,267 @@
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### Why did you start this project?
I wanted a next generation config management solution that didn't have all of
the design flaws or limitations that the current generation of tools do, and no
tool existed!
### How do I contribute to the project if I don't know `golang`?
There are many different ways you can contribute to the project. They can be
broadly divided into two main categories:
1. With contributions written in `golang`
2. With contributions _not_ written in `golang`
If you do not know `golang`, and have no desire to learn, you can still
contribute to mgmt by using it, testing it, writing docs, or even just by
telling your friends about it. If you don't mind some coding, learning about the
[mgmt language](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)
might be an enjoyable experience for you. It is a small [DSL](https://en.wikipedia.org/wiki/Domain-specific_language)
and not a general purpose programming language, and you might find it more fun
than what you're typically used to. One of the reasons the mgmt author got into
writing automation modules, was because he found it much more fun to build with
a higher level DSL, than in a general purpose programming language.
If you do not know `golang`, and would like to learn, are a beginner and want to
improve your skills, or want to gain some great interdisciplinary systems
engineering knowledge around a cool automation project, we're happy to mentor
you. Here are some pre-requisites steps which we recommend:
1. Make sure you have a somewhat recent GNU/Linux environment to hack on. A
recent [Fedora](https://getfedora.org/) or [Debian](https://www.debian.org/)
environment is recommended. Developing, testing, and contributing on `macOS` or
`Windows` will be either more difficult or impossible.
2. Ensure that you're mildly comfortable with the basics of using `git`. You can
find a number of tutorials online.
3. Spend between four to six hours with the [golang tour](https://tour.golang.org/).
Skip over the longer problems, but try and get a solid overview of everything.
If you forget something, you can always go back and repeat those parts.
4. Connect to our [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig)
IRC channel on the [Freenode](https://freenode.net/) network. You can use any
IRC client that you'd like, but the [hosted web portal](https://webchat.freenode.net/?channels=#mgmtconfig)
will suffice if you don't know what else to use.
5. Now it's time to try and starting writing a patch! We have tagged a bunch of
[open issues as #mgmtlove](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
for new users to have somewhere to get involved. Look through them to see if
something interests you. If you find one, let us know you're working on it by
leaving a comment in the ticket. We'll be around to answer questions in the IRC
channel, and to create new issues if there wasn't something that fit your
interests. When you submit a patch, we'll review it and give you some feedback.
Over time, we hope you'll learn a lot while supporting the project! Now get
hacking!
### Is this project ready for production?
Compared to some existing automation tools out there, mgmt is a relatively new
project. It is probably not as feature complete as some other software, but it
also offers a number of features which are not currently available elsewhere.
Because we have not released a `1.0` release yet, we are not guaranteeing
stability of the internal or external API's. We only change them if it's really
necessary, and we don't expect anything particularly drastic to occur. We would
expect it to be relatively easy to adapt your code if such changes happened.
As with all software, bugs can occur, and while we make no guarantees of being
bug-free, there are a number of things we've done to reduce the chances of one
causing you trouble:
1. Our software is written in golang, which is a memory-safe language, and which
is known to reduce or eliminate entire classes of bugs.
2. We have a test suite which we run on every commit, and every 24 hours. If you
have a particular case that you'd like to test, you are welcome to add it in!
3. The mgmt language itself offers a number of safety features. You can
[read about them in the introductory blog post](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/).
Having said all this, as with all software, there are still missing features
which some users might want in their production environments. We're working hard
to get all of those implemented, but we hope that you'll get involved and help
us finish off the ones that are most important to you. We are happy to mentor
new contributors, and have even [tagged](https://github.com/purpleidea/mgmt/issues?q=is%3Aissue+is%3Aopen+label%3Amgmtlove)
a number of issues if you need help getting started.
Some of the current limitations include:
* Auth hasn't been implemented yet, so you should only use it in trusted
environments (not on publicly accessible networks) for now.
* The number of built-in core functions is still small. You may encounter
scenarios where you're missing a function. The good news is that it's relatively
easy to add this missing functionality yourself. In time, with your help, the
list will grow!
* Large file distribution is not yet implemented. You might want a scenario
where mgmt is used to distribute large files (such as `.iso` images) throughout
your cluster. While this isn't a common use-case, it won't be possible until
someone wants to write the patch. (Mentoring available!) You can workaround this
easily by storing those files on a separate fileserver for the interim.
* There isn't an ecosystem of community `modules` yet. We've got this on our
roadmap, so please stay tuned!
We hope you'll participate as an early adopter. Every additional pair of helping
hands gets us all there faster! It's quite possible to use this to build useful
automation today, and we hope you'll start getting familiar with the software.
### Why did you use etcd? What about consul?
Etcd and consul are both written in golang, which made them the top two
contenders for my prototype. Ultimately a choice had to be made, and etcd was
chosen, but it was also somewhat arbitrary. If there is available interest,
good reasoning, *and* patches, then we would consider either switching or
supporting both, but this is not a high priority at this time.
### Can I use an existing etcd cluster instead of the automatic embedded servers?
Yes, it's possible to use an existing etcd cluster instead of the automatic,
elastic embedded etcd servers. To do so, simply point to the cluster with the
`--seeds` variable, the same way you would if you were seeding a new member to
an existing mgmt cluster.
The downside to this approach is that you won't benefit from the automatic
elastic nature of the embedded etcd servers, and that you're responsible if you
accidentally break your etcd cluster, or if you use an unsupported version.
### How can I run `mgmt` on-demand, or in `cron`, instead of continuously?
By default, `mgmt` will run continuously in an attempt to keep your machine in a
converged state, even as external forces change the current state, or as your
time-varying desired state changes over time. (You can write code in the mgmt
language which will let you describe a desired state which might change over
time.)
Some users might prefer to only run `mgmt` on-demand manually, or at a set
interval via a tool like `cron`. In order to do so, `mgmt` must have a way to
shut itself down after a single "run". This feature is possible with the
`--converged-timeout` flag. You may specify this flag, along with a number of
seconds as the argument, and when there has been no activity for that many
seconds, the program will shutdown.
Alternatively, while it is not recommended, if you'd like to ensure the program
never runs for longer that a specific number of seconds, you can ask it to
shutdown after that time interval using the `--max-runtime` flag. This also
requires a number of seconds as an argument.
#### Example:
```
./mgmt run --lang examples/lang/hello0.mcl --converged-timeout=5
```
### What does the error message about an inconsistent dataDir mean?
If you get an error message similar to:
```
Etcd: Connect: CtxError...
Etcd: CtxError: Reason: CtxDelayErr(5s): No endpoints available yet!
Etcd: Connect: Endpoints: []
Etcd: The dataDir (/var/lib/mgmt/etcd) might be inconsistent or corrupt.
```
This happens when there are a series of fatal connect errors in a row. This can
happen when you start `mgmt` using a dataDir that doesn't correspond to the
current cluster view. As a result, the embedded etcd server never finishes
starting up, and as a result, a default endpoint never gets added. The solution
is to either reconcile the mistake, and if there is no important data saved, you
can remove the etcd dataDir. This is typically `/var/lib/mgmt/etcd/member/`.
### Why do resources have both a `Compare` method and an `IFF` (on the UID) method?
The `Compare()` methods are for determining if two resources are effectively the
same, which is used to make graph change delta's efficient. This is when we want
to change from the current running graph to a new graph, but preserve the common
vertices. Since we want to make this process efficient, we only update the parts
that are different, and leave everything else alone. This `Compare()` method can
tell us if two resources are the same.
The `IFF()` method is part of the whole UID system, which is for discerning if a
resource meets the requirements another expects for an automatic edge. This is
because the automatic edge system assumes a unified UID pattern to test for
equality. In the future it might be helpful or sane to merge the two similar
comparison functions although for now they are separate because they are
actually answer different questions.
### Does this support Windows? OSX? GNU Hurd?
Mgmt probably works best on Linux, because that's what most developers use for
serious automation workloads. Support for non-Linux operating systems isn't a
high priority of mine, but we're happy to accept patches for missing features
or resources that you think would make sense on your favourite platform.
### Why aren't you using `glide` or `godep` for dependency management?
Vendoring dependencies means that as the git master branch of each dependency
marches on, you're left behind using an old version. As a result, bug fixes and
improvements are not automatically brought into the project. Instead, we run our
complete test suite against the entire project (with the latest dependencies)
[every 24 hours](https://docs.travis-ci.com/user/cron-jobs/) to ensure that it
all still works.
Occasionally a dependency breaks API and causes a failure. In those situations,
we're notified almost immediately, it's easy to see exactly which commit caused
the breakage, and we can either quickly notify the author (if it was a mistake)
or update our code if it was a sensible change. This also puts less burden on
authors to support old, legacy versions of their software unnecessarily.
Historically, we've had approximately one such breakage per year, which were all
detected and fixed within a few hours. The cost of these small, rare,
interruptions is much less expensive than having to periodically move every
dependency in the project to the latest versions. Some examples of this include:
* We caught the `go-bindata` swap before it was publicly known, and fixed it in:
[adbe9c7be178898de3645b0ed17ed2ca06646017](https://github.com/purpleidea/mgmt/commit/adbe9c7be178898de3645b0ed17ed2ca06646017).
* We caught the `codegangsta/cli` API change improvement, and fixed it in:
[ab73261fd4e98cf7ecb08066ad228a8f559ba16a](https://github.com/purpleidea/mgmt/commit/ab73261fd4e98cf7ecb08066ad228a8f559ba16a).
* We caught an un-announced libvirt API change, and promptly fixed it in:
[95cb94a03958a9d2ebf01df0821a8c13a4f3a28c](https://github.com/purpleidea/mgmt/commit/95cb94a03958a9d2ebf01df0821a8c13a4f3a28c).
If we choose responsible dependencies, then it usually means that those authors
are also responsible with their changes to API and to git master. If we ever
find that it's not the case, then we will either switch that dependency to a
more responsible version, or fork it if necessary.
Occasionally, we want to pin a dependency to a particular version. This can
happen if the project treats `git master` as an unstable branch, or because a
dependency needs a newer version of golang than the minimum that we require for
our project. In those cases it's sensible to assume the technical debt, and
vendor the dependency. The common tools such as `glide` and `godep` work by
requiring you install their software, and by either storing a yaml file with the
version of that dependency in your repository, and/or copying all of that code
into git and explicitly storing it. This project thinks that all of these
solutions are wasteful and unnecessary, particularly when an existing elegant
solution already exists: `[git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules)`.
The advantages of using `git submodules` are three-fold:
1. You already have the required tools installed.
2. You only store a pointer to the dependency, not additional files or code.
3. The git submodule tools let you easily switch dependency versions, see diff
output, and responsibly plan and test your versions bumps with ease.
Don't blindly use the tools that others tell you to. Learn what they do, think
for yourself, and become a power user today! That process led us to using
`git submodules`. Hopefully you'll come to the same conclusions that we did.
### Did you know that there is a band named `MGMT`?
I didn't realize this when naming the project, and it is accidental. After much
anguishing, I chose the name because it was short and I thought it was
appropriately descriptive. If you need a less ambiguous search term or phrase,
you can try using `mgmtconfig` or `mgmt config`.
It also doesn't stand for
[Methyl Guanine Methyl Transferase](https://en.wikipedia.org/wiki/O-6-methylguanine-DNA_methyltransferase)
which definitely existed before the band did.
### You didn't answer my question, or I have a question!
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
to see if someone can help you. Once we get a big enough community going, we'll
add a mailing list. If you don't get any response from the above, you can
contact me through my [technical blog](https://purpleidea.com/contact/)
and I'll do my best to help. If you have a good question, please add it as a
patch to this documentation. I'll merge your question, and add a patch with the
answer!

437
docs/function-guide.md Normal file
View File

@@ -0,0 +1,437 @@
# Function guide
## Overview
The `mgmt` tool has built-in functions which add useful, reactive functionality
to the language. This guide describes the different function API's that are
available. It is meant to instruct developers on how to write new functions.
Since `mgmt` and the core functions are written in golang, some prior golang
knowledge is assumed.
## Theory
Functions in `mgmt` are similar to functions in other languages, however they
also have a [reactive](https://en.wikipedia.org/wiki/Functional_reactive_programming)
component. Our functions can produce events over time, and there are different
ways to write functions. For some background on this design, please read the
[original article](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)
on the subject.
## Native Functions
Native functions are functions which are implemented in the mgmt language
itself. These are currently not available yet, but are coming soon. Stay tuned!
## Simple Function API
Most functions should be implemented using the simple function API. This API
allows you to implement simple, static, [pure](https://en.wikipedia.org/wiki/Pure_function)
functions that don't require you to write much boilerplate code. They will be
automatically re-evaluated as needed when their input values change. These will
all be automatically made available as helper functions within mgmt templates,
and are also available for use anywhere inside mgmt programs.
You'll need some basic knowledge of using the [`types`](https://github.com/purpleidea/mgmt/tree/master/lang/types)
library which is included with mgmt. This library lets you interact with the
available types and values in the mgmt language. It is very easy to use, and
should be fairly intuitive. Most of what you'll need to know can be inferred
from looking at example code.
To implement a function, you'll need to create a file in
[`lang/funcs/simple/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simple/).
The function should be implemented as a `FuncValue` in our type system. It is
then registered with the engine during `init()`. An example explains it best:
### Example
```golang
package simple
import (
"fmt"
"github.com/purpleidea/mgmt/lang/types"
)
// you must register your functions in init when the program starts up
func init() {
// Example function that squares an int and prints out answer as an str.
Register("talkingsquare", &types.FuncValue{
T: types.NewType("func(a int) str"), // declare the signature
V: func(input []types.Value) (types.Value, error) {
i := input[0].Int() // get first arg as an int64
// must return the above specified value
return &types.StrValue{
V: fmt.Sprintf("%d^2 is %d", i, i * i),
}, nil // no serious errors occurred
},
})
}
```
This simple function accepts one `int` as input, and returns one `str`.
Functions can have zero or more inputs, and must have exactly one output. You
must be sure to use the `types` library correctly, since if you try and access
an input which should not exist (eg: `input[2]`, when there are only two
that are expected), then you will cause a panic. If you have declared that a
particular argument is an `int` but you try to read it with `.Bool()` you will
also cause a panic. Lastly, make sure that you return a value in the correct
type or you will also cause a panic!
If anything goes wrong, you can return an error, however this will cause the
mgmt engine to shutdown. It should be seen as the equivalent to calling a
`panic()`, however it is safer because it brings the engine down cleanly.
Ideally, your functions should never need to error. You should never cause a
real `panic()`, since this could have negative consequences to the system.
## Simple Polymorphic Function API
Most functions should be implemented using the simple function API. If they need
to have multiple polymorphic forms under the same name, then you can use this
API. This is useful for situations when it would be unhelpful to name the
functions differently, or when the number of possible signatures for the
function would be infinite.
The canonical example of this is the `len` function which returns the number of
elements in either a `list` or a `map`. Since lists and maps are two different
types, you can see that polymorphism is more convenient than requiring a
`listlen` and `maplen` function. Nevertheless, it is also required because a
`list of int` is a different type than a `list of str`, which is a different
type than a `list of list of str` and so on. As you can see the number of
possible input types for such a `len` function is infinite.
Another downside to implementing your functions with this API is that they will
*not* be made available for use inside templates. This is a limitation of the
`golang` template library. In the future if this limitation proves to be
significantly annoying, we might consider writing our own template library.
As with the simple, non-polymorphic API, you can only implement [pure](https://en.wikipedia.org/wiki/Pure_function)
functions, without writing too much boilerplate code. They will be automatically
re-evaluated as needed when their input values change.
To implement a function, you'll need to create a file in
[`lang/funcs/simplepoly/`](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/simplepoly/).
The function should be implemented as a list of `FuncValue`'s in our type
system. It is then registered with the engine during `init()`. You may also use
the `variant` type in your type definitions. This special type will never be
seen inside a running program, and will get converted to a concrete type if a
suitable match to this signature can be found. Be warned that signatures which
contain too many variants, or which are very general, might be hard for the
compiler to match, and ambiguous type graphs make for user compiler errors.
An example explains it best:
### Example
```golang
package simplepoly
import (
"fmt"
"github.com/purpleidea/mgmt/lang/types"
)
func init() {
Register("len", []*types.FuncValue{
{
T: types.NewType("func([]variant) int"),
V: Len,
},
{
T: types.NewType("func({variant: variant}) int"),
V: Len,
},
})
}
// Len returns the number of elements in a list or the number of key pairs in a
// map. It can operate on either of these types.
func Len(input []types.Value) (types.Value, error) {
var length int
switch k := input[0].Type().Kind; k {
case types.KindList:
length = len(input[0].List())
case types.KindMap:
length = len(input[0].Map())
default:
return nil, fmt.Errorf("unsupported kind: %+v", k)
}
return &types.IntValue{
V: int64(length),
}, nil
}
```
This simple polymorphic function can accept an infinite number of signatures, of
which there are two basic forms. Both forms return an `int` as is seen above.
The first form takes a `[]variant` which means a `list` of `variant`'s, which
means that it can be a list of any type, since `variant` itself is not a
concrete type. The second form accepts a `{variant: variant}`, which means that
it accepts any form of `map` as input.
The implementation for both of these forms is the same: it is handled by the
same `Len` function which is clever enough to be able to deal with any of the
type signatures possible from those two patterns.
At compile time, if your `mcl` code type checks correctly, a concrete type will
be known for each and every usage of the `len` function, and specific values
will be passed in for this code to compute the length of. As usual, make sure to
only write safe code that will not panic! A panic is a bug. If you really cannot
continue, then you must return an error.
## Function API
To implement a reactive function in `mgmt` it must satisfy the
[`Func`](https://github.com/purpleidea/mgmt/blob/master/lang/interfaces/func.go)
interface. Using the [Simple Function API](#simple-function-api) is preferable
if it meets your needs. Most functions will be able to use that API. If you
really need something more powerful, then you can use the regular function API.
What follows are each of the method signatures and a description of each.
### Default
```golang
Info() *interfaces.Info
```
This returns some information about the function. It is necessary so that the
compiler can type check the code correctly, and know what optimizations can be
performed. This is usually the first method which is called by the engine.
#### Example
```golang
func (obj *FooFunc) Info() *interfaces.Info {
return &interfaces.Info{
Pure: true,
Sig: types.NewType("func(a int) str"),
}
}
```
### Init
```golang
Init(init *interfaces.Init) error
```
This is called to initialize the function. If something goes wrong, it should
return an error. It is passed a struct that contains all the important
information and poiinters that it might need to work with throughout its
lifetime. As a result, it will need to save a copy to that pointer for future
use in the other methods.
#### Example
```golang
// Init runs some startup code for this function.
func (obj *FooFunc) Init(init *interfaces.Init) error {
obj.init = init
obj.closeChan = make(chan struct{}) // shutdown signal
return nil
}
```
### Close
```golang
Close() error
```
This is called to cleanup the function. It usually causes the stream to
shutdown. Even if `Stream()` decided to shutdown early, it might still get
called. It is usually called by the engine to tell the function to shutdown.
#### Example
```golang
// Close runs some shutdown code for this function and turns off the stream.
func (obj *FooFunc) Close() error {
close(obj.closeChan) // send a signal to tell the stream to close
return nil
}
```
### Stream
```golang
Stream() error
```
`Stream` is where the real _work_ is done. This method is started by the
language function engine. It will run this function while simultaneously sending
it values on the `input` channel. It will only send a complete set of input
values. You should send a value to the output channel when you have decided that
one should be produced. Make sure to only use input values of the expected type
as declared in the `Info` struct, and send values of the similarly declared
appropriate return type. Failure to do so will may result in a panic and
sadness.
#### Example
```golang
// Stream returns the single value that was generated and then closes.
func (obj *FooFunc) Stream() error {
defer close(obj.init.Output) // the sender closes
var result string
for {
select {
case input, ok := <-obj.init.Input:
if !ok {
return nil // can't output any more
}
ix := input.Struct()["a"].Int()
if ix < 0 {
return fmt.Errorf("we can't deal with negatives")
}
result = fmt.Sprintf("the input is: %d", ix)
case <-obj.closeChan:
return nil
}
select {
case obj.init.Output <- &types.StrValue{
V: result,
}:
case <-obj.closeChan:
return nil
}
}
}
```
As you can see, we read our inputs from the `input` channel, and write to the
`output` channel. Our code is careful to never block or deadlock, and can always
exit if a close signal is requested. It also cleans up after itself by closing
the `output` channel when it is done using it. This is done easily with `defer`.
If it notices that the `input` channel closes, then it knows that no more input
values are coming and it can consider shutting down early.
## Further considerations
There is some additional information that any function author will need to know.
Each issue is listed separately below!
### Function struct
Each function will implement methods as pointer receivers on a function struct.
The naming convention for resources is that they end with a `Func` suffix.
#### Example
```golang
type FooFunc struct {
init *interfaces.Init
// this space can be used if needed
closeChan chan struct{} // shutdown signal
}
```
### Function registration
All functions 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
funcs.Register("foo", func() interfaces.Func { return &FooFunc{} })
}
```
### Composite functions
Composite functions are functions which import one or more existing functions.
This is useful to prevent code duplication in higher level function scenarios.
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 function that uses this feature, or to add it to an existing one!
We don't expect this functionality to be particularly useful or common, as it's
probably easier and preferable to simply import common golang library code into
multiple different functions instead.
## Polymorphic Function API
The polymorphic function API is an API that lets you implement functions which
do not necessarily have a single static function signature. After compile time,
all functions must have a static function signature. We also know that there
might be different ways you would want to call `printf`, such as:
`printf("the %s is %d", "answer", 42)` or `printf("3 * 2 = %d", 3 * 2)`. Since
you couldn't implement the infinite number of possible signatures, this API lets
you write code which can be coerced into different forms. This makes
implementing what would appear to be generic or polymorphic, instead something
that is actually static and that still has the static type safety properties
that were guaranteed by the mgmt language.
Since this is an advanced topic, it is not described in full at this time. For
more information please have a look at the source code comments, some of the
existing implementations, and ask around in the community.
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### Can I use global variables?
Probably not. You must assume that multiple copies of your function may be used
at the same time. If they require a global variable, it's likely this won't
work. Instead it's probably better to use a struct local variable if you need to
store some state.
There might be some rare instances where a global would be acceptable, but if
you need one of these, you're probably already an internals expert. If you think
they need to lock or synchronize so as to not overwhelm an external resource,
then you have to be especially careful not to cause deadlocking the mgmt engine.
### Can I write functions in a different language?
Currently `golang` is the only supported language for built-in functions. We
might consider allowing external functions to be imported in the future. This
will likely require a language that can expose a C-like API, such as `python` or
`ruby`. Custom `golang` functions are already possible when using mgmt as a lib.
### What new functions need writing?
There are still many ideas for new functions that haven't been written yet. If
you'd like to contribute one, please contact us and tell us about your idea!
### Can I generate many different `FuncValue` implementations from one function?
Yes, you can use a function generator in `golang` to build multiple different
implementations from the same function generator. You just need to implement a
function which *returns* a `golang` type of `func([]types.Value) (types.Value, error)`
which is what `FuncValue` expects. The generator function can use any input it
wants to build the individual functions, thus helping with code re-use.
### How do I determine the signature of my simple, polymorphic function?
The determination of the input portion of the function signature can be
determined by inspecting the length of the input, and the specific type each
value has. Length is done in the standard `golang` way, and the type of each
element can be ascertained with the `Type()` method available on every value.
Knowing the output type is trickier. If it can not be inferred in some manner,
then the only way is to keep track of this yourself. You can use a function
generator to build your `FuncValue` implementations, and pass in the unique
signature to each one as you are building them. Using a generator is a common
technique which was mentioned previously.
### 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).
## Suggestions
If you have any ideas for API changes or other improvements to function writing,
please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in
order to get it right!

639
docs/language-guide.md Normal file
View File

@@ -0,0 +1,639 @@
# Language guide
## Overview
The `mgmt` tool has various frontends, each of which may produce a stream of
between zero or more graphs that are passed to the engine for desired state
application. In almost all scenarios, you're going to want to use the language
frontend. This guide describes some of the internals of the language.
## Theory
The mgmt language is a declarative (immutable) functional, reactive programming
language. It is implemented in `golang`. A longer introduction to the language
is [available as a blog post here](https://purpleidea.com/blog/2018/02/05/mgmt-configuration-language/)!
### Types
All expressions must have a type. A composite type such as a list of strings
(`[]str`) is different from a list of integers (`[]int`).
There _is_ a _variant_ type in the language's type system, but it is only used
internally and only appears briefly when needed for type unification hints
during static polymorphic function generation. This is an advanced topic which
is not required for normal usage of the software.
The implementation of the internal types can be found in
[lang/types/](https://github.com/purpleidea/mgmt/tree/master/lang/types/).
#### bool
A `true` or `false` value.
#### str
Any `"string!"` enclosed in quotes.
#### int
A number like `42` or `-13`. Integers are represented internally as golang's
`int64`.
#### float
A floating point number like: `3.1415926`. Float's are represented internally as
golang's `float64`.
#### list
An ordered collection of values of the same type, eg: `[6, 7, 8, 9,]`. It is
worth mentioning that empty lists have a type, although without type hints it
can be impossible to infer the item's type.
#### map
An unordered set of unique keys of the same type and corresponding value pairs
of another type, eg:
`{"boiling" => 100, "freezing" => 0, "room" => "25", "house" => 22, "canada" => -30,}`.
That is to say, all of the keys must have the same type, and all of the values
must have the same type. You can use any type for either, although it is
probably advisable to avoid using very complex types as map keys.
#### struct
An ordered set of field names and corresponding values, each of their own type,
eg: `struct{answer => "42", james => "awesome", is_mgmt_awesome => true,}`.
These are useful for combining more than one type into the same value. Note the
syntactical difference between these and map's: the key's in map's have types,
and as a result, string keys are enclosed in quotes, whereas struct _fields_ are
not string values, and as such are bare and specified without quotes.
#### func
An ordered set of optionally named, differently typed input arguments, and a
return type, eg: `func(s str) int` or:
`func(bool, []str, {str: float}) struct{foo str; bar int}`.
### Expressions
Expressions, and the `Expr` interface need to be better documented. For now
please consume
[lang/interfaces/ast.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/ast.go).
These docs will be expanded on when things are more certain to be stable.
### Statements
There are a very small number of statements in our language. They include:
- **bind**: bind's an expression to a variable within that scope
- eg: `$x = 42`
- **if**: produces up to one branch of statements based on a conditional
expression
```mcl
if <conditional> {
<statements>
} else {
# the else branch is optional for if statements
<statements>
}
```
- **resource**: produces a resource
```mcl
file "/tmp/hello" {
content => "world",
mode => "o=rwx",
}
```
- **edge**: produces an edge
```mcl
File["/tmp/hello"] -> Print["alert4"]
```
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
of executing our programs is to produce a list of `resources`, which along with
the produced `edges`, is built into a resource graph. This graph is then passed
to the engine for desired state application.
#### Bind
This section needs better documentation.
#### If
This section needs better documentation.
#### Resource
Resources express the idempotent workloads that we want to have apply on our
system. They correspond to vertices in a [graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph)
which represent the order in which their declared state is applied. You will
usually want to pass in a number of parameters and associated values to the
resource to control how it behaves. For example, setting the `content` parameter
of a `file` resource to the string `hello`, will cause the contents of that file
to contain the string `hello` after it has run.
For some parameters, there is a distinction between an unspecified parameter,
and a parameter with a `zero` value. For example, for the file resource, you
might choose to set the `content` parameter to be the empty string, which would
ensure that the file has a length of zero. Alternatively you might wish to not
specify the file contents at all, which would leave that property undefined. If
you omit listing a property, then it will be undefined. To control this property
programmatically, you need to specify an `is-defined` value, as well as the
value to use if that boolean is true. You can do this with the resource-specific
`elvis` operator.
```mcl
$b = true # change me to false and then try editing the file manually
file "/tmp/mgmt-elvis" {
content => $b ?: "hello world\n",
state => "exists",
}
```
This example is static, however you can imagine that the `$b` value might be
chosen in a programmatic way, even one in which that value varies over time. If
it evaluates to `true`, then the parameter will be used. If no `elvis` operator
is specified, then the parameter value will also be used. If the parameter is
not specified, then it will obviously not be used.
Resources may also declare edges internally. The edges may point to or from
another resource, and may optionally include a notification. The four properties
are: `Before`, `Depend`, `Notify` and `Listen`. The first two represent normal
edge dependencies, and the second two are normal edge dependencies that also
send notifications. You may have multiples of these per resource, including
multiple `Depend` lines if necessary. Each of these properties also supports the
conditional inclusion `elvis` operator as well.
For example, you may write is:
```mcl
$b = true # for example purposes
if $b {
pkg "drbd" {
state => "installed",
# multiple properties may be used in the same resource
Before => File["/etc/drbd.conf"],
Before => Svc["drbd"],
}
}
file "/etc/drbd.conf" {
content => "some config",
Depend => $b ?: Pkg["drbd"],
Notify => Svc["drbd"],
}
svc "drbd" {
state => "running",
}
```
There are two unique properties about these edges that is different from what
you might expect from other automation software:
1. The ability to specify multiples of these properties allows you to avoid
having to manage arrays and conditional trees of these different dependencies.
2. The keywords all have the same length, which means your code lines up nicely.
#### Edge
Edges express dependencies in the graph of resources which are output. They can
be chained as a pair, or in any greater number. For example, you may write:
```mcl
Pkg["drbd"] -> File["/etc/drbd.conf"] -> Svc["drbd"]
```
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.
### Stages
The mgmt compiler runs in a number of stages. In order of execution they are:
* [Lexing](#lexing)
* [Parsing](#parsing)
* [Interpolation](#interpolation)
* [Scope propagation](#scope-propagation)
* [Type unification](#type-unification)
* [Function graph generation](#function-graph-generation)
* [Function engine creation and validation](#function-engine-creation-and-validation)
All of the above needs to be done every time the source code changes. After this
point, the [function engine runs](#function-engine-running-and-interpret) and
produces events. On every event, we "[interpret](#function-engine-running-and-interpret)"
which produces a resource graph. This series of resource graphs are passed
to the engine as they are produced.
What follows are some notes about each step.
#### Lexing
Lexing is done using [nex](https://github.com/blynn/nex). It is a pure-golang
implementation which is similar to _Lex_ or _Flex_, but which produces golang
code instead of C. It integrates reasonably well with golang's _yacc_ which is
used for parsing. The token definitions are in:
[lang/lexer.nex](https://github.com/purpleidea/mgmt/tree/master/lang/lexer.nex).
Lexing and parsing run together by calling the `LexParse` method.
#### Parsing
The parser used is golang's implementation of
[yacc](https://godoc.org/golang.org/x/tools/cmd/goyacc). The documentation is
quite abysmal, so it's helpful to rely on the documentation from standard yacc
and trial and error. One small advantage yacc has over standard yacc is that it
can produce error messages from examples. The best documentation is to examine
the source. There is a short write up available [here](https://research.swtch.com/yyerror).
The yacc file exists at:
[lang/parser.y](https://github.com/purpleidea/mgmt/tree/master/lang/parser.y).
Lexing and parsing run together by calling the `LexParse` method.
#### Interpolation
Interpolation is used to transform the AST (which was produced from lexing and
parsing) into one which is either identical or different. It expands strings
which might contain expressions to be interpolated (eg: `"the answer is: ${foo}"`)
and can be used for other scenarios in which one statement or expression would
be better represented by a larger AST. Most nodes in the AST simply return their
own node address, and do not modify the AST.
#### Scope propagation
Scope propagation passes the parent scope (starting with the top-level, built-in
scope) down through the AST. This is necessary so that children nodes can access
variables in the scope if needed. Most AST node's simply pass on the scope
without making any changes. The `ExprVar` node naturally consumes scope's and
the `StmtProg` node cleverly passes the scope through in the order expected for
the out-of-order bind logic to work.
#### Type unification
Each expression must have a known type. The unpleasant option is to force the
programmer to specify by annotation every type throughout their whole program
so that each `Expr` node in the AST knows what to expect. Type annotation is
allowed in situations when you want to explicitly specify a type, or when the
compiler cannot deduce it, however, most of it can usually be inferred.
For type inferrence to work, each node in the AST implements a `Unify` method
which is able to return a list of invariants that must hold true. This starts at
the top most AST node, and gets called through to it's children to assemble a
giant list of invariants. The invariants can take different forms. They can
specify that a particular expression must have a particular type, or they can
specify that two expressions must have the same types. More complex invariants
allow you to specify relationships between different types and expressions.
Furthermore, invariants can allow you to specify that only one invariant out of
a set must hold true.
Once the list of invariants has been collected, they are run through an
invariant solver. The solver can return either return successfully or with an
error. If the solver returns successfully, it means that it has found a trivial
mapping between every expression and it's corresponding type. At this point it
is a simple task to run `SetType` on every expression so that the types are
known. If the solver returns in error, it is usually due to one of two
possibilities:
1. Ambiguity
The solver does not have enough information to make a definitive or
unique determination about the expression to type mappings. The set of
invariants is ambiguous, and we cannot continue. An error will be
returned to the programmer. In this scenario the user will probably need
to add a type annotation, possibly because of a design bug in the user's
program.
2. Conflict
The solver has conflicting information that cannot be reconciled. In
this situation an explicit conflict has been found. If two invariants
are found which both expect a particular expression to have different
types, then it is not possible to find a valid solution. This almost
always happens if the user has made a type error in their program.
Only one solver currently exists, but it is possible to easily plug in an
alternate implementation if someone more skilled in the art of solver design
would like to propose a more logical or performant variant.
#### Function graph generation
At this point we have a fully type AST. The AST must now be transformed into a
directed, acyclic graph (DAG) data structure that represents the flow of data as
necessary for everything to be reactive. Note that this graph is *different*
from the resource graph which is produced and sent to the engine. It is just a
coincidence that both happen to be DAG's. (You don't freak out when you see a
list data structure show up in more than one place, do you?)
To produce this graph, each node has a `Graph` method which it can call. This
starts at the top most node, and is called down through the AST. The edges in
the graphs must represent the individual expression values which are passed
from node to node. The names of the edges must match the function type argument
names which are used in the definition of the corresponding function. These
corresponding functions must exist for each expression node and are produced by
calling that expression's `Func` method. These are usually called by the
function engine during function creation and validation.
#### Function engine creation and validation
Finally we have a graph of the data flows. The function engine must first
initialize which creates references to each of the necessary function
implementations, and gets information about each one. It then needs to be type
checked to ensure that the data flows all correctly match what is expected. If
you were to pass an `int` to a function expecting a `bool`, this would be a
problem. If all goes well, the program should get run shortly.
#### Function engine running and interpret
At this point the function engine runs. It produces a stream of events which
cause the `Output()` method of the top-level program to run, which produces the
list of resources and edges. These are then transformed into the resource graph
which is passed to the engine.
### Function API
If you'd like to create a built-in, core function, you'll need to implement the
function API interface named `Func`. It can be found in
[lang/interfaces/func.go](https://github.com/purpleidea/mgmt/tree/master/lang/interfaces/func.go).
Your function must have a specific type. For example, a simple math function
might have a signature of `func(x int, y int) int`. As you can see, all the
types are known _before_ compile time.
A separate discussion on this matter can be found in the [function guide](function-guide.md).
What follows are each of the method signatures and a description of each.
Failure to implement the API correctly can cause the function graph engine to
block, or the program to panic.
### Info
```golang
Info() *Info
```
The Info method must return a struct containing some information about your
function. The struct has the following type:
```golang
type Info struct {
Sig *types.Type // the signature of the function, must be KindFunc
}
```
You must implement this correctly. Other fields in the `Info` struct may be
added in the future. This method is usually called before any other, and should
not depend on any other method being called first. Other methods must not depend
on this method being called first.
#### Example
```golang
func (obj *FooFunc) Info() *interfaces.Info {
return &interfaces.Info{
Sig: types.NewType("func(a str, b int) float"),
}
}
```
### Init
```golang
Init(*Init) error
```
Init is called by the function graph engine to create an implementation of this
function. It is passed in a struct of the following form:
```golang
type Init struct {
Hostname string // uuid for the host
Input chan types.Value // Engine will close `input` chan
Output chan types.Value // Stream must close `output` chan
World resources.World
Debug bool
Logf func(format string, v ...interface{})
}
```
These values and references may be used (wisely) inside your function. `Input`
will contain a channel of input structs matching the expected input signature
for your function. `Output` will be the channel which you must send values to
whenever a new value should be produced. This must be done in the `Stream()`
function. You may carefully use `World` to access functionality provided by the
engine. You may use `Logf` to log informational messages, however there is no
guarantee that they will be displayed to the user. `Debug` specifies whether the
function is running in a user-requested debug mode. This might cause you to want
to print more log messages for example. You will need to save references to any
or all of these info fields that you wish to use in the struct implementing this
`Func` interface. At a minimum you will need to save `Output` as a minimum of
one value must be produced.
#### Example
```golang
Please see the example functions in
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
```
### Stream
```golang
Stream() error
```
Stream is called by the function engine when it is ready for your function to
start accepting input and producing output. You must always produce at least one
value. Failure to produce at least one value will probably cause the function
engine to hang waiting for your output. This function must close the `Output`
channel when it has no more values to send. The engine will close the `Input`
channel when it has no more values to send. This may or may not influence
whether or not you close the `Output` channel.
#### Example
```golang
Please see the example functions in
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
```
### Close
```golang
Close() error
```
Close asks the particular function to shutdown its `Stream()` function and
return.
#### Example
```golang
Please see the example functions in
[lang/funcs/core/](https://github.com/purpleidea/mgmt/tree/master/lang/funcs/core/).
```
### Polymorphic Function API
For some functions, it might be helpful to be able to implement a function once,
but to have multiple polymorphic variants that can be chosen at compile time.
For this more advanced topic, you will need to use the
[Polymorphic Function API](#polymorphic-function-api). This will help with code
reuse when you have a small, finite number of possible type signatures, and also
for more complicated cases where you might have an infinite number of possible
type signatures. (eg: `[]str`, or `[][]str`, or `[][][]str`, etc...)
Suppose you want to implement a function which can assume different type
signatures. The mgmt language does not support polymorphic types-- you must use
static types throughout the language, however, it is legal to implement a
function which can take different specific type signatures based on how it is
used. For example, you might wish to add a math function which could take the
form of `func(x int, x int) int` or `func(x float, x float) float` depending on
the input values. You might also want to implement a function which takes an
arbitrary number of input arguments (the number must be statically fixed at the
compile time of your program though) and which returns a string.
The `PolyFunc` interface adds additional methods which you must implement to
satisfy such a function implementation. If you'd like to implement such a
function, then please notify the project authors, and they will expand this
section with a longer description of the process.
#### Examples
What follows are a few examples that might help you understand some of the
language details.
##### Example Foo
TODO: please add an example here!
##### Example Bar
TODO: please add an example here!
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### What is the difference between `ExprIf` and `StmtIf`?
The language contains both an `if` expression, and and `if` statement. An `if`
expression takes a boolean conditional *and* it must contain exactly _two_
branches (a `then` and an `else` branch) which each contain one expression. The
`if` expression _will_ return the value of one of the two branches based on the
conditional.
#### Example:
```mcl
# this is an if expression, and both branches must exist
$b = true
$x = if $b {
42
} else {
-13
}
```
The `if` statement also takes a boolean conditional, but it may have either one
or two branches. Branches must only directly contain statements. The `if`
statement does not return any value, but it does produce output when it is
evaluated. The output consists primarily of resources (vertices) and edges.
#### Example:
```mcl
# this is an if statement, and in this scenario the else branch was omitted
$b = true
if $b {
file "/tmp/hello" {
content => "world",
}
}
```
### What is the difference `types.Value.Str()` and `types.Value.String()`?
In the `lang/types` library, there is a `types.Value` interface. Every value in
our type system must implement this interface. One of the methods in this
interface is the `String() string` method. This lets you print a representation
of the value. You will probably never need to use this method.
In addition, the `types.Value` interface implements a number of helper functions
which return the value as an equivalent golang type. If you know that the value
is a `bool`, you can call `x.Bool()` on it. If it's a `string` you can call
`x.Str()`. Make sure not to call one of those type methods unless you know the
value is of that type, or you will trigger a panic!
### I created a `&ListValue{}` but it's not working!
If you create a base type like `bool`, `str`, `int`, or `float`, all you need to
do is build the `&BoolValue` and set the `V` field. Eg:
```golang
someBool := &types.BoolValue{V: true}
```
If you are building a container type like `list`, `map`, `struct`, or `func`,
then you *also* need to specify the type of the contained values. This is
because a list has a type of `[]str`, or `[]int`, or even `[][]foo`. Eg:
```golang
someListOfStrings := &types.ListValue{
T: types.NewType("[]str"), # must match the contents!
V: []types.Value{
&types.StrValue{V: "a"},
&types.StrValue{V: "bb"},
&types.StrValue{V: "ccc"},
},
}
```
If you don't build these properly, then you will cause a panic! Even empty lists
have a type.
### 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
of graphs to the engine "backend". While it _is_ the recommended way of using
mgmt, you're welcome to either use an alternate frontend, or write your own. To
write your own frontend, you must implement the
[GAPI](https://github.com/purpleidea/mgmt/blob/master/gapi/gapi.go) interface.
### I'm an expert in FRP, and you got it all wrong; even the names of things!
I am certainly no expert in FRP, and I've certainly got lots more to learn. One
thing FRP experts might notice is that some of the concepts from FRP are either
named differently, or are notably absent.
In mgmt, we don't talk about behaviours, events, or signals in the strict FRP
definitons of the words. Firstly, because we only support discretized, streams
of values with no plan to add continuous semantics. Secondly, because we prefer
to use terms which are more natural and relatable to what our target audience is
expecting. Our users are more likely to have a background in Physiology, or
systems administration than a background in FRP.
Having said that, we hope that the FRP community will engage with us and help
improve the parts that we got wrong. Even if that means adding continuous
behaviours!
### This is brilliant, may I give you a high-five?
Thank you, and yes, probably. "Props" may also be accepted, although patches are
preferred. If you can't do either, [donations](https://purpleidea.com/misc/donate/)
to support the project are welcome too!
### 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).
## Suggestions
If you have any ideas for changes or other improvements to the language, please
let us know! We're still pre 1.0 and pre 0.1 and happy to change it in order to
get it right!

45
docs/on-the-web.md Normal file
View File

@@ -0,0 +1,45 @@
# On the web
Here is a list of places mgmt has appeared on the web. Feel free to send a patch
if we missed something that you think is relevant!
## Links
| Author | Format | Subject |
|---|---|---|
| James Shubin | blog | [Next generation configuration mgmt](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/) |
| James Shubin | video | [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1) |
| James Shubin | video | [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1) |
| Julian Dunn | video | [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1) |
| Walter Heck | slides | [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3) |
| Marco Marongiu | blog | [On mgmt](http://syslog.me/2016/02/15/leap-or-die/) |
| Felix Frank | blog | [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/) |
| James Shubin | blog | [Automatic edges in mgmt (...and the pkg resource)](https://purpleidea.com/blog/2016/03/14/automatic-edges-in-mgmt/) |
| James Shubin | blog | [Automatic grouping in mgmt](https://purpleidea.com/blog/2016/03/30/automatic-grouping-in-mgmt/) |
| John Arundel | tweet | [“Puppets days are numbered.”](https://twitter.com/bitfield/status/732157519142002688) |
| Felix Frank | blog | [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/) |
| Felix Frank | blog | [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/) |
| James Shubin | blog | [Automatic clustering in mgmt](https://purpleidea.com/blog/2016/06/20/automatic-clustering-in-mgmt/) |
| James Shubin | video | [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1) |
| James Shubin | video | [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf)) |
| Felix Frank | blog | [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/) |
| Felix Frank | blog | [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/) |
| James Shubin | video | [Recording from systemd.conf 2016](https://www.youtube.com/watch?v=jB992Zb3nH0&html5=1) |
| James Shubin | blog | [Remote execution in mgmt](https://purpleidea.com/blog/2016/10/07/remote-execution-in-mgmt/) |
| James Shubin | video | [Recording from High Load Strategy 2016](https://vimeo.com/191493409) |
| James Shubin | video | [Recording from NLUUG 2016](https://www.youtube.com/watch?v=MmpwOQAb_SE&html5=1) |
| James Shubin | blog | [Send/Recv in mgmt](https://purpleidea.com/blog/2016/12/07/sendrecv-in-mgmt/) |
| Julien Pivotto | blog | [Augeas resource for mgmt](https://roidelapluie.be/blog/2017/02/14/mgmt-augeas/) |
| James Shubin | blog | [Metaparameters in mgmt](https://purpleidea.com/blog/2017/03/01/metaparameters-in-mgmt/) |
| 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/) |
| 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) |
| James Shubin | video | [Recording from LCA 2018, Main Conference](https://www.youtube.com/watch?v=_9PG64AOQ3w) |
| James Shubin | video | [Recording from DevConf.cz 2017](https://www.youtube.com/watch?v=-FPEK08l1Zk) |
| 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) |

View File

@@ -30,8 +30,9 @@ Here is a list of the metrics we provide:
- `mgmt_resources_total`: The number of resources that mgmt is managing
- `mgmt_checkapply_total`: The number of CheckApply's that mgmt has run
- `mgmt_failures_total`: The number of resources that have failed
- `mgmt_failures_current`: The number of resources that have failed
- `mgmt_graph_start_time_seconds`: Start time of the current graph since unix epoch in seconds
- `mgmt_failures`: The number of resources that have failed
- `mgmt_graph_start_time_seconds`: Start time of the current graph since unix
epoch in seconds
For each metric, you will get some extra labels:
@@ -57,10 +58,9 @@ We do not have grafana dashboards yet. Patches welcome!
- [prometheus website](https://prometheus.io/)
- [prometheus documentation](https://prometheus.io/docs/introduction/overview/)
- [prometheus best practices regarding metrics
naming](https://prometheus.io/docs/practices/naming/)
- [prometheus best practices regarding metrics naming](https://prometheus.io/docs/practices/naming/)
- [grafana website](http://grafana.org/)
[pgc]: https://github.com/prometheus/client_golang/blob/master/prometheus/go_collector.go
[etcdm]: https://coreos.com/etcd/docs/latest/metrics.html
[pd]: https://github.com/prometheus/prometheus/wiki/Default-port-allocation
[pd]: https://github.com/prometheus/prometheus/wiki/Default-port-allocations

View File

@@ -109,8 +109,8 @@ file { "/tmp/mgmt-test":
To avoid this, specify the parameter explicitly:
```
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
```bash
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
```
This is tedious in a more complex manifest. A good simplification is the

View File

@@ -1,94 +1,182 @@
# Quick start guide
## Introduction
This guide is intended for developers. Once `mgmt` is minimally viable, we'll
publish a quick start guide for users too. In the meantime, please contribute!
If you're brand new to `mgmt`, it's probably a good idea to start by reading the
[introductory article](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
or to watch an [introductory video](https://github.com/purpleidea/mgmt/#on-the-web).
publish a quick start guide for users too. If you're brand new to `mgmt`, it's
probably a good idea to start by reading the
[introductory article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
or to watch an [introductory video](https://www.youtube.com/watch?v=LkEtBVLfygE&html5=1).
Once you're familiar with the general idea, please start hacking...
## Vagrant
If you would like to avoid doing the following steps manually, we have prepared
a [Vagrant](https://www.vagrantup.com/) environment for your convenience. From
the project directory, run a `vagrant up`, and then a `vagrant status`. From
there, you can `vagrant ssh` into the `mgmt` machine. The MOTD will explain the
rest.
## Dependencies
Software projects have a few different kinds of dependencies. There are _build_
dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
required for running the _test_ suite.
### Build
* `golang` 1.8 or higher (required, available in some distros and distributed
as a binary officially by [golang.org](https://golang.org/dl/))
* golang libraries (required, available with `go get ./...`) a partial list includes:
```
github.com/coreos/etcd/client
gopkg.in/yaml.v2
gopkg.in/fsnotify.v1
github.com/urfave/cli
github.com/coreos/go-systemd/dbus
github.com/coreos/go-systemd/util
github.com/libvirt/libvirt-go
```
* `stringer` (optional), available as a package on some platforms, otherwise via `go get`
```
golang.org/x/tools/cmd/stringer
```
* `pandoc` (optional), for building a pdf of the documentation
### Runtime
A relatively modern GNU/Linux system should be able to run `mgmt` without any
problems. Since `mgmt` runs as a single statically compiled binary, all of the
library dependencies are included. It is expected, that certain advanced
resources require host specific facilities to work. These requirements are
listed below:
| Resource | Dependency | Version |
|----------|-------------------|---------|
| file | inotify | ? |
| hostname | systemd-hostnamed | ? |
| nspawn | systemd-nspawn | ? |
| pkg | packagekitd | ? |
| svc | systemd | ? |
| virt | libvirtd | ? |
For building a visual representation of the graph, `graphviz` is required.
### Testing
* golint `github.com/golang/lint/golint`
## Quick start
* Make sure you have golang version 1.8 or greater installed.
### Installing golang
* You need golang version 1.9 or greater installed.
* To install on rpm style systems: `sudo dnf install golang`
* To install on apt style systems: `sudo apt install golang`
* To install on macOS systems install [Homebrew](https://brew.sh)
and run: `brew install go`
* You can run `go version` to check the golang version.
* If your distro is tool old, you may need to [download](https://golang.org/dl/)
a newer golang version.
### Setting up golang
* If you do not have a GOPATH yet, create one and export it:
```
mkdir $HOME/gopath
export GOPATH=$HOME/gopath
```
* You might also want to add the GOPATH to your `~/.bashrc` or `~/.profile`.
* For more information you can read the [GOPATH documentation](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable).
* Next download the mgmt code base, and switch to that directory:
### Getting the mgmt code and dependencies
* Download the `mgmt` code into the GOPATH, and switch to that directory:
```
mkdir -p $GOPATH/src/github.com/purpleidea/
cd $GOPATH/src/github.com/purpleidea/
git clone --recursive https://github.com/purpleidea/mgmt/
cd $GOPATH/src/github.com/purpleidea/mgmt
```
* Run `make deps` to install system and golang dependencies. Take a look at `misc/make-deps.sh` for details.
* Add $GOPATH/bin to $PATH
```
export PATH=$PATH:$GOPATH/bin
```
* Run `make deps` to install system and golang dependencies. Take a look at
`misc/make-deps.sh` for details.
* Run `make build` to get a freshly built `mgmt` binary.
* Run `time ./mgmt run --yaml examples/graph0.yaml --converged-timeout=5 --tmp-prefix` to try out a very simple example!
* To run continuously in the default mode of operation, omit the `--converged-timeout` option.
* Have fun hacking on our future technology!
### Running mgmt
* Run `time ./mgmt run --lang examples/lang/hello0.mcl --tmp-prefix` to try out
a very simple example!
* Look in that example file that you ran to see if you can figure out what it
did!
* Have fun hacking on our future technology and get involved to shape the
project!
## Examples
Please look in the [examples/](../examples/) folder for some examples!
## Installation
Please look in the [examples/lang/](../examples/lang/) folder for some more
examples!
## Vagrant
If you would like to avoid doing the above steps manually, we have prepared a
[Vagrant](https://www.vagrantup.com/) environment for your convenience. From the
project directory, run a `vagrant up`, and then a `vagrant status`. From there,
you can `vagrant ssh` into the `mgmt` machine. The MOTD will explain the rest.
## Using Docker
Alternatively, you can check out the [docker-guide](docs/docker-guide.md) in
order to develop or deploy using docker.
## Information about dependencies
Software projects have a few different kinds of dependencies. There are _build_
dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
required for running the _test_ suite.
### Build
* `golang` 1.9 or higher (required, available in some distros and distributed
as a binary officially by [golang.org](https://golang.org/dl/))
### Runtime
A relatively modern GNU/Linux system should be able to run `mgmt` without any
problems. Since `mgmt` runs as a single statically compiled binary, all of the
library dependencies are included. It is expected, that certain advanced
resources require host specific facilities to work. These requirements are
listed below:
| Resource | Dependency | Version | Check version with |
|----------|-------------------|-----------------------------|-----------------------------------------------------------|
| augeas | augeas-devel | `augeas 1.6` or greater | `dnf info augeas-devel` or `apt-cache show libaugeas-dev` |
| file | inotify | `Linux 2.6.27` or greater | `uname -a` |
| hostname | systemd-hostnamed | `systemd 25` or greater | `systemctl --version` |
| nspawn | systemd-nspawn | `systemd ???` or greater | `systemctl --version` |
| pkg | packagekitd | `packagekit 1.x` or greater | `pkcon --version` |
| svc | systemd | `systemd ???` or greater | `systemctl --version` |
| virt | libvirt-devel | `libvirt 1.2.0` or greater | `dnf info libvirt-devel` or `apt-cache show libvirt-dev` |
| virt | libvirtd | `libvirt 1.2.0` or greater | `libvirtd --version` |
For building a visual representation of the graph, `graphviz` is required.
To build `mgmt` without augeas support please run:
`GOTAGS='noaugeas' make build`
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`
## Binary Package Installation
Installation of `mgmt` from distribution packages currently needs improvement.
They are not always up-to-date with git master and as such are not recommended.
At the moment we have:
* [COPR](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
* [Arch](https://aur.archlinux.org/packages/mgmt/)
Please contribute more! We'd especially like to see a Debian package!
## OSX/macOS/Darwin development
Developing and running `mgmt` on macOS is currently not supported (but not
discouraged either). Meaning it might work but in the case it doesn't you would
have to provide your own patches to fix problems (the project maintainer and
community are glad to assist where needed).
There are currently some issues that make `mgmt` less suitable to run for provisioning
macOS. But as a client to provision remote servers it should run fine.
Since the primary supported systems are Linux and these are the environments
tested for it is wise to run these suites during macOS development as well. To
ease this Docker can be leveraged ([Docker for Mac](https://docs.docker.com/docker-for-mac/)).
Before running any of the commands below create the development Docker image:
```
docker/scripts/build-development
```
This image requires updating every time dependencies (`make-deps.sh`) change.
Then to run the test suite:
```
docker run --rm -ti \
-v $PWD:/go/src/github.com/purpleidea/mgmt/ \
-w /go/src/github.com/purpleidea/mgmt/ \
purpleidea/mgmt:development \
make test
```
For convenience this command is wrapped in `docker/scripts/exec-development`.
Basically any command can be executed this way. Because the repository source is
mounted into the Docker container invocation will be quick and allow rapid
testing, example:
```
docker/scripts/exec-development test/test-shell.sh load0.sh
```
Other examples:
```
docker/scripts/exec-development make build
docker/scripts/exec-development ./mgmt run --tmp-prefix --lang examples/lang/load0.mcl
```

View File

@@ -16,7 +16,7 @@ Resources in `mgmt` are similar to resources in other systems in that they are
uniquely different in that they can detect when their state has changed, and as
a result can run to revert or repair this change instantly. For some background
on this design, please read the
[original article](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
[original article](https://purpleidea.com/blog/2016/01/18/next-generation-configuration-mgmt/)
on the subject.
## Resource API
@@ -27,6 +27,7 @@ interface. What follows are each of the method signatures and a description of
each.
### Default
```golang
Default() Res
```
@@ -36,6 +37,7 @@ values which already have the correct default as the golang zero value. In
general it is preferable if the zero values make for the correct defaults.
#### Example
```golang
// Default returns some sensible defaults for this resource.
func (obj *FooRes) Default() Res {
@@ -46,6 +48,7 @@ func (obj *FooRes) Default() Res {
```
### Validate
```golang
Validate() error
```
@@ -57,6 +60,7 @@ 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`.
#### Example
```golang
// Validate reports any problems with the struct definition.
func (obj *FooRes) Validate() error {
@@ -68,6 +72,7 @@ func (obj *FooRes) Validate() error {
```
### Init
```golang
Init() error
```
@@ -77,6 +82,7 @@ return an error. It should do any resource specific work, and finish by calling
the `Init` method of the base resource.
#### Example
```golang
// Init initializes the Foo resource.
func (obj *FooRes) Init() error {
@@ -95,6 +101,7 @@ shouldn't allow `Init` to dangerously `rm -rf /$the_world` if your code only
checks `$the_world` in `Validate`. Remember to always program safely!
### Close
```golang
Close() error
```
@@ -104,6 +111,7 @@ 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.
#### Example
```golang
// Close runs some cleanup code for this resource.
func (obj *FooRes) Close() error {
@@ -125,6 +133,7 @@ method! If you plan to return early if you hit an internal error, then at least
call it with a defer!
### CheckApply
```golang
CheckApply(apply bool) (checkOK bool, err error)
```
@@ -150,6 +159,7 @@ facility will detect the change, ultimately resulting in a subsequent call to
`CheckApply`.
#### Example
```golang
// CheckApply does the idempotent work of checking and applying resource state.
func (obj *FooRes) CheckApply(apply bool) (bool, error) {
@@ -171,6 +181,7 @@ 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
@@ -184,6 +195,7 @@ 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
succession. This is usually not a pathological occurrence, but rather a healthy
pattern which is a consequence of the event system. When the state of the
@@ -193,6 +205,7 @@ trigger the `Watch` code! In response, a second `CheckApply` is triggered, which
will likely find the state to now be correct.
#### Summary
* Anytime an error occurs during `CheckApply`, you should return `(false, err)`.
* 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`.
@@ -200,6 +213,7 @@ will likely find the state to now be correct.
* Returning `(true, err)` is a programming error and will cause a `Fatal`.
### Watch
```golang
Watch() error
```
@@ -229,6 +243,7 @@ executed. As a result, the resource must still work even if the main loop is not
running.
#### Select
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
@@ -237,6 +252,7 @@ events from the engine via the `<-obj.Events()` call, 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,
@@ -245,6 +261,7 @@ state as `dirty` if we believe it might have changed. We do this with the
`StateOK(false)` function.
#### Startup
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
@@ -252,6 +269,7 @@ 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.
#### 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.
@@ -270,6 +288,7 @@ prove to be useful if a resource wants to start off a long operation, but avoid
sending out erroneous `Event` messages to keep things alive until it finishes.
#### Example
```golang
// Watch is the listener and main loop for this resource.
func (obj *FooRes) Watch() error {
@@ -317,6 +336,7 @@ 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 process internal events and shutdown promptly if asked to.
@@ -324,6 +344,7 @@ func (obj *FooRes) Watch() error {
* Have a look at the existing resources for a rough idea of how this all works.
### Compare
```golang
Compare(Res) bool
```
@@ -341,6 +362,7 @@ particular if they store some generated state, or if they aren't significant in
some way.
#### Example
```golang
// Compare two resources and return if they are equivalent.
func (obj *FooRes) Compare(r Res) bool {
@@ -368,6 +390,7 @@ func (obj *FooRes) Compare(r Res) bool {
```
### UIDs
```golang
UIDs() []ResUID
```
@@ -377,6 +400,7 @@ particular resource uniquely. This is used with the AutoEdges API to determine
if another resource can match a dependency to this one.
### AutoEdges
```golang
AutoEdges() (AutoEdge, error)
```
@@ -386,6 +410,7 @@ is used to match other resources that might be relevant dependencies for this
resource.
### CollectPattern
```golang
CollectPattern() string
```
@@ -393,6 +418,7 @@ CollectPattern() string
This is currently a stub and will be updated once the DSL is further along.
### UnmarshalYAML
```golang
UnmarshalYAML(unmarshal func(interface{}) error) error // optional
```
@@ -406,6 +432,7 @@ The signature intentionally matches what is required to satisfy the `go-yaml`
[Unmarshaler](https://godoc.org/gopkg.in/yaml.v2#Unmarshaler) interface.
#### Example
```golang
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
@@ -429,10 +456,12 @@ 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
@@ -440,6 +469,7 @@ 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
@@ -453,6 +483,7 @@ type FooRes struct {
```
### 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.
@@ -465,23 +496,25 @@ func init() { // special golang method that runs once
```
## Automatic edges
Automatic edges in `mgmt` are well described in [this article](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/).
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://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/).
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://ttboj.wordpress.com/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.
Any time that one of the destination values is changed, the engine automatically
@@ -523,6 +556,7 @@ such as for cache invalidation.
Remember, `Send/Recv` only changes your resource code if you cache state.
## Composite resources
Composite resources are resources which embed one or more existing resources.
This is useful to prevent code duplication in higher level resource scenarios.
The best example of this technique can be seen in the `nspawn` resource which
@@ -532,24 +566,70 @@ 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!
## Frequently asked questions
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
respond by commit with the answer.)
### Can I write resources in a different language?
Currently `golang` is the only supported language for built-in resources. We
might consider allowing external resources to be imported in the future. This
will likely require a language that can expose a C-like API, such as `python` or
`ruby`. Custom `golang` resources are already possible when using mgmt as a lib.
Higher level resource collections will be possible once the `mgmt` DSL is ready.
### Why does the resource API have `CheckApply` instead of two separate methods?
In an early version we actually had both "parts" as separate methods, namely:
`StateOK` (Check) and `Apply`, but the [decision](58f41eddd9c06b183f889f15d7c97af81b0331cc)
was made to merge the two into a single method. There are two reasons for this:
1. Many situations would involve the engine running both `Check` and `Apply`. If
the resource needed to share some state (for efficiency purposes) between the
two calls, this is much more difficult. A common example is that a resource
might want to open a connection to `dbus` or `http` to do resource state testing
and applying. If the methods are combined, there's no need to open and close
them twice. A counter argument might be that you could open the connection in
`Init`, and close it in `Close`, however you might not want that open for the
full lifetime of the resource if you only change state occasionally.
2. Suppose you came up with a really good reason why you wanted the two methods
to be separate. It turns out that the current `CheckApply` can wrap this easily.
It would look approximately like this:
```golang
func (obj *FooRes) CheckApply(apply bool) (bool, error) {
// my private split implementation of check and apply
if c, err := obj.check(); err != nil {
return false, err // we errored
} else if c {
return true, nil // state was good!
}
if !apply {
return false, nil // state needs fixing, but apply is false
}
err := obj.apply() // errors if failure or unable to apply
return false, err // always return false, with an optional error
}
```
Feel free to use this pattern if you're convinced it's necessary. Alternatively,
if you think I got the `Res` API wrong and you have an improvement, please let
us know!
### What new resource primitives need writing?
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!
### Where can I find more information about mgmt?
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/#on-the-web).
Additional blog posts, videos and other material [is available!](https://github.com/purpleidea/mgmt/blob/master/docs/on-the-web.md).
## Suggestions
If you have any ideas for API changes or other improvements to resource writing,
please let us know! We're still pre 1.0 and pre 0.1 and happy to break API in
order to get it right!

177
docs/resources.md Normal file
View File

@@ -0,0 +1,177 @@
# Resources
Here we list all the built-in resources and their properties. The resource
primitives in `mgmt` are typically more powerful than resources in other
configuration management systems because they can be event based which lets them
respond in real-time to converge to the desired state. This property allows you
to build more complex resources that you probably hadn't considered in the past.
In addition to the resource specific properties, there are resource properties
(otherwise known as parameters) which can apply to every resource. These are
called [meta parameters](documentation.md#meta-parameters) and are listed
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)
for more up-to-date information about these resources.
* [Augeas](#Augeas): Manipulate files using augeas.
* [Exec](#Exec): Execute shell commands on the system.
* [File](#File): Manage files and directories.
* [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.
* [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.
* [Svc](#Svc): Manage system systemd services.
* [Timer](#Timer): Manage system systemd services.
* [Virt](#Virt): Manage virtual machines with libvirt.
## Augeas
The augeas resource uses [augeas](http://augeas.net/) commands to manipulate
files.
## Exec
The exec resource can execute commands on your system.
## File
The file resource manages files and directories. In `mgmt`, directories are
identified by a trailing slash in their path name. File have no such slash.
It has the following properties:
* `path`: file path (directories have a trailing slash here)
* `content`: raw file content
* `state`: either `exists` (the default value) or `absent`
* `mode`: octal unix file permissions
* `owner`: username or uid for the file owner
* `group`: group name or gid for the file group
### Path
The path property specifies the file or directory that we are managing.
### Content
The content property is a string that specifies the desired file contents.
### Source
The source property points to a source file or directory path that we wish to
copy over and use as the desired contents for our resource.
### State
The state property describes the action we'd like to apply for the resource. The
possible values are: `exists` and `absent`.
### Recurse
The recurse property limits whether file resource operations should recurse into
and monitor directory contents with a depth greater than one.
### Force
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.
## Hostname
The hostname resource manages static, transient/dynamic and pretty hostnames
on the system and watches them for changes.
### static_hostname
The static hostname is the one configured in /etc/hostname or a similar
file.
It is chosen by the local user. It is not always in sync with the current
host name as returned by the gethostname() system call.
### transient_hostname
The transient / dynamic hostname is the one configured via the kernel's
sethostbyname().
It can be different from the static hostname in case DHCP or mDNS have been
configured to change the name based on network information.
### pretty_hostname
The pretty hostname is a free-form UTF8 host name for presentation to the user.
### hostname
Hostname is the fallback value for all 3 fields above, if only `hostname` is
specified, it will set all 3 fields to this value.
## KV
The KV resource sets a key and value pair in the global world database. This is
quite useful for setting a flag after a number of resources have run. It will
ignore database updates to the value that are greater in compare order than the
requested key if the `SkipLessThan` parameter is set to true. If we receive a
refresh, then the stored value will be reset to the requested value even if the
stored value is greater.
### Key
The string key used to store the key.
### Value
The string value to set. This can also be set via Send/Recv.
### SkipLessThan
If this parameter is set to `true`, then it will ignore updating the value as
long as the database versions are greater than the requested value. The compare
operation used is based on the `SkipCmpStyle` parameter.
### SkipCmpStyle
By default this converts the string values to integers and compares them as you
would expect.
## Msg
The msg resource sends messages to the main log, or an external service such
as systemd's journal.
## Noop
The noop resource does absolutely nothing. It does have some utility in testing
`mgmt` and also as a placeholder in the resource graph.
## Nspawn
The nspawn resource is used to manage systemd-machined style containers.
## Password
The password resource can generate a random string to be used as a password. It
will re-generate the password if it receives a refresh notification.
## Pkg
The pkg resource is used to manage system packages. This resource works on many
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.
## Svc
The service resource is still very WIP. Please help us my improving it!
## Timer
This resource needs better documentation. Please help us my improving it!
## Virt
The virt resource can manage virtual machines via libvirt.

96
docs/style-guide.md Normal file
View File

@@ -0,0 +1,96 @@
# Style guide
## Overview
This document aims to be a reference for the desired style for patches to mgmt.
In particular it describes conventions which we use which are not officially
enforced by the `gofmt` tool, and which might not be clearly defined elsewhere.
Most of these are common sense to seasoned programmers, and we hope this will be
a useful reference for new programmers.
There are a lot of useful code review comments described
[here](https://github.com/golang/go/wiki/CodeReviewComments). We don't
necessarily follow everything strictly, but it is in general a very good guide.
## Basics
* All of our golang code is formatted with `gofmt`.
## Comments
All of our code is commented with the minimums required for `godoc` to function,
and so that our comments pass `golint`. Code comments should either be full
sentences (which end with a period, use proper punctuation, and capitalize the
first word when it is not a lower cased identifier), or are short one-line
comments in the source which are not full sentences and don't end with a period.
They should explain algorithms, describe non-obvious behaviour, or situations
which would otherwise need explanation or additional research during a code
review. Notes about use of unfamiliar API's is a good idea for a code comment.
### Example
Here you can see a function with the correct `godoc` string. The first word must
match the name of the function. It is _not_ capitalized because the function is
private.
```golang
// square multiplies the input integer by itself and returns this product.
func square(x int) int {
return x * x // we don't care about overflow errors
}
```
## Line length
In general we try to stick to 80 character lines when it is appropriate. It is
almost *always* appropriate for function `godoc` comments and most longer
paragraphs. Exceptions are always allowed based on the will of the maintainer.
It is usually better to exceed 80 characters than to break code unnecessarily.
If your code often exceeds 80 characters, it might be an indication that it
needs refactoring.
Occasionally inline, two line source code comments are used within a function.
These should usually be balanced so that you don't have one line with 78
characters and the second with only four. Split the comment between the two.
## Method receiver naming
[Contrary](https://github.com/golang/go/wiki/CodeReviewComments#receiver-names)
to the specialized naming of the method receiver variable, we usually name all
of these `obj` for ease of code copying throughout the project, and for faster
identification when reviewing code. Some anecdotal studies have shown that it
makes the code easier to read since you don't need to remember the name of the
method receiver variable in each different method. This is very similar to what
is done in `python`.
### Example
```golang
// Bar does a thing, and returns the number of baz results found in our
database.
func (obj *Foo) Bar(baz string) int {
if len(obj.s) > 0 {
return strings.Count(obj.s, baz)
}
return -1
}
```
## Consistent ordering
In general we try to preserve a logical ordering in source files which usually
matches the common order of execution that a _lazy evaluator_ would follow.
This is also the order which is recommended when creating interface types. When
implementing an interface, arrange your methods in the same order that they are
declared in the interface.
When implementing code for the various types in the language, please follow this
order: `bool`, `str`, `int`, `float`, `list`, `map`, `struct`, `func`.
## Suggestions
If you have any ideas for suggestions or other improvements to this guide,
please let us know!

94
etcd/client.go Normal file
View File

@@ -0,0 +1,94 @@
// 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 etcd
import (
"time"
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
errwrap "github.com/pkg/errors"
context "golang.org/x/net/context"
)
// ClientEtcd provides a simple etcd client for deploy and status operations.
type ClientEtcd struct {
Seeds []string // list of endpoints to try to connect
client *etcd.Client
}
// GetClient returns a handle to the raw etcd client object.
func (obj *ClientEtcd) GetClient() *etcd.Client {
return obj.client
}
// GetConfig returns the config struct to be used for the etcd client connect.
func (obj *ClientEtcd) GetConfig() etcd.Config {
cfg := etcd.Config{
Endpoints: obj.Seeds,
// RetryDialer chooses the next endpoint to use
// it comes with a default dialer if unspecified
DialTimeout: 5 * time.Second,
}
return cfg
}
// Connect connects the client to a server, and then builds the *API structs.
// If reconnect is true, it will force a reconnect with new config endpoints.
func (obj *ClientEtcd) Connect() error {
if obj.client != nil { // memoize
return nil
}
var err error
cfg := obj.GetConfig()
obj.client, err = etcd.New(cfg) // connect!
if err != nil {
return errwrap.Wrapf(err, "client connect error")
}
return nil
}
// Destroy cleans up the entire etcd client connection.
func (obj *ClientEtcd) Destroy() error {
err := obj.client.Close()
//obj.wg.Wait()
return err
}
// Get runs a get on the client connection. This has the same signature as our
// EmbdEtcd Get function.
func (obj *ClientEtcd) Get(path string, opts ...etcd.OpOption) (map[string]string, error) {
resp, err := obj.client.Get(context.TODO(), path, opts...)
if err != nil || resp == nil {
return nil, err
}
// TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse
result := make(map[string]string)
for _, x := range resp.Kvs {
result[string(x.Key)] = string(x.Value)
}
return result, nil
}
// Txn runs a transaction on the client connection. This has the same signature
// as our EmbdEtcd Txn function.
func (obj *ClientEtcd) Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) {
return obj.client.KV.Txn(context.TODO()).If(ifcmps...).Then(thenops...).Else(elseops...).Commit()
}

171
etcd/deploy.go Normal file
View File

@@ -0,0 +1,171 @@
// 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 etcd
import (
"fmt"
"strconv"
"strings"
etcd "github.com/coreos/etcd/clientv3"
errwrap "github.com/pkg/errors"
)
const (
deployPath = "deploy"
payloadPath = "payload"
hashPath = "hash"
)
// WatchDeploy returns a channel which spits out events on new deploy activity.
// FIXME: It should close the channel when it's done, and spit out errors when
// something goes wrong.
func WatchDeploy(obj *EmbdEtcd) chan error {
// key structure is $NS/deploy/$id/payload = $data
path := fmt.Sprintf("%s/%s/", NS, deployPath)
ch := make(chan error, 1)
// FIXME: fix our API so that we get a close event on shutdown.
callback := func(re *RE) error {
// TODO: is this even needed? it used to happen on conn errors
//log.Printf("Etcd: Watch: Path: %v", path) // event
if re == nil || re.response.Canceled {
return fmt.Errorf("watch is empty") // will cause a CtxError+retry
}
if len(ch) == 0 { // send event only if one isn't pending
ch <- nil // event
}
return nil
}
_, _ = obj.AddWatcher(path, callback, true, false, etcd.WithPrefix()) // no need to check errors
return ch
}
// GetDeploys gets all the available deploys.
func GetDeploys(obj Client) (map[uint64]string, error) {
// key structure is $NS/deploy/$id/payload = $data
path := fmt.Sprintf("%s/%s/", NS, deployPath)
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
if err != nil {
return nil, errwrap.Wrapf(err, "could not get deploy")
}
result := make(map[uint64]string)
for key, val := range keyMap {
if !strings.HasPrefix(key, path) { // sanity check
continue
}
str := strings.Split(key[len(path):], "/")
if len(str) != 2 {
return nil, fmt.Errorf("unexpected chunk count of %d", len(str))
}
if s := str[1]; s != payloadPath {
continue // skip, maybe there are other future additions
}
var id uint64
var err error
x := str[0]
if id, err = strconv.ParseUint(x, 10, 64); err != nil {
return nil, fmt.Errorf("invalid id of `%s`", x)
}
// TODO: do some sort of filtering here?
//log.Printf("Etcd: GetDeploys(%s): Id => Data: %d => %s", key, id, val)
result[id] = val
}
return result, nil
}
// GetDeploy gets the latest deploy if id == 0, otherwise it returns the deploy
// with the specified id if it exists.
// FIXME: implement this more efficiently so that it doesn't have to download *all* the old deploys from etcd!
func GetDeploy(obj Client, id uint64) (string, error) {
result, err := GetDeploys(obj)
if err != nil {
return "", err
}
if id != 0 {
str, exists := result[id]
if !exists {
return "", fmt.Errorf("can't find id `%d`", id)
}
return str, nil
}
// find the latest id
var max uint64
for i := range result {
if i > max {
max = i
}
}
if max == 0 {
return "", nil // no results yet
}
return result[max], nil
}
// AddDeploy adds a new deploy. It takes an id and ensures it's sequential. If
// hash is not empty, then it will check that the pHash matches what the
// previous hash was, and also adds this new hash along side the id. This is
// useful to make sure you get a linear chain of git patches, and to avoid two
// contributors pushing conflicting deploys. This isn't git specific, and so any
// arbitrary string hash can be used.
// FIXME: prune old deploys from the store when they aren't needed anymore...
func AddDeploy(obj Client, id uint64, hash, pHash string, data *string) error {
// key structure is $NS/deploy/$id/payload = $data
// key structure is $NS/deploy/$id/hash = $hash
path := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, payloadPath)
tPath := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id, hashPath)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this transaction (then)
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
// we're append only, so ensure this unique deploy id doesn't exist
ifs = append(ifs, etcd.Compare(etcd.Version(path), "=", 0)) // KeyMissing
//ifs = append(ifs, etcd.KeyMissing(path))
// don't look for previous deploy if this is the first deploy ever
if id > 1 {
// we append sequentially, so ensure previous key *does* exist
prev := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, payloadPath)
ifs = append(ifs, etcd.Compare(etcd.Version(prev), ">", 0)) // KeyExists
//ifs = append(ifs, etcd.KeyExists(prev))
if hash != "" && pHash != "" {
// does the previously stored hash match what we expect?
prevHash := fmt.Sprintf("%s/%s/%d/%s", NS, deployPath, id-1, hashPath)
ifs = append(ifs, etcd.Compare(etcd.Value(prevHash), "=", pHash))
}
}
ops = append(ops, etcd.OpPut(path, *data))
if hash != "" {
ops = append(ops, etcd.OpPut(tPath, hash)) // store new hash as well
}
// it's important to do this in one transaction, and atomically, because
// this way, we only generate one watch event, and only when it's needed
result, err := obj.Txn(ifs, ops, nil)
if err != nil {
return errwrap.Wrapf(err, "error creating deploy id %d: %s", id)
}
if !result.Succeeded {
return fmt.Errorf("could not create deploy id %d", id)
}
return nil // success
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -79,16 +79,23 @@ import (
// constant parameters which may need to be tweaked or customized
const (
NS = "_mgmt" // root namespace for mgmt operations
seedSentinel = "_seed" // you must not name your hostname this
MaxStartServerTimeout = 60 // max number of seconds to wait for server to start
MaxStartServerRetries = 3 // number of times to retry starting the etcd server
maxClientConnectRetries = 5 // number of times to retry consecutive connect failures
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
NS = "/_mgmt" // root namespace for mgmt operations
seedSentinel = "_seed" // you must not name your hostname this
MaxStartServerTimeout = 60 // max number of seconds to wait for server to start
MaxStartServerRetries = 3 // number of times to retry starting the etcd server
maxClientConnectRetries = 5 // number of times to retry consecutive connect failures
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"
// 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
// some code which is possibly not as efficient as it could be. Let us
// know so that we can analyze the situation, and increase this if
// necessary.
DefaultMaxTxnOps = 512
)
var (
@@ -170,18 +177,21 @@ type EmbdEtcd struct { // EMBeddeD etcd
ctxErr error // permanent ctx error
// exit and cleanup related
cancelLock sync.Mutex // lock for the cancels list
cancels []func() // array of every cancel function for watches
exiting bool
exitchan chan struct{}
exitTimeout <-chan time.Time
cancelLock sync.Mutex // lock for the cancels list
cancels []func() // array of every cancel function for watches
exiting bool
exitchan chan struct{}
exitchanCb chan struct{}
exitwg *sync.WaitGroup // wait for main loops to shutdown
hostname string
memberID uint64 // cluster membership id of server if running
endpoints etcdtypes.URLsMap // map of servers a client could connect to
clientURLs etcdtypes.URLs // locations to listen for clients if i am a server
serverURLs etcdtypes.URLs // locations to listen for servers if i am a server (peer)
noServer bool // disable all server peering if true
hostname string
memberID uint64 // cluster membership id of server if running
endpoints etcdtypes.URLsMap // map of servers a client could connect to
clientURLs etcdtypes.URLs // locations to listen for clients if i am a server
serverURLs etcdtypes.URLs // locations to listen for servers if i am a server (peer)
advertiseClientURLs etcdtypes.URLs // client urls to advertise
advertiseServerURLs etcdtypes.URLs // server urls to advertise
noServer bool // disable all server peering if true
// local tracked state
nominated etcdtypes.URLsMap // copy of who's nominated to locally track state
@@ -208,32 +218,39 @@ type EmbdEtcd struct { // EMBeddeD etcd
}
// NewEmbdEtcd creates the top level embedded etcd struct client and server obj.
func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs etcdtypes.URLs, noServer bool, idealClusterSize uint16, flags Flags, prefix string, converger converger.Converger) *EmbdEtcd {
func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClientURLs, advertiseServerURLs etcdtypes.URLs, noServer bool, idealClusterSize uint16, flags Flags, prefix string, converger converger.Converger) *EmbdEtcd {
endpoints := make(etcdtypes.URLsMap)
if hostname == seedSentinel { // safety
return nil
}
if noServer && len(seeds) == 0 {
log.Printf("Etcd: need at least one seed if running with --no-server!")
return nil
}
if len(seeds) > 0 {
endpoints[seedSentinel] = seeds
idealClusterSize = 0 // unset, get from running cluster
}
obj := &EmbdEtcd{
exitchan: make(chan struct{}), // exit signal for main loop
exitTimeout: nil,
awq: make(chan *AW),
wevents: make(chan *RE),
setq: make(chan *KV),
getq: make(chan *GQ),
delq: make(chan *DL),
txnq: make(chan *TN),
exitchan: make(chan struct{}), // exit signal for main loop
exitchanCb: make(chan struct{}),
exitwg: &sync.WaitGroup{},
awq: make(chan *AW),
wevents: make(chan *RE),
setq: make(chan *KV),
getq: make(chan *GQ),
delq: make(chan *DL),
txnq: make(chan *TN),
nominated: make(etcdtypes.URLsMap),
hostname: hostname,
endpoints: endpoints,
clientURLs: clientURLs,
serverURLs: serverURLs,
noServer: noServer,
hostname: hostname,
endpoints: endpoints,
clientURLs: clientURLs,
serverURLs: serverURLs,
advertiseClientURLs: advertiseClientURLs,
advertiseServerURLs: advertiseServerURLs,
noServer: noServer,
idealClusterSize: idealClusterSize,
converger: converger,
@@ -261,6 +278,11 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs etcdtypes.URLs,
return obj
}
// GetClient returns a handle to the raw etcd client object for those scenarios.
func (obj *EmbdEtcd) GetClient() *etcd.Client {
return obj.client
}
// GetConfig returns the config struct to be used for the etcd client connect.
func (obj *EmbdEtcd) GetConfig() etcd.Config {
endpoints := []string{}
@@ -317,7 +339,7 @@ func (obj *EmbdEtcd) Connect(reconnect bool) error {
emax++
if emax > maxClientConnectRetries {
log.Printf("Etcd: The dataDir (%s) might be inconsistent or corrupt.", obj.dataDir)
log.Printf("Etcd: Please see: %s", "https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md#what-does-the-error-message-about-an-inconsistent-datadir-mean")
log.Printf("Etcd: Please see: %s", "https://github.com/purpleidea/mgmt/blob/master/docs/faq.md#what-does-the-error-message-about-an-inconsistent-datadir-mean")
obj.cError = fmt.Errorf("can't find an available endpoint")
return obj.cError
}
@@ -359,11 +381,11 @@ func (obj *EmbdEtcd) Startup() error {
go obj.Loop() // start main loop
// TODO: implement native etcd watcher method on member API changes
path := fmt.Sprintf("/%s/nominated/", NS)
path := fmt.Sprintf("%s/nominated/", NS)
go obj.AddWatcher(path, obj.nominateCallback, true, false, etcd.WithPrefix()) // no block
// setup ideal cluster size watcher
key := fmt.Sprintf("/%s/idealClusterSize", NS)
key := fmt.Sprintf("%s/idealClusterSize", NS)
go obj.AddWatcher(key, obj.idealClusterSizeCallback, true, false) // no block
// if we have no endpoints, it means we are bootstrapping...
@@ -389,16 +411,20 @@ func (obj *EmbdEtcd) Startup() error {
}
if !obj.noServer {
path := fmt.Sprintf("/%s/volunteers/", NS)
path := fmt.Sprintf("%s/volunteers/", NS)
go obj.AddWatcher(path, obj.volunteerCallback, true, false, etcd.WithPrefix()) // no block
}
// if i am alone and will have to be a server...
if !obj.noServer && bootstrapping {
log.Printf("Etcd: Bootstrapping...")
surls := obj.serverURLs
if len(obj.advertiseServerURLs) > 0 {
surls = obj.advertiseServerURLs
}
// give an initial value to the obj.nominate map we keep in sync
// this emulates Nominate(obj, obj.hostname, obj.serverURLs)
obj.nominated[obj.hostname] = obj.serverURLs // initial value
obj.nominated[obj.hostname] = surls // initial value
// NOTE: when we are stuck waiting for the server to start up,
// it is probably happening on this call right here...
obj.nominateCallback(nil) // kick this off once
@@ -407,8 +433,12 @@ func (obj *EmbdEtcd) Startup() error {
// self volunteer
if !obj.noServer && len(obj.serverURLs) > 0 {
// we run this in a go routine because it blocks waiting for server
surls := obj.serverURLs
if len(obj.advertiseServerURLs) > 0 {
surls = obj.advertiseServerURLs
}
log.Printf("Etcd: Startup: Volunteering...")
go Volunteer(obj, obj.serverURLs)
go Volunteer(obj, surls)
}
if bootstrapping {
@@ -419,7 +449,7 @@ func (obj *EmbdEtcd) Startup() error {
}
}
go obj.AddWatcher(fmt.Sprintf("/%s/endpoints/", NS), obj.endpointCallback, true, false, etcd.WithPrefix())
go obj.AddWatcher(fmt.Sprintf("%s/endpoints/", NS), obj.endpointCallback, true, false, etcd.WithPrefix())
if err := obj.Connect(false); err != nil { // don't exit from this Startup function until connected!
return err
@@ -449,7 +479,8 @@ func (obj *EmbdEtcd) Destroy() error {
}
obj.cancelLock.Unlock()
obj.exitchan <- struct{}{} // cause main loop to exit
close(obj.exitchan) // cause main loop to exit
close(obj.exitchanCb)
obj.rLock.Lock()
if obj.client != nil {
@@ -462,6 +493,7 @@ func (obj *EmbdEtcd) Destroy() error {
//if obj.server != nil {
// return obj.DestroyServer()
//}
obj.exitwg.Wait()
return nil
}
@@ -537,7 +569,7 @@ func (obj *EmbdEtcd) CtxError(ctx context.Context, err error) (context.Context,
// tmin <= texp^iter - 1 <= tmax // TODO: check my math
return time.Duration(math.Min(math.Max(math.Pow(float64(texp), float64(iter))-1.0, float64(tmin)), float64(tmax))) * time.Millisecond
}
var isTimeout = false
var isTimeout bool
var iter int // = 0
if ctxerr, ok := ctx.Value(ctxErr).(error); ok {
if obj.flags.Debug {
@@ -703,12 +735,15 @@ func (obj *EmbdEtcd) CtxError(ctx context.Context, err error) (context.Context,
// CbLoop is the loop where callback execution is serialized.
func (obj *EmbdEtcd) CbLoop() {
obj.exitwg.Add(1)
defer obj.exitwg.Done()
cuid := obj.converger.Register()
cuid.SetName("Etcd: CbLoop")
defer cuid.Unregister()
if e := obj.Connect(false); e != nil {
return // fatal
}
var exitTimeout <-chan time.Time // = nil is implied
// we use this timer because when we ignore un-converge events and loop,
// we reset the ConvergedTimer case statement, ruining the timeout math!
cuid.StartTimer()
@@ -748,8 +783,18 @@ func (obj *EmbdEtcd) CbLoop() {
log.Printf("Trace: Etcd: CbLoop: Event: FinishLoop")
}
// exit loop signal
case <-obj.exitchanCb:
obj.exitchanCb = nil
log.Println("Etcd: Exiting loop shortly...")
// activate exitTimeout switch which only opens after N
// seconds of inactivity in this select switch, which
// lets everything get bled dry to avoid blocking calls
// which would otherwise block us from exiting cleanly!
exitTimeout = util.TimeAfterOrBlock(exitDelay)
// exit loop commit
case <-obj.exitTimeout:
case <-exitTimeout:
log.Println("Etcd: Exiting callback loop!")
cuid.StopTimer() // clean up nicely
return
@@ -759,12 +804,15 @@ func (obj *EmbdEtcd) CbLoop() {
// Loop is the main loop where everything is serialized.
func (obj *EmbdEtcd) Loop() {
obj.exitwg.Add(1) // TODO: add these to other go routines?
defer obj.exitwg.Done()
cuid := obj.converger.Register()
cuid.SetName("Etcd: Loop")
defer cuid.Unregister()
if e := obj.Connect(false); e != nil {
return // fatal
}
var exitTimeout <-chan time.Time // = nil is implied
cuid.StartTimer()
for {
ctx := context.Background() // TODO: inherit as input argument?
@@ -899,15 +947,16 @@ func (obj *EmbdEtcd) Loop() {
// exit loop signal
case <-obj.exitchan:
obj.exitchan = nil
log.Println("Etcd: Exiting loop shortly...")
// activate exitTimeout switch which only opens after N
// seconds of inactivity in this select switch, which
// lets everything get bled dry to avoid blocking calls
// which would otherwise block us from exiting cleanly!
obj.exitTimeout = util.TimeAfterOrBlock(exitDelay)
exitTimeout = util.TimeAfterOrBlock(exitDelay)
// exit loop commit
case <-obj.exitTimeout:
case <-exitTimeout:
log.Println("Etcd: Exiting loop!")
cuid.StopTimer() // clean up nicely
return
@@ -1436,14 +1485,21 @@ func (obj *EmbdEtcd) nominateCallback(re *RE) error {
// client connects to one of the obj.endpoints servers...
log.Printf("Etcd: Addresses are: %s", addresses)
surls := obj.serverURLs
if len(obj.advertiseServerURLs) > 0 {
surls = obj.advertiseServerURLs
}
// XXX: just put this wherever for now so we don't block
// nominate self so "member" list is correct for peers to see
Nominate(obj, obj.hostname, obj.serverURLs)
Nominate(obj, obj.hostname, surls)
// XXX: if this fails, where will we retry this part ?
}
// advertise client urls
if curls := obj.clientURLs; len(curls) > 0 {
if len(obj.advertiseClientURLs) > 0 {
curls = obj.advertiseClientURLs
}
// XXX: don't advertise local addresses! 127.0.0.1:2381 doesn't really help remote hosts
// XXX: but sometimes this is what we want... hmmm how do we decide? filter on callback?
AdvertiseEndpoints(obj, curls)
@@ -1578,7 +1634,7 @@ func (obj *EmbdEtcd) idealClusterSizeCallback(re *RE) error {
log.Printf("Trace: Etcd: idealClusterSizeCallback()")
defer log.Printf("Trace: Etcd: idealClusterSizeCallback(): Finished!")
}
path := fmt.Sprintf("/%s/idealClusterSize", NS)
path := fmt.Sprintf("%s/idealClusterSize", NS)
for _, event := range re.response.Events {
if key := bytes.NewBuffer(event.Kv.Key).String(); key != path {
continue
@@ -1647,15 +1703,25 @@ func (obj *EmbdEtcd) StartServer(newCluster bool, peerURLsMap etcdtypes.URLsMap)
initialPeerURLsMap[memberName] = peerURLs
}
aCUrls := obj.clientURLs
if len(obj.advertiseClientURLs) > 0 {
aCUrls = obj.advertiseClientURLs
}
aPUrls := peerURLs
if len(obj.advertiseServerURLs) > 0 {
aPUrls = obj.advertiseServerURLs
}
// embed etcd
cfg := embed.NewConfig()
cfg.Name = memberName // hostname
cfg.Dir = obj.dataDir
cfg.ACUrls = obj.clientURLs
cfg.APUrls = peerURLs
cfg.LCUrls = obj.clientURLs
cfg.LPUrls = peerURLs
cfg.ACUrls = aCUrls
cfg.APUrls = aPUrls
cfg.StrictReconfigCheck = false // XXX: workaround https://github.com/coreos/etcd/issues/6305
cfg.MaxTxnOps = DefaultMaxTxnOps
cfg.InitialCluster = initialPeerURLsMap.String() // including myself!
if newCluster {

49
etcd/etcd_test.go Normal file
View File

@@ -0,0 +1,49 @@
// 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 etcd
import (
"testing"
etcdtypes "github.com/coreos/etcd/pkg/types"
)
func TestNewEmbdEtcd(t *testing.T) {
// should return a new etcd object
noServer := false
var flags Flags
obj := NewEmbdEtcd("", nil, nil, nil, nil, nil, noServer, 0, flags, "", nil)
if obj == nil {
t.Fatal("failed to create server object")
}
}
func TestNewEmbdEtcdConfigValidation(t *testing.T) {
// running --no-server with no --seeds specified should fail early
seeds := make(etcdtypes.URLs, 0)
noServer := true
var flags Flags
obj := NewEmbdEtcd("", seeds, nil, nil, nil, nil, noServer, 0, flags, "", nil)
if obj != nil {
t.Fatal("server initialization should fail on invalid configuration")
}
}

540
etcd/fs/file.go Normal file
View File

@@ -0,0 +1,540 @@
// 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 fs
import (
"bytes"
"encoding/gob"
"fmt"
"io"
"log"
"os"
"path"
"strings"
"syscall"
"time"
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
errwrap "github.com/pkg/errors"
)
func init() {
gob.Register(&File{})
}
// File represents a file node. This is the node of our tree structure. This is
// not thread safe, and you can have at most one open file handle at a time.
type File struct {
// FIXME: add a rwmutex to make this thread safe
fs *Fs // pointer to file system
Path string // relative path to file, trailing slash if it's a directory
Mode os.FileMode
ModTime time.Time
//Size int64 // XXX: cache the size to avoid full file downloads for stat!
Children []*File // dir's use this
Hash string // string not []byte so it's readable, matches data
data []byte // cache of the data. private so it doesn't get encoded
cursor int64
dirCursor int64
readOnly bool // is the file read-only?
closed bool // is file closed?
}
// path returns the expected path to the actual file in etcd.
func (obj *File) path() string {
// keys are prefixed with the hash-type eg: {sha256} to allow different
// superblocks to share the same data prefix even with different hashes
return fmt.Sprintf("%s/{%s}%s", obj.fs.sb.DataPrefix, obj.fs.Hash, obj.Hash)
}
// cache downloads the file contents from etcd and stores them in our cache.
func (obj *File) cache() error {
if obj.Mode.IsDir() {
return nil
}
h, err := obj.fs.hash(obj.data) // update hash
if err != nil {
return err
}
if h == obj.Hash { // we already have the correct data cached
return nil
}
p := obj.path() // get file data from this path in etcd
result, err := obj.fs.get(p) // download the file...
if err != nil {
return err
}
if result == nil || len(result) == 0 { // nothing found
return err
}
data, exists := result[p]
if !exists {
return fmt.Errorf("could not find data") // programming error?
}
obj.data = data // save
return nil
}
// findNode is the "in array" equivalent for searching through a dir's children.
// You must *not* specify an absolute path as the search string, but rather you
// should specify the name. To search for something name "bar" inside a dir
// named "/tmp/foo/", you just pass in "bar", not "/tmp/foo/bar".
func (obj *File) findNode(name string) (*File, bool) {
for _, node := range obj.Children {
if name == node.Path {
return node, true // found
}
}
return nil, false // not found
}
func fileCreate(fs *Fs, name string) (*File, error) {
if name == "" {
return nil, fmt.Errorf("invalid input path")
}
if !strings.HasPrefix(name, "/") {
return nil, fmt.Errorf("invalid input path (not absolute)")
}
cleanPath := path.Clean(name) // remove possible trailing slashes
// try to add node to tree by first finding the parent node
parentPath, filePath := path.Split(cleanPath) // looking for this
node, err := fs.find(parentPath)
if err != nil { // might be ErrNotExist
return nil, err
}
fi, err := node.Stat()
if err != nil {
return nil, err
}
if !fi.IsDir() { // is the parent a suitable home?
return nil, &os.PathError{Op: "create", Path: name, Err: syscall.ENOTDIR}
}
f, exists := node.findNode(filePath) // does file already exist inside?
if exists { // already exists, overwrite!
if err := f.Truncate(0); err != nil {
return nil, err
}
return f, nil
}
data := []byte("") // empty file contents
h, err := fs.hash(data) // TODO: use memoized value?
if err != nil {
return &File{}, err // TODO: nil instead?
}
f = &File{
fs: fs,
Path: filePath, // the relative path chunk (not incl. dir name)
Hash: h,
data: data,
}
// add to parent
node.Children = append(node.Children, f)
// push new file up if not on server, and then push up the metadata
if err := f.Sync(); err != nil {
return f, err // TODO: ok to return the file so user can run sync?
}
return f, nil
}
func fileOpen(fs *Fs, name string) (*File, error) {
if name == "" {
return nil, fmt.Errorf("invalid input path")
}
if !strings.HasPrefix(name, "/") {
return nil, fmt.Errorf("invalid input path (not absolute)")
}
cleanPath := path.Clean(name) // remove possible trailing slashes
node, err := fs.find(cleanPath)
if err != nil { // might be ErrNotExist
return &File{}, err // TODO: nil instead?
}
// download file contents into obj.data
if err := node.cache(); err != nil {
return &File{}, err // TODO: nil instead?
}
//fi, err := node.Stat()
//if err != nil {
// return nil, err
//}
//if fi.IsDir() { // can we open a directory? - yes we can apparently
// return nil, fmt.Errorf("file is a directory")
//}
node.readOnly = true // as per docs, fileOpen opens files as read-only
node.closed = false // as per docs, fileOpen opens files as read-only
return node, nil
}
// Close closes the file handle. This will try and run Sync automatically.
func (obj *File) Close() error {
if !obj.readOnly {
obj.ModTime = time.Now()
}
if err := obj.Sync(); err != nil {
return err
}
// FIXME: there is a big implementation mistake between the metadata
// node and the file handle, since they're currently sharing a struct!
// invalidate all of the fields
//obj.fs = nil
//obj.Path = ""
//obj.Mode = os.FileMode(0)
//obj.ModTime = time.Time{}
//obj.Children = nil
//obj.Hash = ""
//obj.data = nil
obj.cursor = 0
obj.readOnly = false
obj.closed = true
return nil
}
// Name returns the path of the file.
func (obj *File) Name() string {
return obj.Path
}
// Stat returns some information about the file.
func (obj *File) Stat() (os.FileInfo, error) {
// download file contents into obj.data
if err := obj.cache(); err != nil { // needed so Size() works correctly
return nil, err
}
return &FileInfo{ // everything is actually stored in the main file node
file: obj,
}, nil
}
// Sync flushes the file contents to the server and calls the filesystem
// metadata sync as well.
// FIXME: instead of a txn, run a get and then a put in two separate stages. if
// the get already found the data up there, then we don't need to push it all in
// the put phase. with the txn it is always all sent up even if the put is never
// needed. the get should just be a "key exists" test, and not a download of the
// whole file. if we *do* do the download, we can byte-by-byte check for hash
// collisions and panic if we find one :)
func (obj *File) Sync() error {
if obj.closed {
return ErrFileClosed
}
p := obj.path() // store file data at this path in etcd
// TODO: use https://github.com/coreos/etcd/pull/7417 if merged
cmp := etcd.Compare(etcd.Version(p), "=", 0) // KeyMissing
//cmp := etcd.KeyMissing(p))
op := etcd.OpPut(p, string(obj.data)) // this pushes contents to server
// it's important to do this in one transaction, and atomically, because
// this way, we only generate one watch event, and only when it's needed
result, err := obj.fs.txn([]etcd.Cmp{cmp}, []etcd.Op{op}, nil)
if err != nil {
return errwrap.Wrapf(err, "sync error with: %s (%s)", obj.Path, p)
}
if !result.Succeeded {
if obj.fs.Debug {
log.Printf("debug: data already exists in storage")
}
}
if err := obj.fs.sync(); err != nil { // push metadata up to server
return err
}
return nil
}
// Truncate trims the file to the requested size. Since our file system can only
// read and write data, but never edit existing data blocks, doing this will not
// cause more space to be available.
func (obj *File) Truncate(size int64) error {
if obj.closed {
return ErrFileClosed
}
if obj.readOnly {
return &os.PathError{Op: "truncate", Path: obj.Path, Err: ErrFileReadOnly}
}
if size < 0 {
return ErrOutOfRange
}
if size > 0 { // if size == 0, we don't need to run cache!
// download file contents into obj.data
if err := obj.cache(); err != nil {
return err
}
}
if size > int64(len(obj.data)) {
diff := size - int64(len(obj.data))
obj.data = append(obj.data, bytes.Repeat([]byte{00}, int(diff))...)
} else {
obj.data = obj.data[0:size]
}
h, err := obj.fs.hash(obj.data) // update hash
if err != nil {
return err
}
obj.Hash = h
obj.ModTime = time.Now()
// this pushes the new data and metadata up to etcd
return obj.Sync()
}
// Read reads up to len(b) bytes from the File. It returns the number of bytes
// read and any error encountered. At end of file, Read returns 0, io.EOF.
// NOTE: This reads into the byte input. It's a side effect!
func (obj *File) Read(b []byte) (n int, err error) {
if obj.closed {
return 0, ErrFileClosed
}
if obj.Mode.IsDir() {
return 0, fmt.Errorf("file is a directory")
}
// download file contents into obj.data
if err := obj.cache(); err != nil {
return 0, err // TODO: -1 ?
}
// TODO: can we optimize by reading just the length from etcd, and also
// by only downloading the data range we're interested in?
if len(b) > 0 && int(obj.cursor) == len(obj.data) {
return 0, io.EOF
}
if len(obj.data)-int(obj.cursor) >= len(b) {
n = len(b)
} else {
n = len(obj.data) - int(obj.cursor)
}
copy(b, obj.data[obj.cursor:obj.cursor+int64(n)]) // store into input b
obj.cursor = obj.cursor + int64(n) // update cursor
return
}
// ReadAt reads len(b) bytes from the File starting at byte offset off. It
// returns the number of bytes read and the error, if any. ReadAt always returns
// a non-nil error when n < len(b). At end of file, that error is io.EOF.
func (obj *File) ReadAt(b []byte, off int64) (n int, err error) {
obj.cursor = off
return obj.Read(b)
}
// Readdir lists the contents of the directory and returns a list of file info
// objects for each entry.
func (obj *File) Readdir(count int) ([]os.FileInfo, error) {
if !obj.Mode.IsDir() {
return nil, &os.PathError{Op: "readdir", Path: obj.Name(), Err: syscall.ENOTDIR}
}
children := obj.Children[obj.dirCursor:] // available children to output
var l = int64(len(children)) // initially assume to return them all
var err error
// for count > 0, if we return the last entry, also return io.EOF
if count > 0 {
l = int64(count) // initial assumption
if c := len(children); count >= c {
l = int64(c)
err = io.EOF // this result includes the last dir entry
}
}
obj.dirCursor += l // store our progress
output := make([]os.FileInfo, l)
// TODO: should this be sorted by "directory order" what does that mean?
// from `man 3 readdir`: "unlikely that the names will be sorted"
for i := range output {
output[i] = &FileInfo{
file: children[i],
}
}
// we're seen the whole directory, so reset the cursor
if err == io.EOF || count <= 0 {
obj.dirCursor = 0 // TODO: is it okay to reset the cursor?
}
return output, err
}
// Readdirnames returns a list of name is the current file handle's directory.
// TODO: this implementation shares the dirCursor with Readdir, is this okay?
// TODO: should Readdirnames even use a dirCursor at all?
func (obj *File) Readdirnames(n int) (names []string, _ error) {
fis, err := obj.Readdir(n)
if fis != nil {
for i, x := range fis {
if x != nil {
names = append(names, fis[i].Name())
}
}
}
return names, err
}
// Seek sets the offset for the next Read or Write on file to offset,
// interpreted according to whence: 0 means relative to the origin of the file,
// 1 means relative to the current offset, and 2 means relative to the end. It
// returns the new offset and an error, if any. The behavior of Seek on a file
// opened with O_APPEND is not specified.
func (obj *File) Seek(offset int64, whence int) (int64, error) {
if obj.closed {
return 0, ErrFileClosed
}
switch whence {
case io.SeekStart: // 0
obj.cursor = offset
case io.SeekCurrent: // 1
obj.cursor += offset
case io.SeekEnd: // 2
// download file contents into obj.data
if err := obj.cache(); err != nil {
return 0, err // TODO: -1 ?
}
obj.cursor = int64(len(obj.data)) + offset
}
return obj.cursor, nil
}
// Write writes to the given file.
func (obj *File) Write(b []byte) (n int, err error) {
if obj.closed {
return 0, ErrFileClosed
}
if obj.readOnly {
return 0, &os.PathError{Op: "write", Path: obj.Path, Err: ErrFileReadOnly}
}
// download file contents into obj.data
if err := obj.cache(); err != nil {
return 0, err // TODO: -1 ?
}
// calculate the write
n = len(b)
cur := obj.cursor
diff := cur - int64(len(obj.data))
var tail []byte
if n+int(cur) < len(obj.data) {
tail = obj.data[n+int(cur):]
}
if diff > 0 {
obj.data = append(bytes.Repeat([]byte{00}, int(diff)), b...)
obj.data = append(obj.data, tail...)
} else {
obj.data = append(obj.data[:cur], b...)
obj.data = append(obj.data, tail...)
}
h, err := obj.fs.hash(obj.data) // update hash
if err != nil {
return 0, err // TODO: -1 ?
}
obj.Hash = h
obj.ModTime = time.Now()
// this pushes the new data and metadata up to etcd
if err := obj.Sync(); err != nil {
return 0, err // TODO: -1 ?
}
obj.cursor = int64(len(obj.data))
return
}
// WriteAt writes into the given file at a certain offset.
func (obj *File) WriteAt(b []byte, off int64) (n int, err error) {
obj.cursor = off
return obj.Write(b)
}
// WriteString writes a string to the file.
func (obj *File) WriteString(s string) (n int, err error) {
return obj.Write([]byte(s))
}
// FileInfo is a struct which provides some information about a file handle.
type FileInfo struct {
file *File // anonymous pointer to the actual file
}
// Name returns the base name of the file.
func (obj *FileInfo) Name() string {
return obj.file.Name()
}
// Size returns the length in bytes.
func (obj *FileInfo) Size() int64 {
return int64(len(obj.file.data))
}
// Mode returns the file mode bits.
func (obj *FileInfo) Mode() os.FileMode {
return obj.file.Mode
}
// ModTime returns the modification time.
func (obj *FileInfo) ModTime() time.Time {
return obj.file.ModTime
}
// IsDir is an abbreviation for Mode().IsDir().
func (obj *FileInfo) IsDir() bool {
//return obj.file.Mode&os.ModeDir != 0
return obj.file.Mode.IsDir()
}
// Sys returns the underlying data source (can return nil).
func (obj *FileInfo) Sys() interface{} {
return nil // TODO: should we do something better?
//return obj.file.fs // TODO: would this work?
}

821
etcd/fs/fs.go Normal file
View File

@@ -0,0 +1,821 @@
// 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 fs implements a very simple and limited file system on top of etcd.
package fs
import (
"bytes"
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"errors"
"fmt"
"hash"
"io"
"log"
"os"
"path"
"strings"
"syscall"
"time"
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
rpctypes "github.com/coreos/etcd/etcdserver/api/v3rpc/rpctypes"
errwrap "github.com/pkg/errors"
"github.com/spf13/afero"
context "golang.org/x/net/context"
)
func init() {
gob.Register(&superBlock{})
}
const (
// EtcdTimeout is the timeout to wait before erroring.
EtcdTimeout = 5 * time.Second // FIXME: chosen arbitrarily
// DefaultDataPrefix is the default path for data storage in etcd.
DefaultDataPrefix = "/_etcdfs/data"
// DefaultHash is the default hashing algorithm to use.
DefaultHash = "sha256"
// PathSeparator is the path separator to use on this filesystem.
PathSeparator = os.PathSeparator // usually the slash character
)
// TODO: https://dave.cheney.net/2016/04/07/constant-errors
var (
IsPathSeparator = os.IsPathSeparator
// ErrNotImplemented is returned when something is not implemented by design.
ErrNotImplemented = errors.New("not implemented")
// ErrExist is returned when requested path already exists.
ErrExist = os.ErrExist
// ErrNotExist is returned when we can't find the requested path.
ErrNotExist = os.ErrNotExist
ErrFileClosed = errors.New("File is closed")
ErrFileReadOnly = errors.New("File handle is read only")
ErrOutOfRange = errors.New("Out of range")
)
// Fs is a specialized afero.Fs implementation for etcd. It implements a small
// subset of the features, and has some special properties. In particular, file
// data is stored with it's unique reference being a hash of the data. In this
// way, you cannot actually edit a file, but rather you create a new one, and
// update the metadata pointer to point to the new blob. This might seem slow,
// but it has the unique advantage of being relatively straight forward to
// implement, and repeated uploads of the same file cost almost nothing. Since
// etcd isn't meant for large file systems, this fits the desired use case.
// This implementation is designed to have a single writer for each superblock,
// but as many readers as you like.
// FIXME: this is not currently thread-safe, nor is it clear if it needs to be.
// XXX: we probably aren't updating the modification time everywhere we should!
// XXX: because we never delete data blocks, we need to occasionally "vacuum".
// XXX: this is harder because we need to list of *all* metadata paths, if we
// want them to be able to share storage backends. (we do)
type Fs struct {
Client *etcd.Client
Metadata string // location of "superblock" for this filesystem
DataPrefix string // prefix of data storage (no trailing slashes)
Hash string // eg: sha256
Debug bool
sb *superBlock
mounted bool
}
// superBlock is the metadata structure of everything stored outside of the data
// section in etcd. Its fields need to be exported or they won't get marshalled.
type superBlock struct {
DataPrefix string // prefix of data storage
Hash string // hashing algorithm used
Tree *File // filesystem tree
}
// NewEtcdFs creates a new filesystem handle on an etcd client connection. You
// must specify the metadata string that you wish to use.
func NewEtcdFs(client *etcd.Client, metadata string) afero.Fs {
return &Fs{
Client: client,
Metadata: metadata,
}
}
// get a number of values from etcd.
func (obj *Fs) get(path string, opts ...etcd.OpOption) (map[string][]byte, error) {
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
resp, err := obj.Client.Get(ctx, path, opts...)
cancel()
if err != nil || resp == nil {
return nil, err
}
// TODO: write a resp.ToMap() function on https://godoc.org/github.com/coreos/etcd/etcdserver/etcdserverpb#RangeResponse
result := make(map[string][]byte) // formerly: map[string][]byte
for _, x := range resp.Kvs {
result[string(x.Key)] = x.Value // formerly: bytes.NewBuffer(x.Value).String()
}
return result, nil
}
// put a value into etcd.
func (obj *Fs) put(path string, data []byte, opts ...etcd.OpOption) error {
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
_, err := obj.Client.Put(ctx, path, string(data), opts...) // TODO: obj.Client.KV ?
cancel()
if err != nil {
switch err {
case context.Canceled:
return errwrap.Wrapf(err, "ctx canceled")
case context.DeadlineExceeded:
return errwrap.Wrapf(err, "ctx deadline exceeded")
case rpctypes.ErrEmptyKey:
return errwrap.Wrapf(err, "client-side error")
default:
return errwrap.Wrapf(err, "invalid endpoints")
}
}
return nil
}
// txn runs a txn in etcd.
func (obj *Fs) txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error) {
ctx, cancel := context.WithTimeout(context.Background(), EtcdTimeout)
resp, err := obj.Client.Txn(ctx).If(ifcmps...).Then(thenops...).Else(elseops...).Commit()
cancel()
return resp, err
}
// hash is a small helper that does the hashing for us.
func (obj *Fs) hash(input []byte) (string, error) {
var h hash.Hash
switch obj.Hash {
// TODO: add other hashes
case "sha256":
h = sha256.New()
default:
return "", fmt.Errorf("hash does not exist")
}
src := bytes.NewReader(input)
if _, err := io.Copy(h, src); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// sync overwrites the superblock with whatever version we have stored.
func (obj *Fs) sync() error {
b := bytes.Buffer{}
e := gob.NewEncoder(&b)
err := e.Encode(&obj.sb) // pass with &
if err != nil {
return errwrap.Wrapf(err, "gob failed to encode")
}
//base64.StdEncoding.EncodeToString(b.Bytes())
return obj.put(obj.Metadata, b.Bytes())
}
// mount downloads the initial cache of metadata, including the *file tree.
// Since there's no explicit mount API in the afero.Fs interface, we hide this
// method inside any operation that might do any real work, and make it
// idempotent so that it can be called as much as we want. If there's no
// metadata found (superblock) then we create one.
func (obj *Fs) mount() error {
if obj.mounted {
return nil
}
result, err := obj.get(obj.Metadata) // download the metadata...
if err != nil {
return err
}
if result == nil || len(result) == 0 { // nothing found, create the fs
if obj.Debug {
log.Printf("debug: mount: creating new fs at: %s", obj.Metadata)
}
// trim any trailing slashes from DataPrefix
for strings.HasSuffix(obj.DataPrefix, "/") {
obj.DataPrefix = strings.TrimSuffix(obj.DataPrefix, "/")
}
if obj.DataPrefix == "" {
obj.DataPrefix = DefaultDataPrefix
}
if obj.Hash == "" {
obj.Hash = DefaultHash
}
// test run an empty string to see if our hash selection works!
if _, err := obj.hash([]byte("")); err != nil {
return fmt.Errorf("cannot hash with %s", obj.Hash)
}
obj.sb = &superBlock{
DataPrefix: obj.DataPrefix,
Hash: obj.Hash,
Tree: &File{ // include a root directory
fs: obj,
Path: "", // root dir is "" (empty string)
Mode: os.ModeDir,
},
}
if err := obj.sync(); err != nil {
return err
}
obj.mounted = true
return nil
}
if obj.Debug {
log.Printf("debug: mount: opening old fs at: %s", obj.Metadata)
}
sb, exists := result[obj.Metadata]
if !exists {
return fmt.Errorf("could not find metadata") // programming error?
}
// decode into obj.sb
//bb, err := base64.StdEncoding.DecodeString(str)
//if err != nil {
// return errwrap.Wrapf(err, "base64 failed to decode")
//}
//b := bytes.NewBuffer(bb)
b := bytes.NewBuffer(sb)
d := gob.NewDecoder(b)
if err := d.Decode(&obj.sb); err != nil { // pass with &
return errwrap.Wrapf(err, "gob failed to decode")
}
if obj.DataPrefix != "" && obj.DataPrefix != obj.sb.DataPrefix {
return fmt.Errorf("the DataPrefix mount option `%s` does not match the remote value of `%s`", obj.DataPrefix, obj.sb.DataPrefix)
}
if obj.Hash != "" && obj.Hash != obj.sb.Hash {
return fmt.Errorf("the Hash mount option `%s` does not match the remote value of `%s`", obj.Hash, obj.sb.Hash)
}
// if all checks passed, copy these values down locally
obj.DataPrefix = obj.sb.DataPrefix
obj.Hash = obj.sb.Hash
// hook up file system pointers to each element in the tree structure
obj.traverse(obj.sb.Tree)
obj.mounted = true
return nil
}
// traverse adds the file system pointer to each element in the tree structure.
func (obj *Fs) traverse(node *File) {
if node == nil {
return
}
node.fs = obj
for _, n := range node.Children {
obj.traverse(n)
}
}
// find returns the file node corresponding to this absolute path if it exists.
func (obj *Fs) find(absPath string) (*File, error) { // TODO: function naming?
if absPath == "" {
return nil, fmt.Errorf("empty path specified")
}
if !strings.HasPrefix(absPath, "/") {
return nil, fmt.Errorf("invalid input path (not absolute)")
}
node := obj.sb.Tree
if node == nil {
return nil, ErrNotExist // no nodes exist yet, not even root dir
}
var x string // first value
sp := PathSplit(absPath)
if x, sp = sp[0], sp[1:]; x != node.Path {
return nil, fmt.Errorf("root values do not match") // TODO: panic?
}
for _, p := range sp {
n, exists := node.findNode(p)
if !exists {
return nil, ErrNotExist
}
node = n // descend into this node
}
return node, nil
}
// Name returns the name of this filesystem.
func (obj *Fs) Name() string { return "etcdfs" }
// URI returns a URI representing this particular filesystem.
func (obj *Fs) URI() string {
return fmt.Sprintf("%s://%s", obj.Name(), obj.Metadata)
}
// Create creates a new file.
func (obj *Fs) Create(name string) (afero.File, error) {
if err := obj.mount(); err != nil {
return nil, err
}
return fileCreate(obj, name)
}
// Mkdir makes a new directory.
func (obj *Fs) Mkdir(name string, perm os.FileMode) error {
if err := obj.mount(); err != nil {
return err
}
if name == "" {
return fmt.Errorf("invalid input path")
}
if !strings.HasPrefix(name, "/") {
return fmt.Errorf("invalid input path (not absolute)")
}
// remove possible trailing slashes
cleanPath := path.Clean(name)
for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input
cleanPath = strings.TrimSuffix(cleanPath, "/")
}
if cleanPath == "" {
if obj.sb.Tree == nil {
return fmt.Errorf("woops, missing root directory")
}
return ErrExist // root directory already exists
}
// try to add node to tree by first finding the parent node
parentPath, dirPath := path.Split(cleanPath) // looking for this
f := &File{
fs: obj,
Path: dirPath,
Mode: os.ModeDir,
// TODO: add perm to struct or let chmod below do it
}
node, err := obj.find(parentPath)
if err != nil { // might be ErrNotExist
return err
}
fi, err := node.Stat()
if err != nil {
return err
}
if !fi.IsDir() { // is the parent a suitable home?
return &os.PathError{Op: "mkdir", Path: name, Err: syscall.ENOTDIR}
}
_, exists := node.findNode(dirPath) // does file already exist inside?
if exists {
return ErrExist
}
// add to parent
node.Children = append(node.Children, f)
// push new file up if not on server, and then push up the metadata
if err := f.Sync(); err != nil {
return err
}
return obj.Chmod(name, perm)
}
// MkdirAll creates a directory named path, along with any necessary parents,
// and returns nil, or else returns an error. The permission bits perm are used
// for all directories that MkdirAll creates. If path is already a directory,
// MkdirAll does nothing and returns nil.
func (obj *Fs) MkdirAll(path string, perm os.FileMode) error {
if err := obj.mount(); err != nil {
return err
}
// Copied mostly verbatim from golang stdlib.
// Fast path: if we can tell whether path is a directory or file, stop
// with success or error.
dir, err := obj.Stat(path)
if err == nil {
if dir.IsDir() {
return nil
}
return &os.PathError{Op: "mkdir", Path: path, Err: syscall.ENOTDIR}
}
// Slow path: make sure parent exists and then call Mkdir for path.
i := len(path)
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
i--
}
j := i
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
j--
}
if j > 1 {
// Create parent
err = obj.MkdirAll(path[0:j-1], perm)
if err != nil {
return err
}
}
// Parent now exists; invoke Mkdir and use its result.
err = obj.Mkdir(path, perm)
if err != nil {
// Handle arguments like "foo/." by
// double-checking that directory doesn't exist.
dir, err1 := obj.Lstat(path)
if err1 == nil && dir.IsDir() {
return nil
}
return err
}
return nil
}
// Open opens a path. It will be opened read-only.
func (obj *Fs) Open(name string) (afero.File, error) {
if err := obj.mount(); err != nil {
return nil, err
}
return fileOpen(obj, name) // this opens as read-only
}
// OpenFile opens a path with a particular flag and permission.
func (obj *Fs) OpenFile(name string, flag int, perm os.FileMode) (afero.File, error) {
if err := obj.mount(); err != nil {
return nil, err
}
chmod := false
f, err := fileOpen(obj, name)
if os.IsNotExist(err) && (flag&os.O_CREATE > 0) {
f, err = fileCreate(obj, name)
chmod = true
}
if err != nil {
return nil, err
}
f.readOnly = (flag == os.O_RDONLY)
if flag&os.O_APPEND > 0 {
if _, err := f.Seek(0, os.SEEK_END); err != nil {
f.Close()
return nil, err
}
}
if flag&os.O_TRUNC > 0 && flag&(os.O_RDWR|os.O_WRONLY) > 0 {
if err := f.Truncate(0); err != nil {
f.Close()
return nil, err
}
}
if chmod {
// TODO: the golang stdlib doesn't check this error, should we?
if err := obj.Chmod(name, perm); err != nil {
return f, err // TODO: should we return the file handle?
}
}
return f, nil
}
// Remove removes a path.
func (obj *Fs) Remove(name string) error {
if err := obj.mount(); err != nil {
return err
}
if name == "" {
return fmt.Errorf("invalid input path")
}
if !strings.HasPrefix(name, "/") {
return fmt.Errorf("invalid input path (not absolute)")
}
// remove possible trailing slashes
cleanPath := path.Clean(name)
for strings.HasSuffix(cleanPath, "/") { // bonus clean for "/" as input
cleanPath = strings.TrimSuffix(cleanPath, "/")
}
if cleanPath == "" {
return fmt.Errorf("can't remove root")
}
f, err := obj.find(name) // get the file
if err != nil {
return err
}
if len(f.Children) > 0 { // this file or dir has children, can't remove!
return &os.PathError{Op: "remove", Path: name, Err: syscall.ENOTEMPTY}
}
// find the parent node
parentPath, filePath := path.Split(cleanPath) // looking for this
node, err := obj.find(parentPath)
if err != nil { // might be ErrNotExist
if os.IsNotExist(err) { // race! must have just disappeared
return nil
}
return err
}
var index = -1 // int
for i, n := range node.Children {
if n.Path == filePath {
index = i // found here!
break
}
}
if index == -1 {
return fmt.Errorf("programming error")
}
// remove from list
node.Children = append(node.Children[:index], node.Children[index+1:]...)
return obj.sync()
}
// RemoveAll removes path and any children it contains. It removes everything it
// can but returns the first error it encounters. If the path does not exist,
// RemoveAll returns nil (no error).
func (obj *Fs) RemoveAll(path string) error {
if err := obj.mount(); err != nil {
return err
}
// Simple case: if Remove works, we're done.
err := obj.Remove(path)
if err == nil || os.IsNotExist(err) {
return nil
}
// Otherwise, is this a directory we need to recurse into?
dir, serr := obj.Lstat(path)
if serr != nil {
// TODO: I didn't check this logic thoroughly (edge cases?)
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
return nil
}
return serr
}
if !dir.IsDir() {
// Not a directory; return the error from Remove.
return err
}
// Directory.
fd, err := obj.Open(path)
if err != nil {
if os.IsNotExist(err) {
// Race. It was deleted between the Lstat and Open.
// Return nil per RemoveAll's docs.
return nil
}
return err
}
// Remove contents & return first error.
err = nil
for {
// TODO: why not do this in one shot? is there a syscall limit?
names, err1 := fd.Readdirnames(100)
for _, name := range names {
err1 := obj.RemoveAll(path + string(PathSeparator) + name)
if err == nil {
err = err1
}
}
if err1 == io.EOF {
break
}
// If Readdirnames returned an error, use it.
if err == nil {
err = err1
}
if len(names) == 0 {
break
}
}
// Close directory, because windows won't remove opened directory.
fd.Close()
// Remove directory.
err1 := obj.Remove(path)
if err1 == nil || os.IsNotExist(err1) {
return nil
}
if err == nil {
err = err1
}
return err
}
// Rename moves or renames a file or directory.
// TODO: seems it's okay to move files or directories, but you can't clobber dirs
// but you can clobber single files. a dir can't clobber a file and a file can't
// clobber a dir. but a file can clobber another file but a dir can't clobber
// another dir. you can also transplant dirs or files into other dirs.
func (obj *Fs) Rename(oldname, newname string) error {
// XXX: do we need to check if dest path is inside src path?
// XXX: if dirs/files are next to each other, do we mess up the .Children list of the common parent?
if err := obj.mount(); err != nil {
return err
}
if oldname == newname {
return nil
}
if oldname == "" || newname == "" {
return fmt.Errorf("invalid input path")
}
if !strings.HasPrefix(oldname, "/") || !strings.HasPrefix(newname, "/") {
return fmt.Errorf("invalid input path (not absolute)")
}
// remove possible trailing slashes
srcCleanPath := path.Clean(oldname)
dstCleanPath := path.Clean(newname)
src, err := obj.find(srcCleanPath) // get the file
if err != nil {
return err
}
srcInfo, err := src.Stat()
if err != nil {
return err
}
srcParentPath, srcName := path.Split(srcCleanPath) // looking for this
parent, err := obj.find(srcParentPath)
if err != nil { // might be ErrNotExist
return err
}
var rmi = -1 // index of node to remove from parent
// find the thing to be deleted
for i, n := range parent.Children {
if n.Path == srcName {
rmi = i // found here!
break
}
}
if rmi == -1 {
return fmt.Errorf("programming error")
}
dst, err := obj.find(dstCleanPath) // does the destination already exist?
if err != nil && !os.IsNotExist(err) {
return err
}
if err == nil { // dst exists!
dstInfo, err := dst.Stat()
if err != nil {
return err
}
// dir's can clobber anything or be clobbered apparently
if srcInfo.IsDir() || dstInfo.IsDir() {
return ErrExist // dir's can't clobber anything
}
// remove from list by index
parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...)
// we're a file clobbering another file...
// move file content from src -> dst and then delete src
// TODO: run a dst.Close() for extra safety first?
save := dst.Path // save the "name"
*dst = *src // TODO: is this safe?
dst.Path = save // "rename" it
} else { // dst does not exist
// check if the dst's parent exists and is a dir, if not, error
// if it is a dir, add src as a child to it and then delete src
dstParentPath, dstName := path.Split(dstCleanPath) // looking for this
node, err := obj.find(dstParentPath)
if err != nil { // might be ErrNotExist
return err
}
fi, err := node.Stat()
if err != nil {
return err
}
if !fi.IsDir() { // is the parent a suitable home?
return &os.LinkError{Op: "rename", Old: oldname, New: newname, Err: syscall.ENOTDIR}
}
// remove from list by index
parent.Children = append(parent.Children[:rmi], parent.Children[rmi+1:]...)
src.Path = dstName // "rename" it
node.Children = append(node.Children, src) // "copied"
}
return obj.sync() // push up metadata changes
}
// Stat returns some information about the particular path.
func (obj *Fs) Stat(name string) (os.FileInfo, error) {
if err := obj.mount(); err != nil {
return nil, err
}
if !strings.HasPrefix(name, "/") {
return nil, fmt.Errorf("invalid input path (not absolute)")
}
f, err := obj.find(name) // get the file
if err != nil {
return nil, err
}
return f.Stat()
}
// Lstat does exactly the same as Stat because we currently do not support
// symbolic links.
func (obj *Fs) Lstat(name string) (os.FileInfo, error) {
if err := obj.mount(); err != nil {
return nil, err
}
// TODO: we don't have symbolic links in our fs, so we pass this to stat
return obj.Stat(name)
}
// Chmod changes the mode of a file.
func (obj *Fs) Chmod(name string, mode os.FileMode) error {
if err := obj.mount(); err != nil {
return err
}
if !strings.HasPrefix(name, "/") {
return fmt.Errorf("invalid input path (not absolute)")
}
f, err := obj.find(name) // get the file
if err != nil {
return err
}
f.Mode = f.Mode | mode // XXX: what is the correct way to do this?
return f.Sync() // push up the changed metadata
}
// Chtimes changes the access and modification times of the named file, similar
// to the Unix utime() or utimes() functions. The underlying filesystem may
// truncate or round the values to a less precise time unit. If there is an
// error, it will be of type *PathError.
// FIXME: make sure everything we error is a *PathError
// TODO: atime is not currently implement and so it is silently ignored.
func (obj *Fs) Chtimes(name string, atime time.Time, mtime time.Time) error {
if err := obj.mount(); err != nil {
return err
}
if !strings.HasPrefix(name, "/") {
return fmt.Errorf("invalid input path (not absolute)")
}
f, err := obj.find(name) // get the file
if err != nil {
return err
}
f.ModTime = mtime
// TODO: add atime
return f.Sync() // push up the changed metadata
}
// PathSplit splits a path into an array of tokens excluding any trailing empty
// tokens.
func PathSplit(p string) []string {
if p == "/" { // TODO: can't this all be expressed nicely in one line?
return []string{""}
}
return strings.Split(path.Clean(p), "/")
}

227
etcd/fs/fs_test.go Normal file
View File

@@ -0,0 +1,227 @@
// 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 fs_test // named this way to make it easier for examples
import (
"io"
"testing"
"github.com/purpleidea/mgmt/etcd"
etcdfs "github.com/purpleidea/mgmt/etcd/fs"
"github.com/purpleidea/mgmt/util"
"github.com/spf13/afero"
)
// XXX: spawn etcd for this test, like `cdtmpmkdir && etcd` and then kill it...
// XXX: write a bunch more tests to test this
// TODO: apparently using 0666 is equivalent to respecting the current umask
const (
umask = 0666
superblock = "/some/superblock" // TODO: generate randomly per test?
)
func TestFs1(t *testing.T) {
etcdClient := &etcd.ClientEtcd{
Seeds: []string{"localhost:2379"}, // endpoints
}
if err := etcdClient.Connect(); err != nil {
t.Logf("client connection error: %+v", err)
return
}
defer etcdClient.Destroy()
etcdFs := &etcdfs.Fs{
Client: etcdClient.GetClient(),
Metadata: superblock,
DataPrefix: etcdfs.DefaultDataPrefix,
}
//var etcdFs afero.Fs = NewEtcdFs()
if err := etcdFs.Mkdir("/", umask); err != nil {
t.Logf("error: %+v", err)
if err != etcdfs.ErrExist {
return
}
}
if err := etcdFs.Mkdir("/tmp", umask); err != nil {
t.Logf("error: %+v", err)
if err != etcdfs.ErrExist {
return
}
}
fi, err := etcdFs.Stat("/tmp")
if err != nil {
t.Logf("stat error: %+v", err)
return
}
t.Logf("fi: %+v", fi)
t.Logf("isdir: %t", fi.IsDir())
f, err := etcdFs.Create("/tmp/foo")
if err != nil {
t.Logf("error: %+v", err)
return
}
t.Logf("handle: %+v", f)
i, err := f.WriteString("hello world!\n")
if err != nil {
t.Logf("error: %+v", err)
return
}
t.Logf("wrote: %d", i)
if err := etcdFs.Mkdir("/tmp/d1", umask); err != nil {
t.Logf("error: %+v", err)
if err != etcdfs.ErrExist {
return
}
}
if err := etcdFs.Rename("/tmp/foo", "/tmp/bar"); err != nil {
t.Logf("rename error: %+v", err)
return
}
//f2, err := etcdFs.Create("/tmp/bar")
//if err != nil {
// t.Logf("error: %+v", err)
// return
//}
//i2, err := f2.WriteString("hello bar!\n")
//if err != nil {
// t.Logf("error: %+v", err)
// return
//}
//t.Logf("wrote: %d", i2)
dir, err := etcdFs.Open("/tmp")
if err != nil {
t.Logf("error: %+v", err)
return
}
names, err := dir.Readdirnames(-1)
if err != nil && err != io.EOF {
t.Logf("error: %+v", err)
return
}
for _, name := range names {
t.Logf("name in /tmp: %+v", name)
}
//dir, err := etcdFs.Open("/")
//if err != nil {
// t.Logf("error: %+v", err)
// return
//}
//names, err := dir.Readdirnames(-1)
//if err != nil && err != io.EOF {
// t.Logf("error: %+v", err)
// return
//}
//for _, name := range names {
// t.Logf("name in /: %+v", name)
//}
}
func TestFs2(t *testing.T) {
etcdClient := &etcd.ClientEtcd{
Seeds: []string{"localhost:2379"}, // endpoints
}
if err := etcdClient.Connect(); err != nil {
t.Logf("client connection error: %+v", err)
return
}
defer etcdClient.Destroy()
etcdFs := &etcdfs.Fs{
Client: etcdClient.GetClient(),
Metadata: superblock,
DataPrefix: etcdfs.DefaultDataPrefix,
}
tree, err := util.FsTree(etcdFs, "/")
if err != nil {
t.Errorf("tree error: %+v", err)
return
}
t.Logf("tree: \n%s", tree)
tree2, err := util.FsTree(etcdFs, "/tmp")
if err != nil {
t.Errorf("tree2 error: %+v", err)
return
}
t.Logf("tree2: \n%s", tree2)
}
func TestFs3(t *testing.T) {
etcdClient := &etcd.ClientEtcd{
Seeds: []string{"localhost:2379"}, // endpoints
}
if err := etcdClient.Connect(); err != nil {
t.Logf("client connection error: %+v", err)
return
}
defer etcdClient.Destroy()
etcdFs := &etcdfs.Fs{
Client: etcdClient.GetClient(),
Metadata: superblock,
DataPrefix: etcdfs.DefaultDataPrefix,
}
tree, err := util.FsTree(etcdFs, "/")
if err != nil {
t.Errorf("tree error: %+v", err)
return
}
t.Logf("tree: \n%s", tree)
var memFs = afero.NewMemMapFs()
if err := util.CopyFs(etcdFs, memFs, "/", "/", false); err != nil {
t.Errorf("CopyFs error: %+v", err)
return
}
if err := util.CopyFs(etcdFs, memFs, "/", "/", true); err != nil {
t.Errorf("CopyFs2 error: %+v", err)
return
}
if err := util.CopyFs(etcdFs, memFs, "/", "/tmp/d1/", false); err != nil {
t.Errorf("CopyFs3 error: %+v", err)
return
}
tree2, err := util.FsTree(memFs, "/")
if err != nil {
t.Errorf("tree2 error: %+v", err)
return
}
t.Logf("tree2: \n%s", tree2)
}

88
etcd/fs/util.go Normal file
View File

@@ -0,0 +1,88 @@
// 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 fs
import (
"os"
"path/filepath"
"github.com/spf13/afero"
)
// ReadAll reads from r until an error or EOF and returns the data it read.
// A successful call returns err == nil, not err == EOF. Because ReadAll is
// defined to read from src until EOF, it does not treat an EOF from Read
// as an error to be reported.
//func ReadAll(r io.Reader) ([]byte, error) {
// return afero.ReadAll(r)
//}
// ReadDir reads the directory named by dirname and returns
// a list of sorted directory entries.
func (obj *Fs) ReadDir(dirname string) ([]os.FileInfo, error) {
return afero.ReadDir(obj, dirname)
}
// ReadFile reads the file named by filename and returns the contents.
// A successful call returns err == nil, not err == EOF. Because ReadFile
// reads the whole file, it does not treat an EOF from Read as an error
// to be reported.
func (obj *Fs) ReadFile(filename string) ([]byte, error) {
return afero.ReadFile(obj, filename)
}
// TempDir creates a new temporary directory in the directory dir
// with a name beginning with prefix and returns the path of the
// new directory. If dir is the empty string, TempDir uses the
// default directory for temporary files (see os.TempDir).
// Multiple programs calling TempDir simultaneously
// will not choose the same directory. It is the caller's responsibility
// to remove the directory when no longer needed.
func (obj *Fs) TempDir(dir, prefix string) (name string, err error) {
return afero.TempDir(obj, dir, prefix)
}
// TempFile creates a new temporary file in the directory dir
// with a name beginning with prefix, opens the file for reading
// and writing, and returns the resulting *File.
// If dir is the empty string, TempFile uses the default directory
// for temporary files (see os.TempDir).
// Multiple programs calling TempFile simultaneously
// will not choose the same file. The caller can use f.Name()
// to find the pathname of the file. It is the caller's responsibility
// to remove the file when no longer needed.
func (obj *Fs) TempFile(dir, prefix string) (f afero.File, err error) {
return afero.TempFile(obj, dir, prefix)
}
// WriteFile writes data to a file named by filename.
// If the file does not exist, WriteFile creates it with permissions perm;
// otherwise WriteFile truncates it before writing.
func (obj *Fs) WriteFile(filename string, data []byte, perm os.FileMode) error {
return afero.WriteFile(obj, filename, data, perm)
}
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order, which makes the output deterministic but means that for very
// large directories Walk can be inefficient.
// Walk does not follow symbolic links.
func (obj *Fs) Walk(root string, walkFn filepath.WalkFunc) error {
return afero.Walk(obj, root, walkFn)
}

30
etcd/interfaces.go Normal file
View File

@@ -0,0 +1,30 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd
import (
etcd "github.com/coreos/etcd/clientv3" // "clientv3"
)
// Client provides a simple interface specification for client requests. Both
// EmbdEtcd and ClientEtcd implement this.
type Client interface {
// TODO: add more method signatures
Get(path string, opts ...etcd.OpOption) (map[string]string, error)
Txn(ifcmps []etcd.Cmp, thenops, elseops []etcd.Op) (*etcd.TxnResponse, error)
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -39,7 +39,7 @@ func Nominate(obj *EmbdEtcd, hostname string, urls etcdtypes.URLs) error {
defer log.Printf("Trace: Etcd: Nominate(%v): Finished!", hostname)
}
// nominate someone to be a server
nominate := fmt.Sprintf("/%s/nominated/%s", NS, hostname)
nominate := fmt.Sprintf("%s/nominated/%s", NS, hostname)
ops := []etcd.Op{} // list of ops in this txn
if urls != nil {
ops = append(ops, etcd.OpPut(nominate, urls.String())) // TODO: add a TTL? (etcd.WithLease)
@@ -57,7 +57,7 @@ func Nominate(obj *EmbdEtcd, hostname string, urls etcdtypes.URLs) error {
// Nominated returns a urls map of nominated etcd server volunteers.
// NOTE: I know 'nominees' might be more correct, but is less consistent here
func Nominated(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
path := fmt.Sprintf("/%s/nominated/", NS)
path := fmt.Sprintf("%s/nominated/", NS)
keyMap, err := obj.Get(path, etcd.WithPrefix()) // map[string]string, bool
if err != nil {
return nil, fmt.Errorf("nominated isn't available: %v", err)
@@ -90,7 +90,7 @@ func Volunteer(obj *EmbdEtcd, urls etcdtypes.URLs) error {
defer log.Printf("Trace: Etcd: Volunteer(%v): Finished!", obj.hostname)
}
// volunteer to be a server
volunteer := fmt.Sprintf("/%s/volunteers/%s", NS, obj.hostname)
volunteer := fmt.Sprintf("%s/volunteers/%s", NS, obj.hostname)
ops := []etcd.Op{} // list of ops in this txn
if urls != nil {
// XXX: adding a TTL is crucial! (i think)
@@ -112,7 +112,7 @@ func Volunteers(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
log.Printf("Trace: Etcd: Volunteers()")
defer log.Printf("Trace: Etcd: Volunteers(): Finished!")
}
path := fmt.Sprintf("/%s/volunteers/", NS)
path := fmt.Sprintf("%s/volunteers/", NS)
keyMap, err := obj.Get(path, etcd.WithPrefix())
if err != nil {
return nil, fmt.Errorf("volunteers aren't available: %v", err)
@@ -145,7 +145,7 @@ func AdvertiseEndpoints(obj *EmbdEtcd, urls etcdtypes.URLs) error {
defer log.Printf("Trace: Etcd: AdvertiseEndpoints(%v): Finished!", obj.hostname)
}
// advertise endpoints
endpoints := fmt.Sprintf("/%s/endpoints/%s", NS, obj.hostname)
endpoints := fmt.Sprintf("%s/endpoints/%s", NS, obj.hostname)
ops := []etcd.Op{} // list of ops in this txn
if urls != nil {
// TODO: add a TTL? (etcd.WithLease)
@@ -167,7 +167,7 @@ func Endpoints(obj *EmbdEtcd) (etcdtypes.URLsMap, error) {
log.Printf("Trace: Etcd: Endpoints()")
defer log.Printf("Trace: Etcd: Endpoints(): Finished!")
}
path := fmt.Sprintf("/%s/endpoints/", NS)
path := fmt.Sprintf("%s/endpoints/", NS)
keyMap, err := obj.Get(path, etcd.WithPrefix())
if err != nil {
return nil, fmt.Errorf("endpoints aren't available: %v", err)
@@ -199,7 +199,7 @@ func SetHostnameConverged(obj *EmbdEtcd, hostname string, isConverged bool) erro
log.Printf("Trace: Etcd: SetHostnameConverged(%s): %v", hostname, isConverged)
defer log.Printf("Trace: Etcd: SetHostnameConverged(%v): Finished!", hostname)
}
converged := fmt.Sprintf("/%s/converged/%s", NS, hostname)
converged := fmt.Sprintf("%s/converged/%s", NS, hostname)
op := []etcd.Op{etcd.OpPut(converged, fmt.Sprintf("%t", isConverged))}
if _, err := obj.Txn(nil, op, nil); err != nil { // TODO: do we need a skipConv flag here too?
return fmt.Errorf("set converged failed") // exit in progress?
@@ -213,7 +213,7 @@ func HostnameConverged(obj *EmbdEtcd) (map[string]bool, error) {
log.Printf("Trace: Etcd: HostnameConverged()")
defer log.Printf("Trace: Etcd: HostnameConverged(): Finished!")
}
path := fmt.Sprintf("/%s/converged/", NS)
path := fmt.Sprintf("%s/converged/", NS)
keyMap, err := obj.ComplexGet(path, true, etcd.WithPrefix()) // don't un-converge
if err != nil {
return nil, fmt.Errorf("converged values aren't available: %v", err)
@@ -239,7 +239,7 @@ func HostnameConverged(obj *EmbdEtcd) (map[string]bool, error) {
// AddHostnameConvergedWatcher adds a watcher with a callback that runs on
// hostname state changes.
func AddHostnameConvergedWatcher(obj *EmbdEtcd, callbackFn func(map[string]bool) error) (func(), error) {
path := fmt.Sprintf("/%s/converged/", NS)
path := fmt.Sprintf("%s/converged/", NS)
internalCbFn := func(re *RE) error {
// TODO: get the value from the response, and apply delta...
// for now, just run a get operation which is easier to code!
@@ -258,7 +258,7 @@ func SetClusterSize(obj *EmbdEtcd, value uint16) error {
log.Printf("Trace: Etcd: SetClusterSize(): %v", value)
defer log.Printf("Trace: Etcd: SetClusterSize(): Finished!")
}
key := fmt.Sprintf("/%s/idealClusterSize", NS)
key := fmt.Sprintf("%s/idealClusterSize", NS)
if err := obj.Set(key, strconv.FormatUint(uint64(value), 10)); err != nil {
return fmt.Errorf("function SetClusterSize failed: %v", err) // exit in progress?
@@ -268,7 +268,7 @@ func SetClusterSize(obj *EmbdEtcd, value uint16) error {
// GetClusterSize gets the ideal target cluster size of etcd peers.
func GetClusterSize(obj *EmbdEtcd) (uint16, error) {
key := fmt.Sprintf("/%s/idealClusterSize", NS)
key := fmt.Sprintf("%s/idealClusterSize", NS)
keyMap, err := obj.Get(key)
if err != nil {
return 0, fmt.Errorf("function GetClusterSize failed: %v", err)
@@ -371,9 +371,8 @@ func Members(obj *EmbdEtcd) (map[uint64]string, error) {
// Leader returns the current leader of the etcd server cluster.
func Leader(obj *EmbdEtcd) (string, error) {
//obj.Connect(false) // TODO: ?
var err error
membersMap := make(map[uint64]string)
if membersMap, err = Members(obj); err != nil {
membersMap, err := Members(obj)
if err != nil {
return "", err
}
addresses := obj.LocalhostClientURLs() // heuristic, but probably correct

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -34,7 +34,7 @@ import (
// collection prefixes and filters that we care about...
func WatchResources(obj *EmbdEtcd) chan error {
ch := make(chan error, 1) // buffer it so we can measure it
path := fmt.Sprintf("/%s/exported/", NS)
path := fmt.Sprintf("%s/exported/", NS)
callback := func(re *RE) error {
// TODO: is this even needed? it used to happen on conn errors
log.Printf("Etcd: Watch: Path: %v", path) // event
@@ -61,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 {
// key structure is /$NS/exported/$hostname/resources/$uid = $data
// key structure is $NS/exported/$hostname/resources/$uid = $data
var kindFilter []string // empty to get from everyone
hostnameFilter := []string{hostname}
@@ -83,7 +83,7 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
}
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName())
path := fmt.Sprintf("/%s/exported/%s/resources/%s", NS, hostname, uid)
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
if data, err := resources.ResToB64(res); err == nil {
ifs = append(ifs, etcd.Compare(etcd.Value(path), "=", data)) // desired state
ops = append(ops, etcd.OpPut(path, data))
@@ -108,7 +108,7 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
log.Fatalf("Etcd: SetResources: Error: Empty kind: %v", res.GetName())
}
uid := fmt.Sprintf("%s/%s", res.GetKind(), res.GetName())
path := fmt.Sprintf("/%s/exported/%s/resources/%s", NS, hostname, uid)
path := fmt.Sprintf("%s/exported/%s/resources/%s", NS, hostname, uid)
if match(res, resourceList) { // if we match, no need to delete!
continue
@@ -134,10 +134,10 @@ func SetResources(obj *EmbdEtcd, hostname string, resourceList []resources.Res)
// If the kindfilter or hostnameFilter is empty, then it assumes no filtering...
// 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.
// We could do this if the pattern was $NS/exported/$kind/$hostname/$uid = $data.
func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resources.Res, error) {
// key structure is /$NS/exported/$hostname/resources/$uid = $data
path := fmt.Sprintf("/%s/exported/", NS)
// key structure is $NS/exported/$hostname/resources/$uid = $data
path := fmt.Sprintf("%s/exported/", NS)
resourceList := []resources.Res{}
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
if err != nil {

View File

@@ -0,0 +1,49 @@
// 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 scheduler // TODO: i'd like this to be a separate package, but cycles!
import (
"fmt"
"sort"
)
func init() {
Register("alpha", func() Strategy { return &alphaStrategy{} }) // must register the func and name
}
type alphaStrategy struct {
// no state to store
}
// Schedule returns the first host out of a sorted group of available hostnames.
func (obj *alphaStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
if len(hostnames) <= 0 {
return nil, fmt.Errorf("strategy: cannot schedule from zero hosts")
}
if opts.maxCount <= 0 {
return nil, fmt.Errorf("strategy: cannot schedule with a max of zero")
}
sortedHosts := []string{}
for key := range hostnames {
sortedHosts = append(sortedHosts, key)
}
sort.Strings(sortedHosts)
return []string{sortedHosts[0]}, nil // pick first host
}

100
etcd/scheduler/options.go Normal file
View File

@@ -0,0 +1,100 @@
// 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 scheduler
import (
"fmt"
)
// Option is a type that can be used to configure the scheduler.
type Option func(*schedulerOptions)
// schedulerOptions represents the different possible configurable options. Not
// all options necessarily work for each scheduler strategy algorithm.
type schedulerOptions struct {
debug bool
logf func(format string, v ...interface{})
strategy Strategy
maxCount int // TODO: should this be *int to know when it's set?
reuseLease bool
sessionTTL int // TODO: should this be *int to know when it's set?
hostsFilter []string
// TODO: add more options
}
// Debug specifies whether we should run in debug mode or not.
func Debug(debug bool) Option {
return func(so *schedulerOptions) {
so.debug = debug
}
}
// Logf passes a logger function that we can use if so desired.
func Logf(logf func(format string, v ...interface{})) Option {
return func(so *schedulerOptions) {
so.logf = logf
}
}
// StrategyKind sets the scheduler strategy used.
func StrategyKind(strategy string) Option {
return func(so *schedulerOptions) {
f, exists := registeredStrategies[strategy]
if !exists {
panic(fmt.Sprintf("scheduler: undefined strategy: %s", strategy))
}
so.strategy = f()
}
}
// MaxCount is the maximum number of hosts that should get simultaneously
// scheduled.
func MaxCount(maxCount int) Option {
return func(so *schedulerOptions) {
if maxCount > 0 {
so.maxCount = maxCount
}
}
}
// ReuseLease specifies whether we should try and re-use the lease between runs.
// Ordinarily it would get discarded with each new version (deploy) of the code.
func ReuseLease(reuseLease bool) Option {
return func(so *schedulerOptions) {
so.reuseLease = reuseLease
}
}
// SessionTTL is the amount of time to delay before expiring a key on abrupt
// host disconnect of if ReuseLease is true.
func SessionTTL(sessionTTL int) Option {
return func(so *schedulerOptions) {
if sessionTTL > 0 {
so.sessionTTL = sessionTTL
}
}
}
// HostsFilter specifies a manual list of hosts, to use as a subset of whatever
// was auto-discovered.
// XXX: think more about this idea...
func HostsFilter(hosts []string) Option {
return func(so *schedulerOptions) {
so.hostsFilter = hosts
}
}

View File

@@ -0,0 +1,84 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package scheduler // TODO: i'd like this to be a separate package, but cycles!
import (
"fmt"
"sort"
"github.com/purpleidea/mgmt/util"
)
func init() {
Register("rr", func() Strategy { return &rrStrategy{} }) // must register the func and name
}
type rrStrategy struct {
// some stored state
hosts []string
}
// Schedule returns hosts in round robin style from the available hostnames.
func (obj *rrStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
if len(hostnames) <= 0 {
return nil, fmt.Errorf("strategy: cannot schedule from zero hosts")
}
if opts.maxCount <= 0 {
return nil, fmt.Errorf("strategy: cannot schedule with a max of zero")
}
// always get a deterministic list of current hosts first...
sortedHosts := []string{}
for key := range hostnames {
sortedHosts = append(sortedHosts, key)
}
sort.Strings(sortedHosts)
if obj.hosts == nil {
obj.hosts = []string{} // initialize if needed
}
// add any new hosts we learned about, to the end of the list
for _, x := range sortedHosts {
if !util.StrInList(x, obj.hosts) {
obj.hosts = append(obj.hosts, x)
}
}
// remove any hosts we previouly knew about from the list
for ix := len(obj.hosts) - 1; ix >= 0; ix-- {
if !util.StrInList(obj.hosts[ix], sortedHosts) {
// delete entry at this index
obj.hosts = append(obj.hosts[:ix], obj.hosts[ix+1:]...)
}
}
// get the maximum number of hosts to return
max := len(obj.hosts) // can't return more than we have
if opts.maxCount < max { // found a smaller limit
max = opts.maxCount
}
result := []string{}
// now return the number of needed hosts from the list
for i := 0; i < max; i++ {
result = append(result, obj.hosts[i])
}
return result, nil
}

564
etcd/scheduler/scheduler.go Normal file
View File

@@ -0,0 +1,564 @@
// 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 scheduler implements a distributed consensus scheduler with etcd.
package scheduler
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"sync"
etcd "github.com/coreos/etcd/clientv3"
"github.com/coreos/etcd/clientv3/concurrency"
errwrap "github.com/pkg/errors"
)
const (
// DefaultSessionTTL is the number of seconds to wait before a dead or
// unresponsive host is removed from the scheduled pool.
DefaultSessionTTL = 10 // seconds
// DefaultMaxCount is the maximum number of hosts to schedule on if not
// specified.
DefaultMaxCount = 1 // TODO: what is the logical value to choose? +Inf?
hostnameJoinChar = "," // char used to join and split lists of hostnames
)
// ErrEndOfResults is a sentinel that represents no more results will be coming.
var ErrEndOfResults = errors.New("scheduler: end of results")
var schedulerLeases = make(map[string]etcd.LeaseID) // process lifetime in-memory lease store
// schedulerResult represents output from the scheduler.
type schedulerResult struct {
hosts []string
err error
}
// Result is what is returned when you request a scheduler. You can call methods
// on it, and it stores the necessary state while you're running. When one of
// these is produced, the scheduler has already kicked off running for you
// automatically.
type Result struct {
results chan *schedulerResult
closeFunc func() // run this when you're done with the scheduler // TODO: replace with an input `context`
}
// Next returns the next output from the scheduler when it changes. This blocks
// until a new value is available, which is why you may wish to use a context to
// cancel any read from this. It returns ErrEndOfResults if the scheduler shuts
// down.
func (obj *Result) Next(ctx context.Context) ([]string, error) {
select {
case val, ok := <-obj.results:
if !ok {
return nil, ErrEndOfResults
}
return val.hosts, val.err
case <-ctx.Done():
return nil, ctx.Err()
}
}
// Shutdown causes everything to clean up. We no longer need the scheduler.
// TODO: should this be named Close() instead? Should it return an error?
func (obj *Result) Shutdown() {
obj.closeFunc()
// XXX: should we have a waitgroup to wait for it all to close?
}
// Schedule returns a scheduler result which can be queried with it's available
// methods. This automatically causes different etcd clients sharing the same
// path to discover each other and be part of the scheduled set. On close the
// keys expire and will get removed from the scheduled set. Different options
// can be passed in to customize the behaviour. Hostname represents the unique
// identifier for the caller. The behaviour is undefined if this is run more
// than once with the same path and hostname simultaneously.
func Schedule(client *etcd.Client, path string, hostname string, opts ...Option) (*Result, error) {
if strings.HasSuffix(path, "/") {
return nil, fmt.Errorf("scheduler: path must not end with the slash char")
}
if !strings.HasPrefix(path, "/") {
return nil, fmt.Errorf("scheduler: path must start with the slash char")
}
if hostname == "" {
return nil, fmt.Errorf("scheduler: hostname must not be empty")
}
if strings.Contains(hostname, hostnameJoinChar) {
return nil, fmt.Errorf("scheduler: hostname must not contain join char: %s", hostnameJoinChar)
}
// key structure is $path/election = ???
// key structure is $path/exchange/$hostname = ???
// key structure is $path/scheduled = ???
options := &schedulerOptions{ // default scheduler options
// If reuseLease is false, then on host disconnect, that hosts
// entry will immediately expire, and the scheduler will react
// instantly and remove that host entry from the list. If this
// is true, or if the host closes without a clean shutdown, it
// will take the TTL number of seconds to remove the key. This
// can be set using the concurrency.WithTTL option to Session.
reuseLease: false,
sessionTTL: DefaultSessionTTL,
maxCount: DefaultMaxCount,
}
for _, optionFunc := range opts { // apply the scheduler options
optionFunc(options)
}
if options.strategy == nil {
return nil, fmt.Errorf("scheduler: strategy must be specified")
}
sessionOptions := []concurrency.SessionOption{}
// here we try to re-use lease between multiple runs of the code
// TODO: is it a good idea to try and re-use the lease b/w runs?
if options.reuseLease {
if leaseID, exists := schedulerLeases[path]; exists {
sessionOptions = append(sessionOptions, concurrency.WithLease(leaseID))
}
}
// ttl for key expiry on abrupt disconnection or if reuseLease is true!
if options.sessionTTL > 0 {
sessionOptions = append(sessionOptions, concurrency.WithTTL(options.sessionTTL))
}
//options.debug = true // use this for local debugging
session, err := concurrency.NewSession(client, sessionOptions...)
if err != nil {
return nil, errwrap.Wrapf(err, "scheduler: could not create session")
}
leaseID := session.Lease()
if options.reuseLease {
// save for next time, otherwise run session.Close() somewhere
schedulerLeases[path] = leaseID
}
ctx, cancel := context.WithCancel(context.Background()) // cancel below
//defer cancel() // do NOT do this, as it would cause an early cancel!
// stored scheduler results
scheduledPath := fmt.Sprintf("%s/scheduled", path)
scheduledChan := client.Watcher.Watch(ctx, scheduledPath)
// exchange hostname, and attach it to session (leaseID) so it expires
// (gets deleted) when we disconnect...
exchangePath := fmt.Sprintf("%s/exchange", path)
exchangePathHost := fmt.Sprintf("%s/%s", exchangePath, hostname)
exchangePathPrefix := fmt.Sprintf("%s/", exchangePath)
// open the watch *before* we set our key so that we can see the change!
watchChan := client.Watcher.Watch(ctx, exchangePathPrefix, etcd.WithPrefix())
data := "TODO" // XXX: no data to exchange alongside hostnames yet
ifops := []etcd.Cmp{
etcd.Compare(etcd.Value(exchangePathHost), "=", data),
etcd.Compare(etcd.LeaseValue(exchangePathHost), "=", leaseID),
}
elsop := etcd.OpPut(exchangePathHost, data, etcd.WithLease(leaseID))
// it's important to do this in one transaction, and atomically, because
// this way, we only generate one watch event, and only when it's needed
// updating leaseID, or key expiry (deletion) both generate watch events
// XXX: context!!!
if txn, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil {
defer cancel() // cancel to avoid leaks if we exit early...
return nil, errwrap.Wrapf(err, "could not exchange in `%s`", path)
} else if txn.Succeeded {
options.logf("txn did nothing...") // then branch
} else {
options.logf("txn did an update...")
}
// create an election object
electionPath := fmt.Sprintf("%s/election", path)
election := concurrency.NewElection(session, electionPath)
electionChan := election.Observe(ctx)
elected := "" // who we "assume" is elected
wg := &sync.WaitGroup{}
ch := make(chan *schedulerResult)
closeChan := make(chan struct{})
send := func(hosts []string, err error) bool { // helper function for sending
select {
case ch <- &schedulerResult{ // send
hosts: hosts,
err: err,
}:
return true
case <-closeChan: // unblock
return false // not sent
}
}
once := &sync.Once{}
onceBody := func() { // do not call directly, use closeFunc!
//cancel() // TODO: is this needed here?
// request a graceful shutdown, caller must call this to
// shutdown when they are finished with the scheduler...
// calling this will cause their hosts channels to close
close(closeChan) // send a close signal
}
closeFunc := func() {
once.Do(onceBody)
}
result := &Result{
results: ch,
// TODO: we could accept a context to watch for cancel instead?
closeFunc: closeFunc,
}
mutex := &sync.Mutex{}
var campaignClose chan struct{}
var campaignRunning bool
// goroutine to vote for someone as scheduler! each participant must be
// able to run this or nobody will be around to vote if others are down
campaignFunc := func() {
options.logf("starting campaign...")
// the mutex ensures we don't fly past the wg.Wait() if someone
// shuts down the scheduler right as we are about to start this
// campaigning loop up. we do not want to fail unnecessarily...
mutex.Lock()
wg.Add(1)
mutex.Unlock()
go func() {
defer wg.Done()
ctx, cancel := context.WithCancel(context.Background())
go func() {
defer cancel() // run cancel to stop campaigning...
select {
case <-campaignClose:
return
case <-closeChan:
return
}
}()
for {
// TODO: previously, this looped infinitely fast
// TODO: add some rate limiting here for initial
// campaigning which occasionally loops a lot...
if options.debug {
//fmt.Printf(".") // debug
options.logf("campaigning...")
}
// "Campaign puts a value as eligible for the election.
// It blocks until it is elected, an error occurs, or
// the context is cancelled."
// vote for ourselves, as it's the only host we can
// guarantee is alive, otherwise we wouldn't be voting!
// it would be more sensible to vote for the last valid
// hostname to keep things more stable, but if that
// information was stale, and that host wasn't alive,
// then this would defeat the point of picking them!
if err := election.Campaign(ctx, hostname); err != nil {
if err != context.Canceled {
send(nil, errwrap.Wrapf(err, "scheduler: error campaigning"))
}
return
}
}
}()
}
go func() {
defer close(ch)
if !options.reuseLease {
defer session.Close() // this revokes the lease...
}
defer func() {
// XXX: should we ever resign? why would this block and thus need a context?
if elected == hostname { // TODO: is it safe to just always do this?
if err := election.Resign(context.TODO()); err != nil { // XXX: add a timeout?
}
}
elected = "" // we don't care anymore!
}()
// this "last" defer (first to run) should block until the other
// goroutine has closed so we don't Close an in-use session, etc
defer wg.Wait()
go func() {
defer cancel() // run cancel to "free" Observe...
defer wg.Wait() // also wait here if parent exits first
select {
case <-closeChan:
// we want the above wg.Wait() to work if this
// close happens. lock with the campaign start
defer mutex.Unlock()
mutex.Lock()
return
}
}()
hostnames := make(map[string]string)
for {
select {
case val, ok := <-electionChan:
if options.debug {
options.logf("electionChan(%t): %+v", ok, val)
}
if !ok {
if options.debug {
options.logf("elections stream shutdown...")
}
electionChan = nil
// done
// TODO: do we need to send on error channel?
// XXX: maybe if context was not called to exit us?
// ensure everyone waiting on closeChan
// gets cleaned up so we free mem, etc!
if watchChan == nil && scheduledChan == nil { // all now closed
closeFunc()
return
}
continue
}
elected = string(val.Kvs[0].Value)
//if options.debug {
options.logf("elected: %s", elected)
//}
if elected != hostname { // not me!
// start up the campaign function
if !campaignRunning {
campaignClose = make(chan struct{})
campaignFunc() // run
campaignRunning = true
}
continue // someone else does the scheduling...
} else { // campaigning while i am it loops fast
// shutdown the campaign function
if campaignRunning {
close(campaignClose)
wg.Wait()
campaignRunning = false
}
}
// i was voted in to make the scheduling choice!
case watchResp, ok := <-watchChan:
if options.debug {
options.logf("watchChan(%t): %+v", ok, watchResp)
}
if !ok {
if options.debug {
options.logf("watch stream shutdown...")
}
watchChan = nil
// done
// TODO: do we need to send on error channel?
// XXX: maybe if context was not called to exit us?
// ensure everyone waiting on closeChan
// gets cleaned up so we free mem, etc!
if electionChan == nil && scheduledChan == nil { // all now closed
closeFunc()
return
}
continue
}
err := watchResp.Err()
if watchResp.Canceled || err == context.Canceled {
// channel get closed shortly...
continue
}
if watchResp.Header.Revision == 0 { // by inspection
// received empty message ?
// switched client connection ?
// FIXME: what should we do here ?
continue
}
if err != nil {
send(nil, errwrap.Wrapf(err, "scheduler: exchange watcher failed"))
continue
}
if len(watchResp.Events) == 0 { // nothing interesting
continue
}
options.logf("running exchange values get...")
resp, err := client.Get(ctx, exchangePathPrefix, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
if err != nil || resp == nil {
if err != nil {
send(nil, errwrap.Wrapf(err, "scheduler: could not get exchange values in `%s`", path))
} else { // if resp == nil
send(nil, fmt.Errorf("scheduler: could not get exchange values in `%s`, resp is nil", path))
}
continue
}
// FIXME: the value key could instead be host
// specific information which is used for some
// purpose, eg: seconds active, and other data?
hostnames = make(map[string]string) // reset
for _, x := range resp.Kvs {
k := string(x.Key)
if !strings.HasPrefix(k, exchangePathPrefix) {
continue
}
k = k[len(exchangePathPrefix):] // strip
hostnames[k] = string(x.Value)
}
if options.debug {
options.logf("available hostnames: %+v", hostnames)
}
case scheduledResp, ok := <-scheduledChan:
if options.debug {
options.logf("scheduledChan(%t): %+v", ok, scheduledResp)
}
if !ok {
if options.debug {
options.logf("scheduled stream shutdown...")
}
scheduledChan = nil
// done
// TODO: do we need to send on error channel?
// XXX: maybe if context was not called to exit us?
// ensure everyone waiting on closeChan
// gets cleaned up so we free mem, etc!
if electionChan == nil && watchChan == nil { // all now closed
closeFunc()
return
}
continue
}
// event! continue below and get new result...
// NOTE: not needed, exit this via Observe ctx cancel,
// which will ultimately cause the chan to shutdown...
//case <-closeChan:
// return
} // end select
if len(hostnames) == 0 {
if options.debug {
options.logf("zero hosts available")
}
continue // not enough hosts available
}
// if we're currently elected, make a scheduling decision
// if not, lookup the existing leader scheduling decision
if elected != hostname {
options.logf("i am not the leader, running scheduling result get...")
resp, err := client.Get(ctx, scheduledPath)
if err != nil || resp == nil || len(resp.Kvs) != 1 {
if err != nil {
send(nil, errwrap.Wrapf(err, "scheduler: could not get scheduling result in `%s`", path))
} else if resp == nil {
send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp is nil", path))
} else if len(resp.Kvs) > 1 {
send(nil, fmt.Errorf("scheduler: could not get scheduling result in `%s`, resp kvs: %+v", path, resp.Kvs))
}
// if len(resp.Kvs) == 0, we shouldn't error
// in that situation it's just too early...
continue
}
result := string(resp.Kvs[0].Value)
hosts := strings.Split(result, hostnameJoinChar)
if options.debug {
options.logf("sending hosts: %+v", hosts)
}
// send that on channel!
if !send(hosts, nil) {
//return // pass instead, let channels clean up
}
continue
}
// i am the leader, run scheduler and store result
options.logf("i am elected, running scheduler...")
// run actual scheduler and decide who should be chosen
// TODO: is there any additional data that we can pass
// to the scheduler so it can make a better decision ?
hosts, err := options.strategy.Schedule(hostnames, options)
if err != nil {
send(nil, errwrap.Wrapf(err, "scheduler: strategy failed"))
continue
}
sort.Strings(hosts) // for consistency
options.logf("storing scheduling result...")
data := strings.Join(hosts, hostnameJoinChar)
ifops := []etcd.Cmp{
etcd.Compare(etcd.Value(scheduledPath), "=", data),
}
elsop := etcd.OpPut(scheduledPath, data)
// it's important to do this in one transaction, and atomically, because
// this way, we only generate one watch event, and only when it's needed
// updating leaseID, or key expiry (deletion) both generate watch events
// XXX: context!!!
if _, err := client.KV.Txn(context.TODO()).If(ifops...).Then([]etcd.Op{}...).Else(elsop).Commit(); err != nil {
send(nil, errwrap.Wrapf(err, "scheduler: could not set scheduling result in `%s`", path))
continue
}
if options.debug {
options.logf("sending hosts: %+v", hosts)
}
// send that on channel!
if !send(hosts, nil) {
//return // pass instead, let channels clean up
}
}
}()
// kick off an initial campaign if none exist already...
options.logf("checking for existing leader...")
leaderResult, err := election.Leader(ctx)
if err == concurrency.ErrElectionNoLeader {
// start up the campaign function
if !campaignRunning {
campaignClose = make(chan struct{})
campaignFunc() // run
campaignRunning = true
}
}
if options.debug {
if err != nil {
options.logf("leader information error: %+v", err)
} else {
options.logf("leader information: %+v", leaderResult)
}
}
return result, nil
}

View File

@@ -0,0 +1,51 @@
// 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 scheduler
import (
"fmt"
)
// registeredStrategies is a global map of all possible strategy implementations
// which can be used. You should never touch this map directly. Use methods like
// Register instead.
var registeredStrategies = make(map[string]func() Strategy) // must initialize
// Strategy represents the methods a scheduler strategy must implement.
type Strategy interface {
Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error)
}
// Register takes a func and its name and makes it available for use. It is
// commonly called in the init() method of the func at program startup. There is
// no matching Unregister function.
func Register(name string, fn func() Strategy) {
if _, ok := registeredStrategies[name]; ok {
panic(fmt.Sprintf("a strategy named %s is already registered", name))
}
//gob.Register(fn())
registeredStrategies[name] = fn
}
type nilStrategy struct {
}
// Schedule returns an error for any scheduling request for this nil strategy.
func (obj *nilStrategy) Schedule(hostnames map[string]string, opts *schedulerOptions) ([]string, error) {
return nil, fmt.Errorf("scheduler: cannot schedule with nil scheduler")
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -32,9 +32,13 @@ var ErrNotExist = errors.New("errNotExist")
// WatchStr returns a channel which spits out events on key activity.
// FIXME: It should close the channel when it's done, and spit out errors when
// something goes wrong.
// XXX: since the caller of this (via the World API) has no way to tell it it's
// done, does that mean we leak go-routines since it might still be running, but
// perhaps even blocked??? Could this cause a dead-lock? Should we instead return
// some sort of struct which has a close method with it to ask for a shutdown?
func WatchStr(obj *EmbdEtcd, key string) chan error {
// new key structure is /$NS/strings/$key = $data
path := fmt.Sprintf("/%s/strings/%s", NS, key)
// new key structure is $NS/strings/$key = $data
path := fmt.Sprintf("%s/strings/%s", NS, key)
ch := make(chan error, 1)
// FIXME: fix our API so that we get a close event on shutdown.
callback := func(re *RE) error {
@@ -54,8 +58,8 @@ func WatchStr(obj *EmbdEtcd, key string) chan error {
// GetStr collects the string which matches a global namespace in etcd.
func GetStr(obj *EmbdEtcd, key string) (string, error) {
// new key structure is /$NS/strings/$key = $data
path := fmt.Sprintf("/%s/strings/%s", NS, key)
// new key structure is $NS/strings/$key = $data
path := fmt.Sprintf("%s/strings/%s", NS, key)
keyMap, err := obj.Get(path, etcd.WithPrefix())
if err != nil {
return "", errwrap.Wrapf(err, "could not get strings in: %s", key)
@@ -82,8 +86,8 @@ func GetStr(obj *EmbdEtcd, key string) (string, error) {
// nil, then it deletes the key. Otherwise the value should point to a string.
// TODO: TTL or delete disconnect?
func SetStr(obj *EmbdEtcd, key string, data *string) error {
// key structure is /$NS/strings/$key = $data
path := fmt.Sprintf("/%s/strings/%s", NS, key)
// key structure is $NS/strings/$key = $data
path := fmt.Sprintf("%s/strings/%s", NS, key)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this transaction (then)
els := []etcd.Op{} // list of ops in this transaction (else)

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -31,8 +31,8 @@ import (
// FIXME: It should close the channel when it's done, and spit out errors when
// something goes wrong.
func WatchStrMap(obj *EmbdEtcd, key string) chan error {
// new key structure is /$NS/strings/$key/$hostname = $data
path := fmt.Sprintf("/%s/strings/%s", NS, key)
// new key structure is $NS/strings/$key/$hostname = $data
path := fmt.Sprintf("%s/strings/%s", NS, key)
ch := make(chan error, 1)
// FIXME: fix our API so that we get a close event on shutdown.
callback := func(re *RE) error {
@@ -52,12 +52,12 @@ func WatchStrMap(obj *EmbdEtcd, key string) chan error {
// GetStrMap collects all of the strings which match a namespace in etcd.
func GetStrMap(obj *EmbdEtcd, hostnameFilter []string, key string) (map[string]string, error) {
// old key structure is /$NS/strings/$hostname/$key = $data
// new key structure is /$NS/strings/$key/$hostname = $data
// old key structure is $NS/strings/$hostname/$key = $data
// new key structure is $NS/strings/$key/$hostname = $data
// FIXME: if we have the $key as the last token (old key structure), we
// can allow the key to contain the slash char, otherwise we need to
// verify that one isn't present in the input string.
path := fmt.Sprintf("/%s/strings/%s", NS, key)
path := fmt.Sprintf("%s/strings/%s", NS, key)
keyMap, err := obj.Get(path, etcd.WithPrefix(), etcd.WithSort(etcd.SortByKey, etcd.SortAscend))
if err != nil {
return nil, errwrap.Wrapf(err, "could not get strings in: %s", key)
@@ -92,8 +92,8 @@ func GetStrMap(obj *EmbdEtcd, hostnameFilter []string, key string) (map[string]s
// nil, then it deletes the key. Otherwise the value should point to a string.
// TODO: TTL or delete disconnect?
func SetStrMap(obj *EmbdEtcd, hostname, key string, data *string) error {
// key structure is /$NS/strings/$key/$hostname = $data
path := fmt.Sprintf("/%s/strings/%s/%s", NS, key, hostname)
// key structure is $NS/strings/$key/$hostname = $data
path := fmt.Sprintf("%s/strings/%s/%s", NS, key, hostname)
ifs := []etcd.Cmp{} // list matching the desired state
ops := []etcd.Op{} // list of ops in this transaction (then)
els := []etcd.Op{} // list of ops in this transaction (else)

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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
@@ -18,13 +18,24 @@
package etcd
import (
"fmt"
"net/url"
"strings"
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
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
Debug bool
Logf func(format string, v ...interface{})
}
// ResWatch returns a channel which spits out events on possible exported
@@ -93,3 +104,49 @@ func (obj *World) StrMapSet(namespace, value string) error {
func (obj *World) StrMapDel(namespace string) error {
return SetStrMap(obj.EmbdEtcd, obj.Hostname, namespace, nil)
}
// Scheduler returns a scheduling result of hosts in a particular namespace.
func (obj *World) Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error) {
modifiedOpts := []scheduler.Option{}
for _, o := range opts {
modifiedOpts = append(modifiedOpts, o) // copy in
}
modifiedOpts = append(modifiedOpts, scheduler.Debug(obj.Debug))
modifiedOpts = append(modifiedOpts, scheduler.Logf(obj.Logf))
return scheduler.Schedule(obj.EmbdEtcd.GetClient(), fmt.Sprintf("%s/scheduler/%s", NS, namespace), obj.Hostname, modifiedOpts...)
}
// Fs returns a distributed file system from a unique URI. For single host
// 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) {
u, err := url.Parse(uri)
if err != nil {
return nil, err
}
// we're in standalone mode
if u.Scheme == "memmapfs" && u.Path == "/" {
return obj.StandaloneFs, nil
}
if u.Scheme != "etcdfs" {
return nil, fmt.Errorf("unknown scheme: `%s`", u.Scheme)
}
if u.Path == "" {
return nil, fmt.Errorf("empty path: %s", u.Path)
}
if !strings.HasPrefix(u.Path, obj.MetadataPrefix) {
return nil, fmt.Errorf("wrong path prefix: %s", u.Path)
}
etcdFs := &etcdfs.Fs{
Client: obj.EmbdEtcd.GetClient(),
Metadata: u.Path,
DataPrefix: obj.StoragePrefix,
}
return etcdFs, nil
}

View File

@@ -1,5 +1,5 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// 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

4
examples/lang/answer.mcl Normal file
View File

@@ -0,0 +1,4 @@
# it was a lovely surprise to me, when i realized that mgmt had the answer!
print "answer" {
msg => printf("the answer to life, the universe, and everything is: %d", answer()),
}

View File

@@ -0,0 +1,22 @@
$set = ["a", "b", "c", "d",]
$c1 = "x1" in ["x1", "x2", "x3",]
$c2 = 42 in [4, 13, 42,]
$c3 = "x" in $set
$c4 = "b" in $set
$s = printf("1: %t, 2: %t, 3: %t, 4: %t\n", $c1, $c2, $c3, $c4)
file "/tmp/mgmt/contains" {
content => $s,
}
$x = if hostname() in ["h1", "h3",] {
printf("i (%s) am one of the chosen few!\n", hostname())
} else {
printf("i (%s) was not chosen :(\n", hostname())
}
file "/tmp/mgmt/hello-${hostname()}" {
content => $x,
}

View File

@@ -0,0 +1,4 @@
$d = datetime()
file "/tmp/mgmt/datetime" {
content => template("Hello! It is now: {{ datetime_print . }}\n", $d),
}

View File

@@ -0,0 +1,14 @@
$secplusone = datetime() + $ayear
# note the order of the assignment (year can come later in the code)
$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000)
$tmplvalues = struct{year => $secplusone, load => $theload,}
$theload = structlookup(load(), "x1")
if 5 > 3 {
file "/tmp/mgmt/datetime" {
content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n", $tmplvalues),
}
}

View File

@@ -0,0 +1,14 @@
$secplusone = datetime() + $ayear
# note the order of the assignment (year can come later in the code)
$ayear = 60 * 60 * 24 * 365 # is a year in seconds (31536000)
$tmplvalues = struct{year => $secplusone, load => $theload, vumeter => $vumeter,}
$theload = structlookup(load(), "x1")
$vumeter = vumeter("====", 10, 0.9)
file "/tmp/mgmt/datetime" {
content => template("Now + 1 year is: {{ .year }} seconds, aka: {{ datetime_print .year }}\n\nload average: {{ .load }}\n\nvu: {{ .vumeter }}\n", $tmplvalues),
}

14
examples/lang/edges0.mcl Normal file
View File

@@ -0,0 +1,14 @@
exec "exec0" {
cmd => "sleep 10s",
shell => "/bin/bash",
}
exec "exec1" {
cmd => "sleep 10s",
shell => "/bin/bash",
}
exec "exec2" {
cmd => "sleep 10s",
shell => "/bin/bash",
}
Exec["exec0"] -> Exec["exec1"] -> Exec["exec2"]

18
examples/lang/edges1.mcl Normal file
View File

@@ -0,0 +1,18 @@
$b = true
if $b {
exec "exec0" {
cmd => "sleep 10s",
shell => "/bin/bash",
}
}
exec "exec1" {
cmd => "sleep 10s",
shell => "/bin/bash",
Depend => $b ?: Exec["exec0"],
Before => Exec["exec2"],
}
exec "exec2" {
cmd => "sleep 10s",
shell => "/bin/bash",
}

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

@@ -0,0 +1,5 @@
$b = true # change me to false and then try editing the file manually
file "/tmp/mgmt-elvis" {
content => $b ?: "hello world\n",
state => "exists",
}

20
examples/lang/env0.mcl Normal file
View File

@@ -0,0 +1,20 @@
# read and print environment variable
# env TEST=123 EMPTY= ./mgmt run --tmp-prefix --lang=examples/lang/env0.mcl --converged-timeout=5
$x = getenv("TEST", "321")
print "print1" {
msg => printf("the value of the environment variable TEST is: %s", $x),
}
$y = getenv("DOESNOTEXIT", "321")
print "print2" {
msg => printf("environment variable DOESNOTEXIT does not exist, defaulting to: %s", $y),
}
$z = getenv("EMPTY", "456")
print "print3" {
msg => printf("same goes for epmty variables like EMPTY: %s", $z),
}

10
examples/lang/env1.mcl Normal file
View File

@@ -0,0 +1,10 @@
$env = env()
$m = maplookup($env, "GOPATH", "")
print "print0" {
msg => if hasenv("GOPATH") {
printf("GOPATH is: %s", $m)
} else {
"GOPATH is missing!"
},
}

View File

@@ -0,0 +1,13 @@
# run this example with these commands
# watch -n 0.1 'tail *' # run this in /tmp/mgmt/
# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h1 --ideal-cluster-size 1 --tmp-prefix --no-pgp
# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h2 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2381 --server-urls http://127.0.0.1:2382 --tmp-prefix --no-pgp
# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h3 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2383 --server-urls http://127.0.0.1:2384 --tmp-prefix --no-pgp
# time ./mgmt run --lang examples/lang/exchange0.mcl --hostname h4 --seeds http://127.0.0.1:2379 --client-urls http://127.0.0.1:2385 --server-urls http://127.0.0.1:2386 --tmp-prefix --no-pgp
$rand = random1(8)
$exchanged = exchange("keyns", $rand)
file "/tmp/mgmt/exchange-${hostname()}" {
content => template("Found: {{ . }}\n", $exchanged),
}

4
examples/lang/hello0.mcl Normal file
View File

@@ -0,0 +1,4 @@
file "/tmp/mgmt-hello-world" {
content => "hello world from @purpleidea\n",
state => "exists",
}

View File

@@ -0,0 +1,7 @@
$dt = datetime()
$hystvalues = {"ix0" => $dt, "ix1" => $dt{1}, "ix2" => $dt{2}, "ix3" => $dt{3},}
file "/tmp/mgmt/history" {
content => template("Index(0) {{.ix0}}: {{ datetime_print .ix0 }}\nIndex(1) {{.ix1}}: {{ datetime_print .ix1 }}\nIndex(2) {{.ix2}}: {{ datetime_print .ix2 }}\nIndex(3) {{.ix3}}: {{ datetime_print .ix3 }}\n", $hystvalues),
}

View File

@@ -0,0 +1,4 @@
file "/tmp/mgmt/${hostname()}" {
content => "hello from ${hostname()}!\n",
state => "exists",
}

View File

@@ -0,0 +1,31 @@
file "/tmp/mgmt/systemload" {
content => template("load average: {{ .load }} threshold: {{ .threshold }}\n", $tmplvalues),
}
$tmplvalues = struct{load => $theload, threshold => $threshold,}
$theload = structlookup(load(), "x1")
$threshold = 1.5 # change me if you like
# simple hysteresis implementation
$h1 = $theload > $threshold
$h2 = $theload{1} > $threshold
$h3 = $theload{2} > $threshold
$unload = $h1 || $h2 || $h3
virt "mgmt1" {
uri => "qemu:///session",
cpus => 1,
memory => 524288,
state => "running",
transient => true,
}
# this vm shuts down under load...
virt "mgmt2" {
uri => "qemu:///session",
cpus => 1,
memory => 524288,
state => if $unload { "shutoff" } else { "running" },
transient => true,
}

View File

@@ -0,0 +1,5 @@
$audience = "WORLD!"
file "/tmp/mgmt/hello" {
content => "hello ${audience}!\n",
state => "exists",
}

9
examples/lang/len0.mcl Normal file
View File

@@ -0,0 +1,9 @@
$x1 = ["a", "b", "c", "d",]
print "print4" {
msg => printf("length is: %d", len($x1)),
}
$x2 = {"a" => 1, "b" => 2, "c" => 3,}
print "print3" {
msg => printf("length is: %d", len($x2)),
}

9
examples/lang/load0.mcl Normal file
View File

@@ -0,0 +1,9 @@
$theload = load()
$x1 = structlookup($theload, "x1")
$x5 = structlookup($theload, "x5")
$x15 = structlookup($theload, "x15")
print "print1" {
msg => printf("load average: %f, %f, %f", $x1, $x5, $x15),
}

View File

@@ -0,0 +1,13 @@
$m = {"k1" => 42, "k2" => 13,}
$found = maplookup($m, "k1", 99)
print "print1" {
msg => printf("found value of: %d", $found),
}
$notfound = maplookup($m, "k3", 99)
print "print2" {
msg => printf("notfound value of: %d", $notfound),
}

4
examples/lang/math1.mcl Normal file
View File

@@ -0,0 +1,4 @@
test "t1" {
int64 => (4 + 32) * 15 - 8,
anotherstr => printf("the answer is: %d", 42),
}

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

@@ -0,0 +1,3 @@
print "print0" {
msg => printf("13.0 ^ 4.2 is: %f", pow(13.0, 4.2)),
}

View File

@@ -0,0 +1,8 @@
password "pass0" {
length => 8,
}
file "/tmp/mgmt/password" {
}
Password["pass0"].password -> File["/tmp/mgmt/password"].content

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

@@ -0,0 +1,3 @@
pkg "cowsay" {
state => "installed",
}

View File

@@ -0,0 +1,8 @@
test "printf-a" {
anotherstr => printf("the %s is: %d", "answer", 42),
}
$format = "a %s is: %f"
test "printf-b" {
anotherstr => printf($format, "cool number", 3.14159),
}

View File

@@ -0,0 +1,18 @@
# here are all the possible options:
#$opts = struct{strategy => "rr", max => 3, reuse => false, ttl => 10,}
# although an empty struct is valid too:
#$opts = struct{}
# we'll just use a smaller subset today:
$opts = struct{strategy => "rr", max => 2, ttl => 10,}
# schedule in a particular namespace with options:
$set = schedule("xsched", $opts)
# and if you want, you can omit the options entirely:
#$set = schedule("xsched")
file "/tmp/mgmt/scheduled-${hostname()}" {
content => template("set: {{ . }}\n", $set),
}

View File

@@ -0,0 +1,9 @@
exec "exec0" {
cmd => "echo hello world && echo goodbye world 1>&2", # to stdout && stderr
shell => "/bin/bash",
}
print "print0" {
}
Exec["exec0"].output -> Print["print0"].msg

View File

@@ -0,0 +1,14 @@
file "/tmp/mgmt/foo" {
content => "hello from foo\n",
}
print "print0" {
}
File["/tmp/mgmt/foo"].content -> Print["print0"].msg
print "print1" {
msg => "hello",
}
Print["print0"] -> Print["print1"]

View File

@@ -0,0 +1,8 @@
file "/tmp/mgmt/foo" {
content => "hello from foo\n",
}
file "/tmp/mgmt/bar" {
}
File["/tmp/mgmt/foo"].content -> File["/tmp/mgmt/bar"].content

View File

@@ -0,0 +1,21 @@
$ns = "estate"
$exchanged = kvlookup($ns)
$state = maplookup($exchanged, $hostname, "default")
exec "exec0" {
cmd => "echo hello world && echo goodbye world 1>&2", # to stdout && stderr
shell => "/bin/bash",
}
kv "kv0" {
key => $ns,
#value => "two",
}
Exec["exec0"].output -> Kv["kv0"].value
if $state != "default" {
file "/tmp/mgmt/state" {
content => printf("state: %s\n", $state),
}
}

View File

@@ -0,0 +1,10 @@
$x = "hello"
if true {
$x = "i am shadowed" # this is allowed, but not a good practice to intentionally shadow
print "inner-scope" {
msg => $x, # contents are: i am shadowed
}
}
print "top-scope" {
msg => $x, # contents are: hello
}

50
examples/lang/states0.mcl Normal file
View File

@@ -0,0 +1,50 @@
$ns = "estate"
$exchanged = kvlookup($ns)
$state = maplookup($exchanged, $hostname, "default")
if $state == "one" || $state == "default" {
file "/tmp/mgmt/state" {
content => "state: one\n",
}
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" {
key => $ns,
value => "two",
}
Exec["timer"] -> Kv["${ns}"]
}
if $state == "two" {
file "/tmp/mgmt/state" {
content => "state: two\n",
}
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" {
key => $ns,
value => "three",
}
Exec["timer"] -> Kv["${ns}"]
}
if $state == "three" {
file "/tmp/mgmt/state" {
content => "state: three\n",
}
exec "timer" {
cmd => "/usr/bin/sleep 1s",
}
kv "${ns}" {
key => $ns,
value => "one",
}
Exec["timer"] -> Kv["${ns}"]
}

View File

@@ -0,0 +1,13 @@
$st = struct{f1 => 42, f2 => true, f3 => 3.14,}
$f1 = structlookup($st, "f1")
print "print1" {
msg => printf("f1 field is: %d", $f1),
}
$f2 = structlookup($st, "f2")
print "print2" {
msg => printf("f2 field is: %t", $f2),
}

View File

@@ -0,0 +1,10 @@
$answer = 42
$s = int2str($answer)
print "print1" {
msg => printf("an str is: %s", $s),
}
print "print2" {
msg => template("an str is: {{ int2str . }}", $answer),
}

7
examples/lang/virt1.mcl Normal file
View File

@@ -0,0 +1,7 @@
virt "mgmt3" {
uri => "qemu:///session",
cpus => 1,
memory => 524288,
state => "running",
transient => true,
}

View File

@@ -14,6 +14,15 @@ import (
mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"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.
@@ -36,6 +45,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
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 {
@@ -53,7 +95,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error {
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
g, err := pgraph.NewGraph(obj.Name)
@@ -135,7 +177,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"),
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
@@ -164,7 +206,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
return
}
log.Printf("libmgmt: Generating new graph...")
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
@@ -178,7 +220,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
@@ -199,10 +241,10 @@ func Run() error {
obj.ConvergedTimeout = -1
obj.Noop = false // FIXME: careful!
obj.GAPI = &MyGAPI{ // graph API
Name: "libmgmt", // TODO: set on compilation
Interval: 60 * 10, // arbitrarily change graph every 15 seconds
}
//obj.GAPI = &MyGAPI{ // graph API
// Name: "libmgmt", // TODO: set on compilation
// Interval: 60 * 10, // arbitrarily change graph every 15 seconds
//}
if err := obj.Init(); err != nil {
return err

View File

@@ -16,8 +16,20 @@ import (
"github.com/purpleidea/mgmt/resources"
errwrap "github.com/pkg/errors"
"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"
)
func init() {
gapi.Register(Name, func() gapi.GAPI { return &MyGAPI{} }) // register
}
// MyGAPI implements the main GAPI interface.
type MyGAPI struct {
Name string // graph name
@@ -38,6 +50,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
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 {
@@ -87,7 +132,7 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) {
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
g, err := pgraph.NewGraph(obj.Name)
@@ -142,7 +187,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"),
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
@@ -171,7 +216,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
return
}
log.Printf("libmgmt: Generating new graph...")
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
@@ -185,7 +230,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
@@ -197,19 +242,19 @@ func (obj *MyGAPI) Close() error {
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 // disable for easy debugging
obj.Program = Name // TODO: set on compilation
obj.Version = "0.0.1" // TODO: set on compilation
obj.TmpPrefix = true // disable for easy debugging
//prefix := "/tmp/testprefix/"
//obj.Prefix = &p // enable for easy debugging
obj.IdealClusterSize = -1
obj.ConvergedTimeout = -1
obj.Noop = false // FIXME: careful!
obj.GAPI = &MyGAPI{ // graph API
Name: "libmgmt", // TODO: set on compilation
Interval: 60 * 10, // arbitrarily change graph every 15 seconds
}
//obj.GAPI = &MyGAPI{ // graph API
// Name: Name, // TODO: set on compilation
// Interval: 60 * 10, // arbitrarily change graph every 15 seconds
//}
if err := obj.Init(); err != nil {
return err

View File

@@ -14,6 +14,15 @@ import (
mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"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.
@@ -36,6 +45,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
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 {
@@ -53,7 +95,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error {
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
g, err := pgraph.NewGraph(obj.Name)
@@ -132,7 +174,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"),
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
@@ -161,7 +203,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
return
}
log.Printf("libmgmt: Generating new graph...")
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
@@ -175,7 +217,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
@@ -196,10 +238,10 @@ func Run() error {
obj.ConvergedTimeout = -1
obj.Noop = false // FIXME: careful!
obj.GAPI = &MyGAPI{ // graph API
Name: "libmgmt", // TODO: set on compilation
Interval: 60 * 10, // arbitrarily change graph every 15 seconds
}
//obj.GAPI = &MyGAPI{ // graph API
// Name: "libmgmt", // TODO: set on compilation
// Interval: 60 * 10, // arbitrarily change graph every 15 seconds
//}
if err := obj.Init(); err != nil {
return err

View File

@@ -15,6 +15,15 @@ import (
"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.
@@ -37,6 +46,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
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 {
@@ -54,7 +96,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error {
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
n1, err := resources.NewNamedResource("noop", "noop1")
@@ -96,7 +138,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"),
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
@@ -124,7 +166,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
return
}
log.Printf("libmgmt: Generating new graph...")
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
@@ -138,7 +180,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
@@ -157,10 +199,10 @@ func Run() error {
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
}
//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

View File

@@ -15,6 +15,15 @@ import (
mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"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.
@@ -39,6 +48,39 @@ func NewMyGAPI(data gapi.Data, name string, interval uint, count uint) (*MyGAPI,
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 {
@@ -56,7 +98,7 @@ func (obj *MyGAPI) Init(data gapi.Data) error {
// Graph returns a current Graph.
func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
return nil, fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
g, err := pgraph.NewGraph(obj.Name)
@@ -89,7 +131,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("libmgmt: MyGAPI is not initialized"),
Err: fmt.Errorf("%s: MyGAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
ch <- next
@@ -117,7 +159,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
return
}
log.Printf("libmgmt: Generating new graph...")
log.Printf("%s: Generating new graph...", Name)
select {
case ch <- gapi.Next{}: // trigger a run
case <-obj.closeChan:
@@ -131,7 +173,7 @@ func (obj *MyGAPI) Next() chan gapi.Next {
// Close shuts down the MyGAPI.
func (obj *MyGAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("libmgmt: MyGAPI is not initialized")
return fmt.Errorf("%s: MyGAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
@@ -150,11 +192,11 @@ func Run(count uint) error {
obj.ConvergedTimeout = -1
obj.Noop = true
obj.GAPI = &MyGAPI{ // graph API
Name: "libmgmt", // TODO: set on compilation
Count: count, // number of vertices to add
Interval: 15, // arbitrarily change graph every 15 seconds
}
//obj.GAPI = &MyGAPI{ // graph API
// Name: "libmgmt", // TODO: set on compilation
// Count: count, // number of vertices to add
// Interval: 15, // arbitrarily change graph every 15 seconds
//}
if err := obj.Init(); err != nil {
return err

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