48 Commits

Author SHA1 Message Date
Jonathan Gold
9907c12eda resources: Enhancements to user and group
This patch adds autoedges between users and groups, and extends
users with additional fields for supplementary groups and a named
primary group. Also, some small fixes to log and error messages.
2017-10-23 19:18:52 -04:00
Jonathan Gold
19533a32b5 resources: Add a group resource 2017-10-21 01:28:22 -04:00
Jonathan Gold
c5a5004f9e resources: Fix user gid compare 2017-10-19 06:58:31 -04:00
Jonathan Gold
677cdea99d resources: Improve nspawn resource 2017-10-17 19:23:04 -04:00
Jonathan Gold
4d7c0ddbce resources: Add an Aws resource 2017-10-09 04:05:13 -04:00
James Shubin
81daf10157 test: Fix linter issues
These are some linter issues that were found in a new version of the
linter. Let's fix them now before that linter hits our test suite.
2017-09-26 19:38:53 -04:00
James Shubin
b3ef4e41bf test: Use stable version of gometalinter
Hopefully this prevents the various breakages seen in our lint test.
2017-09-26 19:08:43 -04:00
James Shubin
9fbf149717 etcd: Bump to newer versions 2017-09-19 18:21:15 -04:00
James Shubin
95cb94a039 vendor: Add codec package because of breakage
Recent git master 54210f4e076c57f351166f0ed60e67d3fca57a36 of
github.com/ugorji/go broke the builds. See:
https://github.com/coreos/etcd/issues/8579
2017-09-19 18:21:15 -04:00
Juan Luis de Sousa-Valadas Castaño
21f7f87716 resources: Refresh packagekit cache before install
Fixes #80
2017-09-17 22:29:15 +02:00
Jonathan Gold
831c7e2c32 resources: Add user resource 2017-09-17 01:04:36 -04:00
James Shubin
cc0d04c8b7 git: Ignore .envrc file from direnv
Some find this useful for setting a custom GOPATH per project.
2017-09-15 16:17:40 -04:00
James Shubin
46be83f8f7 legal: Re-license to GPLv3 2017-09-11 18:07:47 -04:00
James Shubin
28560e2045 resources: Fix formatting 2017-09-11 18:06:34 -04:00
James Shubin
0df4824a56 test: Increase timeouts for slow travis
Should prevent more intermittent failures.
2017-09-09 15:31:06 -04:00
James Shubin
dbcabc6517 github: Improve the PR template 2017-09-09 15:03:53 -04:00
Jonathan Gold
69f479b67e virt: Allow more than 26 disks 2017-09-08 02:15:40 +00:00
James Shubin
af75696018 github: Add a PR template to help new users
Hopefully this addresses the most common things.
2017-09-07 16:14:11 -04:00
Arthur Mello
80b8f8740f virt: Added support for ~user into expandHome
- Enabled expandHome to expand both ~/ and ~username/ paths
- Added some unit tests for expandHome
2017-09-06 14:59:08 -04:00
James Shubin
71ab325940 yaml2: Meta should keep defaults, and Res should have kind
This would previously panic since it wouldn't get a kind, and the meta
parameters would overwrite the defaults so it would block because limit
didn't have the default of +inf.

The removal of the SetKind was my fault in:

b8ff6938df

It's funny because it ends in `bad`. Guess I should have checked that!
2017-09-06 13:44:21 -04:00
James Shubin
653c76709a test: Fix another intermittent failure
Some of the tests had very precise timeouts, which weren't very
important. Here's another one that timed out early.
2017-09-04 16:39:01 -04:00
Juan Luis de Sousa-Valadas Castaño
83cc1bab38 vagrant: Fix PATH
gometalinter failed because it's not in $PATH
2017-09-04 22:08:59 +02:00
James Shubin
6c8588c019 test: Increase timeouts because travis is slow
Should hopefully prevent some intermittent failures.
2017-09-04 13:02:05 -04:00
Ismael Puerto
5b00ed2fb2 vagrant: Change box to F26
F26 provides GO 1.8
2017-09-01 22:21:39 +02:00
Juan-Luis de Sousa-Valadas Castaño
9f66962bfb docs: Change go required version to 1.8 2017-08-31 23:56:16 +02:00
James Shubin
0edba74091 etcd: Bump to version 3.2.6 and update all the grpc deps
Note: When go-grpc-prometheus was in the main $gopath (even at this
version) and everyone else was where they always were in vendor/ this
didn't build! It gave errors like:

	have SendHeader("github.com/purpleidea/mgmt/vendor/google.golang.org/grpc/metadata".MD) error
	want SendHeader("google.golang.org/grpc/metadata".MD) error

and I got frustrated. Putting it "next" to the other vendored deps seems
to have fixed this. Where are the golang docs that explain this
phenomenon?

This also requires golang 1.8+ as that is a requirement for etcd. It's
probably a reasonable thing for us too.

Note the older versions of etcd had some bugs with the concurrency
package and other things, so this is a necessary bump.
2017-08-30 14:16:02 -04:00
Dennis Kliban
1003b49dd9 resources: Add validation for Msg Priority field
This adds validation that ensures that Msg Priority field is one of the following values:
"Emerg", "Alert", "Crit", "Err", "Warning", "Notice", "Info", "Debug".
2017-08-20 12:37:39 +00:00
James Shubin
884ba54f96 resources: Include default MetaParams so Validate will pass in tests 2017-08-18 19:52:02 -04:00
Dennis Kliban
cf2325a2da vagrant: Increase amount of RAM allocated to boxes backed by libvirt 2017-08-07 13:55:21 -04:00
AdnanLFC
db6972638d pgraph: test: Added tests for DeleteEdge 2017-07-28 02:02:22 +02:00
James Shubin
74e04e81d5 travis: Update to golang 1.8 as the default
Since the release of Fedora 26 with golang 1.8.1, this is a fine
default.
2017-07-19 12:15:54 -04:00
James Shubin
7c5d7365c7 readme: Add new recording 2017-06-29 13:14:25 -04:00
James Shubin
0dadf3d78a resources: Add NewNamedResource helper
This makes the common pattern of NewResource, SetName, easier. It also
makes it less likely for you to forget to use SetName.
2017-06-17 18:09:49 -04:00
James Shubin
e341256627 resources: Add a utility to map from struct fields
For GAPI front ends that want to know what fields they can use and which
they map to, these two functions can be used.
2017-06-17 11:49:30 -04:00
James Shubin
5a3bd3ca67 hcl: Consistent formatting
Nit picks.
2017-06-16 23:01:46 -04:00
ChrisMcKenzie
8102e0a468 hcl: Added hil string interpolation to hcl frontend 2017-06-15 22:53:55 -07:00
ChrisMcKenzie
7d55179727 hcl: Removed edge object in favor of depends_on field in resource 2017-06-12 10:44:13 -07:00
ChrisMcKenzie
bc1a1d1818 hcl: Added basic hcl frontend 2017-06-09 10:31:34 -07:00
James Shubin
a8bbb22fe8 resources: Fix golint issues
Including a trick to get the golinter to allow our compact code!
2017-06-08 04:38:25 -04:00
James Shubin
6b489f71a1 remote: Add a Ready method to know when startup is finished
Previously, there was an extremely rare race where we would startup,
kick off the Run method in a goroutine, and then run Exit before Run got
very far in its execution. If Run ran some early sections of its code
_after_ we had Exited, we would trigger a panic due to the converger UID
being unregistered.

This patch blocks Exit from progressing until Run has started and
finished running. It also adds a Ready method so that you can monitor
this signal yourself if you'd like to add the necessary wait to your
code.
2017-06-08 03:55:03 -04:00
James Shubin
f1db088af4 test: Don't be noisy when running cd during testing 2017-06-08 01:05:58 -04:00
James Shubin
6fe12b3fb5 resources: Compare grouped resources properly
When comparing resources, we have to recursively compare grouped
resources as well! Now fixed.
2017-06-08 01:05:58 -04:00
James Shubin
dacbf9b68d resources: Add resource sorting and clean tests
Resource sorting is needed for comparing resource groups.
2017-06-08 01:05:58 -04:00
James Shubin
9f5057eac7 resources: Do not panic on autogrouped graph switches
Graph changes from autogrouped -> not autogrouped or vice versa cause a
panic (or I assume a leak) because we compared the auto grouped graph to
the ungrouped one, which would cause an Exit on an unstarted Vertex.
This includes a test that seems to reliably reproduces the issue.
2017-06-08 01:05:58 -04:00
James Shubin
525cd54921 pgraph: Improve testing and refactor out some test utilities 2017-06-07 07:13:12 -04:00
James Shubin
7ac94bbf5f resources: Panic if attempting to register a duplicate resource
Don't silently let this overwrite pass. It would mean a mistake.
2017-06-07 03:15:06 -04:00
James Shubin
b8ff6938df resources: Unify resource creation and kind setting
This removes the duplication of the kind string and cleans up things for
resource creation.
2017-06-07 03:07:02 -04:00
James Shubin
2f6c77fba2 misc: Update my tag script to deal with large releases 2017-06-03 03:54:49 -04:00
140 changed files with 3918 additions and 845 deletions

36
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,36 @@
## Tips:
* 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
git checkout your-feature
git rebase master
git push your-remote your-feature
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
git commit --amend # combine with existing commit
git push your-remote your-feature -f
# now ping @purpleidea in the github PR since it doesn't notify us automatically
```
## Thanks for contributing to mgmt and welcome to the team!

1
.gitignore vendored
View File

@@ -2,6 +2,7 @@
.omv/
.ssh/
.vagrant/
.envrc
old/
tmp/
*_stringer.go

6
.gitmodules vendored
View File

@@ -16,3 +16,9 @@
[submodule "vendor/honnef.co/go/augeas"]
path = vendor/honnef.co/go/augeas
url = https://github.com/dominikh/go-augeas/
[submodule "vendor/github.com/grpc-ecosystem/go-grpc-prometheus"]
path = vendor/github.com/grpc-ecosystem/go-grpc-prometheus
url = https://github.com/grpc-ecosystem/go-grpc-prometheus
[submodule "vendor/github.com/ugorji/go"]
path = vendor/github.com/ugorji/go
url = https://github.com/ugorji/go

View File

@@ -1,8 +1,7 @@
language: go
go:
- 1.6.x
- 1.7.x
- 1.8.x
- 1.9.x
- tip
go_import_path: github.com/purpleidea/mgmt
sudo: true
@@ -16,7 +15,7 @@ matrix:
fast_finish: true
allow_failures:
- go: tip
- go: 1.8.x
- go: 1.9.x
notifications:
irc:
channels:

141
COPYING
View File

@@ -1,5 +1,5 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
@@ -7,15 +7,17 @@
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
@@ -24,34 +26,44 @@ them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
@@ -60,7 +72,7 @@ modification follow.
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
@@ -537,45 +549,35 @@ to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
@@ -633,29 +635,40 @@ the "copyright" line and a pointer to where the full notice is found.
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.

View File

@@ -3,14 +3,14 @@ Copyright (C) 2013-2017+ James Shubin and the project contributors
Written by James Shubin <james@shubin.ca> and the project contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
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 Affero General Public License for more details.
GNU General Public License for more details.
You should have received a copy of the GNU Affero General Public License
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@@ -3,16 +3,16 @@
# Written by James Shubin <james@shubin.ca> and the project contributors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# 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 Affero General Public License for more details.
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
SHELL = /usr/bin/env bash

View File

@@ -80,6 +80,7 @@ We'd love to have your patches! Please send them by email, or as a pull request.
| 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) |
##

5
Vagrantfile vendored
View File

@@ -6,13 +6,16 @@ Vagrant.configure(2) do |config|
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.define "mgmt-dev" do |instance|
instance.vm.box = "fedora/24-cloud-base"
instance.vm.box = "fedora/26-cloud-base"
end
config.vm.provider "virtualbox" do |v|
v.memory = 1536
v.cpus = 2
end
config.vm.provider "libvirt" do |v|
v.memory = 2048
end
config.vm.provision "file", source: "vagrant/motd", destination: ".motd"
config.vm.provision "shell", inline: "cp ~vagrant/.motd /etc/motd"

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package converger is a facility for reporting the converged state.
@@ -85,7 +85,7 @@ type cuid struct {
}
// NewConverger builds a new converger struct.
func NewConverger(timeout int, stateFn func(bool) error) *converger {
func NewConverger(timeout int, stateFn func(bool) error) Converger {
return &converger{
timeout: timeout,
stateFn: stateFn,

6
doc.go
View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package main provides the main entrypoint for using the `mgmt` software.

View File

@@ -21,7 +21,8 @@ dependencies, _runtime_ dependencies, and additionally, a few extra dependencies
required for running the _test_ suite.
### Build
* `golang` 1.6 or higher (required, available in most distros)
* `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
@@ -60,7 +61,7 @@ For building a visual representation of the graph, `graphviz` is required.
* golint `github.com/golang/lint/golint`
## Quick start
* Make sure you have golang version 1.6 or greater installed.
* Make sure you have golang version 1.8 or greater installed.
* If you do not have a GOPATH yet, create one and export it:
```
mkdir $HOME/gopath

View File

@@ -73,14 +73,13 @@ Init() error
```
This is called to initialize the resource. If something goes wrong, it should
return an error. It should set the resource `kind`, do any resource specific
work, and finish by calling the `Init` method of the base resource.
return an error. It should do any resource specific work, and finish by calling
the `Init` method of the base resource.
#### Example
```golang
// Init initializes the Foo resource.
func (obj *FooRes) Init() error {
obj.BaseRes.Kind = "foo" // must lower case resource kind
// run the resource specific initialization, and error if anything fails
if some_error {
return err // something went wrong!
@@ -399,9 +398,9 @@ UnmarshalYAML(unmarshal func(interface{}) error) error // optional
```
This is optional, but recommended for any resource that will have a YAML
accessible struct, and an entry in the `GraphConfig` struct. It is not required
because to do so would mean that third-party or custom resources (such as those
someone writes to use with `libmgmt`) would have to implement this needlessly.
accessible struct. It is not required because to do so would mean that
third-party or custom resources (such as those someone writes to use with
`libmgmt`) would have to implement this needlessly.
The signature intentionally matches what is required to satisfy the `go-yaml`
[Unmarshaler](https://godoc.org/gopkg.in/yaml.v2#Unmarshaler) interface.
@@ -453,35 +452,15 @@ type FooRes struct {
}
```
### YAML
In addition to labelling your resource struct with YAML fields, you must also
add an entry to the internal `GraphConfig` struct. It is a fairly straight
forward one line patch.
### Resource registration
All resources must be registered with the engine so that they can be found. This
also ensures they can be encoded and decoded. Make sure to include the following
code snippet for this to work.
```golang
type GraphConfig struct {
// [snip...]
Resources struct {
Noop []*resources.NoopRes `yaml:"noop"`
File []*resources.FileRes `yaml:"file"`
// [snip...]
Foo []*resources.FooRes `yaml:"foo"` // tada :)
}
}
```
It's also recommended that you add the [UnmarshalYAML](#unmarshalyaml) method to
your resources so that unspecified values are given sane defaults.
### Gob registration
All resources must be registered with the `golang` _gob_ module so that they can
be encoded and decoded. Make sure to include the following code snippet for this
to work.
```golang
import "encoding/gob"
func init() { // special golang method that runs once
gob.Register(&FooRes{}) // substitude your resource here
// set your resource kind and struct here (the kind must be lower case)
RegisterResource("foo", func() Res { return &FooRes{} })
}
```

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// TODO: Add TTL's (eg: volunteering)
@@ -529,8 +529,9 @@ func (obj *EmbdEtcd) CtxError(ctx context.Context, err error) (context.Context,
if obj.ctxErr != nil { // stop on permanent error
return ctx, obj.ctxErr
}
const ctxErr = "ctxErr"
const ctxIter = "ctxIter"
type ctxKey string // use a non-basic type as ctx key (str can conflict)
const ctxErr ctxKey = "ctxErr"
const ctxIter ctxKey = "ctxIter"
expBackoff := func(tmin, texp, iter, tmax int) time.Duration {
// https://en.wikipedia.org/wiki/Exponential_backoff
// tmin <= texp^iter - 1 <= tmax // TODO: check my math

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd
@@ -171,7 +171,6 @@ func GetResources(obj *EmbdEtcd, hostnameFilter, kindFilter []string) ([]resourc
}
if obj, err := resources.B64ToRes(val); err == nil {
obj.SetKind(kind) // cheap init
log.Printf("Etcd: Get: (Hostname, Kind, Name): (%s, %s, %s)", hostname, kind, name)
resourceList = append(resourceList, obj)
} else {

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package etcd

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package event provides some primitives that are used for message passing.

19
examples/autoedges4.yaml Normal file
View File

@@ -0,0 +1,19 @@
---
graph: mygraph
resources:
user:
- name: edgeuser
state: absent
gid: 10000
- name: edgeuser2
state: exists
group: edgegroup
groups: [edgegroup2, edgegroup3]
group:
- name: edgegroup
state: exists
gid: 10000
- name: edgegroup2
state: exists
- name: edgegroup3
state: exists

10
examples/aws_ec2_1.yaml Normal file
View File

@@ -0,0 +1,10 @@
---
graph: mygraph
resources:
aws:ec2:
- name: ec2example
region: ca-central-1
type: t2.micro
imageid: ami-5ac17f3e
state: running
edges: []

14
examples/graph0.hcl Normal file
View File

@@ -0,0 +1,14 @@
resource "file" "file1" {
path = "/tmp/mgmt-hello-world"
content = "hello, world"
state = "exists"
depends_on = ["noop.noop1", "exec.sleep"]
}
resource "noop" "noop1" {
test = "nil"
}
resource "exec" "sleep" {
cmd = "sleep 10s"
}

4
examples/graph1.hcl Normal file
View File

@@ -0,0 +1,4 @@
resource "exec" "exec1" {
cmd = "cat /tmp/mgmt-hello-world"
state = "present"
}

8
examples/group1.yaml Normal file
View File

@@ -0,0 +1,8 @@
---
graph: mygraph
resources:
group:
- name: testgroup
state: exists
gid: 10000
edges: []

9
examples/hil.hcl Normal file
View File

@@ -0,0 +1,9 @@
resource "file" "file1" {
path = "/tmp/mgmt-hello-world"
content = "${exec.sleep.Output}"
state = "exists"
}
resource "exec" "sleep" {
cmd = "echo hello"
}

View File

@@ -61,12 +61,12 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err
}
// FIXME: these are being specified temporarily until it's the default!
metaparams := resources.DefaultMetaParams
exec1 := &resources.ExecRes{
BaseRes: resources.BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: metaparams,
},
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
@@ -77,6 +77,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
output := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "output",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
@@ -92,6 +93,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
stdout := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "stdout",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
@@ -107,6 +109,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
stderr := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "stderr",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
@@ -229,10 +232,7 @@ func Run() error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -63,6 +63,7 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) {
f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/sub1",
@@ -74,6 +75,7 @@ func (obj *MyGAPI) subGraph() (*pgraph.Graph, error) {
n1 := &resources.NoopRes{
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
@@ -93,7 +95,6 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err
}
// FIXME: these are being specified temporarily until it's the default!
metaparams := resources.DefaultMetaParams
content := "I created a subgraph!\n"
@@ -238,10 +239,7 @@ func Run() error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -61,13 +61,13 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err
}
// FIXME: these are being specified temporarily until it's the default!
metaparams := resources.DefaultMetaParams
content := "I created a subgraph!\n"
f0 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "README",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/README",
@@ -86,6 +86,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/sub1",
@@ -97,6 +98,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
n1 := &resources.NoopRes{
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
@@ -110,6 +112,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
subGraphRes0 := &resources.GraphRes{ // TODO: should we name this SubGraphRes ?
BaseRes: resources.BaseRes{
Name: "subgraph1",
Kind: "graph",
MetaParams: metaparams,
},
Graph: subGraph,
@@ -226,10 +229,7 @@ func Run() error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -57,13 +57,13 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, fmt.Errorf("libmgmt: MyGAPI is not initialized")
}
n1 := &resources.NoopRes{
BaseRes: resources.BaseRes{
Name: "noop1",
MetaParams: resources.DefaultMetaParams,
},
n1, err := resources.NewNamedResource("noop", "noop1")
if err != nil {
return nil, err
}
// NOTE: This is considered the legacy method to build graphs. Avoid
// importing the legacy `yamlgraph` lib if possible for custom graphs.
// we can still build a graph via the yaml method
gc := &yamlgraph.GraphConfig{
Graph: obj.Name,
@@ -72,7 +72,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
Exec: []*resources.ExecRes{},
File: []*resources.FileRes{},
Msg: []*resources.MsgRes{},
Noop: []*resources.NoopRes{n1},
Noop: []*resources.NoopRes{n1.(*resources.NoopRes)},
Pkg: []*resources.PkgRes{},
Svc: []*resources.SvcRes{},
Timer: []*resources.TimerRes{},
@@ -190,10 +190,7 @@ func Run() error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -65,11 +65,9 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
}
var vertex pgraph.Vertex
for i := uint(0); i < obj.Count; i++ {
n := &resources.NoopRes{
BaseRes: resources.BaseRes{
Name: fmt.Sprintf("noop%d", i),
MetaParams: resources.DefaultMetaParams,
},
n, err := resources.NewNamedResource("noop", fmt.Sprintf("noop%d", i))
if err != nil {
return nil, err
}
g.AddVertex(n)
if i > 0 {
@@ -186,10 +184,7 @@ func Run(count uint) error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -61,13 +61,13 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err
}
// FIXME: these are being specified temporarily until it's the default!
metaparams := resources.DefaultMetaParams
content := "Delete me to trigger a notification!\n"
f0 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "README",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/README",
@@ -80,6 +80,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
p1 := &resources.PasswordRes{
BaseRes: resources.BaseRes{
Name: "password1",
Kind: "password",
MetaParams: metaparams,
},
Length: 8, // generated string will have this many characters
@@ -90,6 +91,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
@@ -106,6 +108,7 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
n1 := &resources.NoopRes{
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
@@ -231,10 +234,7 @@ func Run() error {
}
}()
if err := obj.Run(); err != nil {
return err
}
return nil
return obj.Run()
}
func main() {

View File

@@ -2,6 +2,6 @@
graph: mygraph
resources:
nspawn:
- name: mgmt-nspawn1
- name: nspawn1
state: running
edges: []

View File

@@ -1,7 +0,0 @@
---
graph: mygraph
resources:
nspawn:
- name: mgmt-nspawn2
state: stopped
edges: []

9
examples/user1.yaml Normal file
View File

@@ -0,0 +1,9 @@
---
graph: mygraph
resources:
user:
- name: testuser
uid: 1002
gid: 100
state: exists
edges: []

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package gapi defines the interface that graph API generators must meet.

155
hcl/gapi.go Normal file
View File

@@ -0,0 +1,155 @@
// Mgmt
// Copyright (C) 2013-2017+ 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 hcl
import (
"fmt"
"log"
"sync"
"github.com/purpleidea/mgmt/gapi"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/recwatch"
)
// GAPI ...
type GAPI struct {
File *string
initialized bool
data gapi.Data
wg sync.WaitGroup
closeChan chan struct{}
configWatcher *recwatch.ConfigWatcher
}
// NewGAPI ...
func NewGAPI(data gapi.Data, file *string) (*GAPI, error) {
if file == nil {
return nil, fmt.Errorf("empty file given")
}
obj := &GAPI{
File: file,
}
return obj, obj.Init(data)
}
// Init ...
func (obj *GAPI) Init(d gapi.Data) error {
if obj.initialized {
return fmt.Errorf("already initialized")
}
if obj.File == nil {
return fmt.Errorf("file cannot be nil")
}
obj.data = d
obj.closeChan = make(chan struct{})
obj.initialized = true
obj.configWatcher = recwatch.NewConfigWatcher()
return nil
}
// Graph ...
func (obj *GAPI) Graph() (*pgraph.Graph, error) {
config, err := loadHcl(obj.File)
if err != nil {
return nil, fmt.Errorf("unable to parse graph: %s", err)
}
return graphFromConfig(config, obj.data)
}
// Next ...
func (obj *GAPI) Next() chan gapi.Next {
ch := make(chan gapi.Next)
obj.wg.Add(1)
go func() {
defer obj.wg.Done()
defer close(ch)
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("hcl: GAPI is not initialized"),
Exit: true,
}
ch <- next
return
}
startChan := make(chan struct{}) // start signal
close(startChan) // kick it off!
watchChan, configChan := make(chan error), make(chan error)
if obj.data.NoConfigWatch {
configChan = nil
} else {
configChan = obj.configWatcher.ConfigWatch(*obj.File) // simple
}
if obj.data.NoStreamWatch {
watchChan = nil
} else {
watchChan = obj.data.World.ResWatch()
}
for {
var err error
var ok bool
select {
case <-startChan:
startChan = nil
case err, ok = <-watchChan:
case err, ok = <-configChan:
if !ok {
return
}
case <-obj.closeChan:
return
}
log.Printf("hcl: generating new graph")
next := gapi.Next{
Err: err,
}
select {
case ch <- next:
case <-obj.closeChan:
return
}
}
}()
return ch
}
// Close ...
func (obj *GAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("hcl: GAPI is not initialized")
}
obj.configWatcher.Close()
close(obj.closeChan)
obj.wg.Wait()
obj.initialized = false
return nil
}

387
hcl/parse.go Normal file
View File

@@ -0,0 +1,387 @@
// Mgmt
// Copyright (C) 2013-2017+ 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 hcl
import (
"fmt"
"io/ioutil"
"log"
"strings"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/hashicorp/hil"
"github.com/purpleidea/mgmt/gapi"
hv "github.com/purpleidea/mgmt/hil"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
)
type collectorResConfig struct {
Kind string
Pattern string
}
// Config defines the structure of the hcl config.
type Config struct {
Resources []*Resource
Edges []*Edge
Collector []collectorResConfig
}
// vertex is the data structure of a vertex.
type vertex struct {
Kind string `hcl:"kind"`
Name string `hcl:"name"`
}
// Edge defines an edge in hcl.
type Edge struct {
Name string
From vertex
To vertex
Notify bool
}
// Resources define the state for resources.
type Resources struct {
Resources []resources.Res
}
// Resource ...
type Resource struct {
Name string
Kind string
resource resources.Res
Meta resources.MetaParams
deps []*Edge
rcv map[string]*hv.ResourceVariable
}
type key struct {
kind, name string
}
func graphFromConfig(c *Config, data gapi.Data) (*pgraph.Graph, error) {
var graph *pgraph.Graph
var err error
graph, err = pgraph.NewGraph("Graph")
if err != nil {
return nil, fmt.Errorf("unable to create graph from config: %s", err)
}
lookup := make(map[key]pgraph.Vertex)
var keep []pgraph.Vertex
var resourceList []resources.Res
log.Printf("hcl: parsing %d resources", len(c.Resources))
for _, r := range c.Resources {
res := r.resource
kind := r.resource.GetKind()
log.Printf("hcl: resource \"%s\" \"%s\"", kind, r.Name)
if !strings.HasPrefix(res.GetName(), "@@") {
fn := func(v pgraph.Vertex) (bool, error) {
return resources.VtoR(v).Compare(res), nil
}
v, err := graph.VertexMatchFn(fn)
if err != nil {
return nil, fmt.Errorf("could not match vertex: %s", err)
}
if v == nil {
v = res
graph.AddVertex(v)
}
lookup[key{kind, res.GetName()}] = v
keep = append(keep, v)
} else if !data.Noop {
res.SetName(res.GetName()[2:])
res.SetKind(kind)
resourceList = append(resourceList, res)
}
}
// store in backend (usually etcd)
if err := data.World.ResExport(resourceList); err != nil {
return nil, fmt.Errorf("Config: Could not export resources: %v", err)
}
// lookup from backend (usually etcd)
var hostnameFilter []string // empty to get from everyone
kindFilter := []string{}
for _, t := range c.Collector {
kind := strings.ToLower(t.Kind)
kindFilter = append(kindFilter, kind)
}
// do all the graph look ups in one single step, so that if the backend
// database changes, we don't have a partial state of affairs...
if len(kindFilter) > 0 { // if kindFilter is empty, don't need to do lookups!
var err error
resourceList, err = data.World.ResCollect(hostnameFilter, kindFilter)
if err != nil {
return nil, fmt.Errorf("Config: Could not collect resources: %v", err)
}
}
for _, res := range resourceList {
matched := false
// see if we find a collect pattern that matches
for _, t := range c.Collector {
kind := strings.ToLower(t.Kind)
// use t.Kind and optionally t.Pattern to collect from storage
log.Printf("Collect: %v; Pattern: %v", kind, t.Pattern)
// XXX: expand to more complex pattern matching here...
if res.GetKind() != kind {
continue
}
if matched {
// we've already matched this resource, should we match again?
log.Printf("Config: Warning: Matching %s again!", res)
}
matched = true
// collect resources but add the noop metaparam
//if noop { // now done in mgmtmain
// res.Meta().Noop = noop
//}
if t.Pattern != "" { // XXX: simplistic for now
res.CollectPattern(t.Pattern) // res.Dirname = t.Pattern
}
log.Printf("Collect: %s: collected!", res)
// XXX: similar to other resource add code:
// if _, exists := lookup[kind]; !exists {
// lookup[kind] = make(map[string]pgraph.Vertex)
// }
fn := func(v pgraph.Vertex) (bool, error) {
return resources.VtoR(v).Compare(res), nil
}
v, err := graph.VertexMatchFn(fn)
if err != nil {
return nil, fmt.Errorf("could not VertexMatchFn() resource: %s", err)
}
if v == nil { // no match found
v = res // a standalone res can be a vertex
graph.AddVertex(v) // call standalone in case not part of an edge
}
lookup[key{kind, res.GetName()}] = v // used for constructing edges
keep = append(keep, v) // append
//break // let's see if another resource even matches
}
}
for _, r := range c.Resources {
for _, e := range r.deps {
if _, ok := lookup[key{strings.ToLower(e.From.Kind), e.From.Name}]; !ok {
return nil, fmt.Errorf("can't find 'from' name")
}
if _, ok := lookup[key{strings.ToLower(e.To.Kind), e.To.Name}]; !ok {
return nil, fmt.Errorf("can't find 'to' name")
}
from := lookup[key{strings.ToLower(e.From.Kind), e.From.Name}]
to := lookup[key{strings.ToLower(e.To.Kind), e.To.Name}]
edge := &resources.Edge{
Name: e.Name,
Notify: e.Notify,
}
graph.AddEdge(from, to, edge)
}
recv := make(map[string]*resources.Send)
// build Rcv's from resource variables
for k, v := range r.rcv {
send, ok := lookup[key{strings.ToLower(v.Kind), v.Name}]
if !ok {
return nil, fmt.Errorf("resource not found")
}
recv[strings.ToUpper(string(k[0]))+k[1:]] = &resources.Send{
Res: resources.VtoR(send),
Key: v.Field,
}
to := lookup[key{strings.ToLower(r.Kind), r.Name}]
edge := &resources.Edge{
Name: v.Name,
Notify: true,
}
graph.AddEdge(send, to, edge)
}
r.resource.SetRecv(recv)
}
return graph, nil
}
func loadHcl(f *string) (*Config, error) {
if f == nil {
return nil, fmt.Errorf("empty file given")
}
data, err := ioutil.ReadFile(*f)
if err != nil {
return nil, fmt.Errorf("unable to read file: %v", err)
}
file, err := hcl.ParseBytes(data)
if err != nil {
return nil, fmt.Errorf("unable to parse file: %s", err)
}
config := new(Config)
list, ok := file.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("unable to parse file: file does not contain root node object")
}
if resources := list.Filter("resource"); len(resources.Items) > 0 {
var err error
config.Resources, err = loadResourcesHcl(resources)
if err != nil {
return nil, fmt.Errorf("unable to parse: %s", err)
}
}
return config, nil
}
func loadResourcesHcl(list *ast.ObjectList) ([]*Resource, error) {
list = list.Children()
if len(list.Items) == 0 {
return nil, nil
}
var result []*Resource
for _, item := range list.Items {
kind := item.Keys[0].Token.Value().(string)
name := item.Keys[1].Token.Value().(string)
var listVal *ast.ObjectList
if ot, ok := item.Val.(*ast.ObjectType); ok {
listVal = ot.List
} else {
return nil, fmt.Errorf("module '%s': should be an object", name)
}
var params = resources.DefaultMetaParams
if o := listVal.Filter("meta"); len(o.Items) > 0 {
err := hcl.DecodeObject(&params, o)
if err != nil {
return nil, fmt.Errorf(
"Error parsing meta for %s: %s",
name,
err)
}
}
var deps []string
if edges := listVal.Filter("depends_on"); len(edges.Items) > 0 {
err := hcl.DecodeObject(&deps, edges.Items[0].Val)
if err != nil {
return nil, fmt.Errorf("unable to parse: %s", err)
}
}
var edges []*Edge
for _, dep := range deps {
vertices := strings.Split(dep, ".")
edges = append(edges, &Edge{
To: vertex{
Kind: kind,
Name: name,
},
From: vertex{
Kind: vertices[0],
Name: vertices[1],
},
})
}
var config map[string]interface{}
if err := hcl.DecodeObject(&config, item.Val); err != nil {
log.Printf("hcl: unable to decode body: %v", err)
return nil, fmt.Errorf(
"Error reading config for %s: %s",
name,
err)
}
delete(config, "meta")
delete(config, "depends_on")
rcv := make(map[string]*hv.ResourceVariable)
// parse strings for hil
for k, v := range config {
n, err := hil.Parse(v.(string))
if err != nil {
return nil, fmt.Errorf("unable to parse fields: %v", err)
}
variables, err := hv.ParseVariables(n)
if err != nil {
return nil, fmt.Errorf("unable to parse variables: %v", err)
}
for _, v := range variables {
val, ok := v.(*hv.ResourceVariable)
if !ok {
continue
}
rcv[k] = val
}
}
res, err := resources.NewNamedResource(kind, name)
if err != nil {
log.Printf("hcl: unable to parse resource: %v", err)
return nil, err
}
if err := hcl.DecodeObject(res, item.Val); err != nil {
log.Printf("hcl: unable to decode body: %v", err)
return nil, fmt.Errorf(
"Error reading config for %s: %s",
name,
err)
}
meta := res.Meta()
*meta = params
result = append(result, &Resource{
Name: name,
Kind: kind,
resource: res,
deps: edges,
rcv: rcv,
})
}
return result, nil
}

89
hil/interpolate.go Normal file
View File

@@ -0,0 +1,89 @@
// Mgmt
// Copyright (C) 2013-2017+ 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 hil
import (
"fmt"
"strings"
"github.com/hashicorp/hil/ast"
)
// Variable defines an interpolated variable.
type Variable interface {
Key() string
}
// ResourceVariable defines a variable type used to reference fields of a resource
// e.g. ${file.file1.Content}
type ResourceVariable struct {
Kind, Name, Field string
}
// Key returns a string representation of the variable key.
func (r *ResourceVariable) Key() string {
return fmt.Sprintf("%s.%s.%s", r.Kind, r.Name, r.Field)
}
// NewInterpolatedVariable takes a variable key and return the interpolated variable
// of the required type.
func NewInterpolatedVariable(k string) (Variable, error) {
// for now resource variables are the only thing.
parts := strings.SplitN(k, ".", 3)
return &ResourceVariable{
Kind: parts[0],
Name: parts[1],
Field: parts[2],
}, nil
}
// ParseVariables will traverse a HIL tree looking for variables and returns a
// list of them.
func ParseVariables(tree ast.Node) ([]Variable, error) {
var result []Variable
var finalErr error
visitor := func(n ast.Node) ast.Node {
if finalErr != nil {
return n
}
switch nt := n.(type) {
case *ast.VariableAccess:
v, err := NewInterpolatedVariable(nt.Name)
if err != nil {
finalErr = err
return n
}
result = append(result, v)
default:
return n
}
return n
}
tree.Accept(visitor)
if finalErr != nil {
return nil, finalErr
}
return result, nil
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
@@ -24,6 +24,7 @@ import (
"os/signal"
"syscall"
"github.com/purpleidea/mgmt/hcl"
"github.com/purpleidea/mgmt/puppet"
"github.com/purpleidea/mgmt/yamlgraph"
"github.com/purpleidea/mgmt/yamlgraph2"
@@ -89,6 +90,14 @@ func run(c *cli.Context) error {
PuppetConf: c.String("puppet-conf"),
}
}
if h := c.String("hcl"); c.IsSet("hcl") {
if obj.GAPI != nil {
return fmt.Errorf("can't combine hcl GAPI with existing GAPI")
}
obj.GAPI = &hcl.GAPI{
File: &h,
}
}
obj.Remotes = c.StringSlice("remote") // FIXME: GAPI-ify somehow?
obj.NoWatch = c.Bool("no-watch")
@@ -222,6 +231,11 @@ func CLI(program, version string, flags Flags) error {
Value: "",
Usage: "yaml graph definition to run (parser v2)",
},
cli.StringFlag{
Name: "hcl",
Value: "",
Usage: "hcl graph definition to run",
},
cli.StringFlag{
Name: "puppet, p",
Value: "",

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package lib
@@ -524,6 +524,8 @@ func (obj *Main) Run() error {
continue
}
//savedGraph := oldGraph.Copy() // save a copy for errors
// TODO: should we call each Res.Setup() here instead?
// add autoedges; modifies the graph only if no error
@@ -537,12 +539,20 @@ func (obj *Main) Run() error {
continue
}
graph.Update(oldGraph) // copy in structure of new graph
// at this point, any time we error after a destructive
// modification of the graph we need to restore the old
// graph that was previously running, eg:
//
// oldGraph = savedGraph.Copy()
//
// which we are (luckily) able to avoid testing for now
resources.AutoGroup(graph.Graph, &resources.NonReachabilityGrouper{}) // run autogroup; modifies the graph
resources.AutoGroup(oldGraph, &resources.NonReachabilityGrouper{}) // run autogroup; modifies the graph
// TODO: do we want to do a transitive reduction?
// FIXME: run a type checker that verifies all the send->recv relationships
graph.Update(oldGraph) // copy in structure of new graph
// Call this here because at this point the graph does
// not know anything about the prometheus instance.
if err := prom.UpdatePgraphStartTime(); err != nil {
@@ -619,6 +629,14 @@ func (obj *Main) Run() error {
// TODO: is there any benefit to running the remotes above in the loop?
// wait for etcd to be running before we remote in, which we do above!
go remotes.Run()
// wait for remotes to be ready before continuing...
select {
case <-remotes.Ready():
log.Printf("Main: Remotes: Run: Ready!")
// pass
//case <-time.After( ? * time.Second):
// obj.Exit(fmt.Errorf("Main: Remotes: Run timeout"))
}
if obj.GAPI == nil {
converger.Start() // better start this for empty graphs

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main

View File

@@ -80,5 +80,5 @@ if [[ $ret != 0 ]]; then
fi
go get golang.org/x/tools/cmd/stringer # for automatic stringer-ing
go get github.com/golang/lint/golint # for `golint`-ing
go get github.com/alecthomas/gometalinter && gometalinter --install # bonus
go get -u gopkg.in/alecthomas/gometalinter.v1 && mv "$(dirname $(which gometalinter.v1))/gometalinter.v1" "$(dirname $(which gometalinter.v1))/gometalinter" && gometalinter --install # bonus
cd "$XPWD" >/dev/null

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgp

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgraph
@@ -23,12 +23,25 @@ import (
errwrap "github.com/pkg/errors"
)
func strVertexCmpFn(v1, v2 Vertex) (bool, error) {
if v1.String() == "" || v2.String() == "" {
return false, fmt.Errorf("empty vertex")
}
return v1.String() == v2.String(), nil
}
func strEdgeCmpFn(e1, e2 Edge) (bool, error) {
if e1.String() == "" || e2.String() == "" {
return false, fmt.Errorf("empty edge")
}
return e1.String() == e2.String(), nil
}
// GraphSync updates the Graph so that it matches the newGraph. It leaves
// identical elements alone so that they don't need to be refreshed.
// It tries to mutate existing elements into new ones, if they support this.
// This updates the Graph on success only.
// FIXME: should we do this with copies of the vertex resources?
// FIXME: add test cases
func (obj *Graph) GraphSync(newGraph *Graph, vertexCmpFn func(Vertex, Vertex) (bool, error), vertexAddFn func(Vertex) error, vertexRemoveFn func(Vertex) error, edgeCmpFn func(Edge, Edge) (bool, error)) error {
oldGraph := obj.Copy() // work on a copy of the old graph
@@ -41,6 +54,19 @@ func (obj *Graph) GraphSync(newGraph *Graph, vertexCmpFn func(Vertex, Vertex) (b
}
oldGraph.SetName(newGraph.GetName()) // overwrite the name
if vertexCmpFn == nil {
vertexCmpFn = strVertexCmpFn // use simple string cmp version
}
if vertexAddFn == nil {
vertexAddFn = func(Vertex) error { return nil } // noop
}
if vertexRemoveFn == nil {
vertexRemoveFn = func(Vertex) error { return nil } // noop
}
if edgeCmpFn == nil {
edgeCmpFn = strEdgeCmpFn // use simple string cmp version
}
var lookup = make(map[Vertex]Vertex)
var vertexKeep []Vertex // list of vertices which are the same in new graph
var edgeKeep []Edge // list of vertices which are the same in new graph

92
pgraph/graphsync_test.go Normal file
View File

@@ -0,0 +1,92 @@
// Mgmt
// Copyright (C) 2013-2017+ 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 pgraph
import (
"testing"
)
func TestGraphSync1(t *testing.T) {
g := &Graph{}
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
e1 := NE("e1")
e2 := NE("e2")
g.AddEdge(v1, v3, e1)
g.AddEdge(v2, v3, e2)
// new graph
newGraph := &Graph{}
v4 := NV("v4")
v5 := NV("v5")
e3 := NE("e3")
newGraph.AddEdge(v4, v5, e3)
err := g.GraphSync(newGraph, nil, nil, nil, nil)
if err != nil {
t.Errorf("GraphSync failed: %v", err)
return
}
// g should change and become the same
if s := runGraphCmp(t, g, newGraph); s != "" {
t.Errorf("%s", s)
}
}
func TestGraphSync2(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
v4 := NV("v4")
v5 := NV("v5")
e1 := NE("e1")
e2 := NE("e2")
e3 := NE("e3")
g := &Graph{}
g.AddEdge(v1, v3, e1)
g.AddEdge(v2, v3, e2)
// new graph
newGraph := &Graph{}
newGraph.AddEdge(v1, v3, e1)
newGraph.AddEdge(v2, v3, e2)
newGraph.AddEdge(v4, v5, e3)
//newGraph.AddEdge(v3, v4, NE("v3,v4"))
//newGraph.AddEdge(v3, v5, NE("v3,v5"))
// graphs should differ!
if runGraphCmp(t, g, newGraph) == "" {
t.Errorf("graphs should differ initially")
return
}
err := g.GraphSync(newGraph, strVertexCmpFn, vertexAddFn, vertexRemoveFn, strEdgeCmpFn)
if err != nil {
t.Errorf("GraphSync failed: %v", err)
return
}
// g should change and become the same
if s := runGraphCmp(t, g, newGraph); s != "" {
t.Errorf("%s", s)
}
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgraph // TODO: this should be a subpackage

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package pgraph represents the internal "pointer graph" that we use.
@@ -142,7 +142,6 @@ func (g *Graph) AddEdge(v1, v2 Vertex, e Edge) {
}
// DeleteEdge deletes a particular edge from the graph.
// FIXME: add test cases
func (g *Graph) DeleteEdge(e Edge) {
for v1 := range g.adjacency {
for v2, edge := range g.adjacency[v1] {

View File

@@ -3,58 +3,26 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgraph
import (
"fmt"
"reflect"
"testing"
)
// vertex is a test struct to test the library.
type vertex struct {
name string
}
// String is a required method of the Vertex interface that we must fulfill.
func (v *vertex) String() string {
return v.name
}
// NV is a helper function to make testing easier. It creates a new noop vertex.
func NV(s string) Vertex {
return &vertex{s}
}
// edge is a test struct to test the library.
type edge struct {
name string
}
// String is a required method of the Edge interface that we must fulfill.
func (e *edge) String() string {
return e.name
}
// NE is a helper function to make testing easier. It creates a new noop edge.
func NE(s string) Edge {
return &edge{s}
}
func TestPgraphT1(t *testing.T) {
func TestCount1(t *testing.T) {
G := &Graph{}
if i := G.NumVertices(); i != 0 {
@@ -79,8 +47,7 @@ func TestPgraphT1(t *testing.T) {
}
}
func TestPgraphT2(t *testing.T) {
func TestAddVertex1(t *testing.T) {
G := &Graph{Name: "g2"}
v1 := NV("v1")
v2 := NV("v2")
@@ -106,8 +73,7 @@ func TestPgraphT2(t *testing.T) {
}
}
func TestPgraphT3(t *testing.T) {
func TestDFS1(t *testing.T) {
G, _ := NewGraph("g3")
v1 := NV("v1")
v2 := NV("v2")
@@ -147,8 +113,7 @@ func TestPgraphT3(t *testing.T) {
}
}
func TestPgraphT4(t *testing.T) {
func TestDFS2(t *testing.T) {
G, _ := NewGraph("g4")
v1 := NV("v1")
v2 := NV("v2")
@@ -170,7 +135,7 @@ func TestPgraphT4(t *testing.T) {
}
}
func TestPgraphT5(t *testing.T) {
func TestFilterGraph1(t *testing.T) {
G, _ := NewGraph("g5")
v1 := NV("v1")
v2 := NV("v2")
@@ -203,7 +168,7 @@ func TestPgraphT5(t *testing.T) {
}
}
func TestPgraphT6(t *testing.T) {
func TestDisconnectedGraphs1(t *testing.T) {
G, _ := NewGraph("g6")
v1 := NV("v1")
v2 := NV("v2")
@@ -235,8 +200,7 @@ func TestPgraphT6(t *testing.T) {
}
}
func TestPgraphT7(t *testing.T) {
func TestDeleteVertex1(t *testing.T) {
G, _ := NewGraph("g7")
v1 := NV("v1")
v2 := NV("v2")
@@ -277,8 +241,17 @@ func TestPgraphT7(t *testing.T) {
}
}
func TestPgraphT8(t *testing.T) {
func TestDeleteVertex2(t *testing.T) {
G := &Graph{}
v1 := NV("v1")
G.DeleteVertex(v1) // check this doesn't panic
if i := G.NumVertices(); i != 0 {
t.Errorf("should have 0 vertices instead of: %d", i)
}
}
func TestVertexContains1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -306,8 +279,7 @@ func TestPgraphT8(t *testing.T) {
}
}
func TestPgraphT9(t *testing.T) {
func TestTopoSort1(t *testing.T) {
G, _ := NewGraph("g9")
v1 := NV("v1")
v2 := NV("v2")
@@ -382,8 +354,7 @@ func TestPgraphT9(t *testing.T) {
}
}
func TestPgraphT10(t *testing.T) {
func TestTopoSort2(t *testing.T) {
G, _ := NewGraph("g10")
v1 := NV("v1")
v2 := NV("v2")
@@ -410,7 +381,7 @@ func TestPgraphT10(t *testing.T) {
}
// empty
func TestPgraphReachability0(t *testing.T) {
func TestReachability0(t *testing.T) {
{
G, _ := NewGraph("g")
result := G.Reachability(nil, nil)
@@ -474,7 +445,7 @@ func TestPgraphReachability0(t *testing.T) {
}
// simple linear path
func TestPgraphReachability1(t *testing.T) {
func TestReachability1(t *testing.T) {
G, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
@@ -508,7 +479,7 @@ func TestPgraphReachability1(t *testing.T) {
}
// pick one of two correct paths
func TestPgraphReachability2(t *testing.T) {
func TestReachability2(t *testing.T) {
G, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
@@ -545,7 +516,7 @@ func TestPgraphReachability2(t *testing.T) {
}
// pick shortest path
func TestPgraphReachability3(t *testing.T) {
func TestReachability3(t *testing.T) {
G, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
@@ -580,7 +551,7 @@ func TestPgraphReachability3(t *testing.T) {
}
// direct path
func TestPgraphReachability4(t *testing.T) {
func TestReachability4(t *testing.T) {
G, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
@@ -614,7 +585,7 @@ func TestPgraphReachability4(t *testing.T) {
}
}
func TestPgraphT11(t *testing.T) {
func TestReverse1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -639,7 +610,7 @@ func TestPgraphT11(t *testing.T) {
}
}
func TestPgraphCopy1(t *testing.T) {
func TestCopy1(t *testing.T) {
g1 := &Graph{}
g2 := g1.Copy() // check this doesn't panic
if !reflect.DeepEqual(g1.String(), g2.String()) {
@@ -647,31 +618,7 @@ func TestPgraphCopy1(t *testing.T) {
}
}
func TestPgraphDelete1(t *testing.T) {
G := &Graph{}
v1 := NV("v1")
G.DeleteVertex(v1) // check this doesn't panic
if i := G.NumVertices(); i != 0 {
t.Errorf("should have 0 vertices instead of: %d", i)
}
}
func vertexCmpFn(v1, v2 Vertex) (bool, error) {
if v1.String() == "" || v2.String() == "" {
return false, fmt.Errorf("oops, empty vertex")
}
return v1.String() == v2.String(), nil
}
func edgeCmpFn(e1, e2 Edge) (bool, error) {
if e1.String() == "" || e2.String() == "" {
return false, fmt.Errorf("oops, empty edge")
}
return e1.String() == e2.String(), nil
}
func TestPgraphGraphCmp1(t *testing.T) {
func TestGraphCmp1(t *testing.T) {
g1 := &Graph{}
g2 := &Graph{}
g3 := &Graph{}
@@ -679,20 +626,20 @@ func TestPgraphGraphCmp1(t *testing.T) {
g4 := &Graph{}
g4.AddVertex(NV("v2"))
if err := g1.GraphCmp(g2, vertexCmpFn, edgeCmpFn); err != nil {
if err := g1.GraphCmp(g2, strVertexCmpFn, strEdgeCmpFn); err != nil {
t.Errorf("should have no error during GraphCmp, but got: %v", err)
}
if err := g1.GraphCmp(g3, vertexCmpFn, edgeCmpFn); err == nil {
if err := g1.GraphCmp(g3, strVertexCmpFn, strEdgeCmpFn); err == nil {
t.Errorf("should have error during GraphCmp, but got nil")
}
if err := g3.GraphCmp(g4, vertexCmpFn, edgeCmpFn); err == nil {
if err := g3.GraphCmp(g4, strVertexCmpFn, strEdgeCmpFn); err == nil {
t.Errorf("should have error during GraphCmp, but got nil")
}
}
func TestPgraphSort0(t *testing.T) {
func TestSort0(t *testing.T) {
vs := []Vertex{}
s := Sort(vs)
@@ -710,7 +657,7 @@ func TestPgraphSort0(t *testing.T) {
}
}
func TestPgraphSort1(t *testing.T) {
func TestSort1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -739,3 +686,84 @@ func TestPgraphSort1(t *testing.T) {
t.Errorf(str)
}
}
func TestDeleteEdge1(t *testing.T) {
g, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
e1 := NE("e1")
e2 := NE("e2")
e3 := NE("e3")
e4 := NE("e4")
e5 := NE("e5")
e6 := NE("e6")
g.AddEdge(v1, v2, e1)
g.AddEdge(v2, v3, e2)
g.AddEdge(v1, v3, e3)
g.AddEdge(v2, v1, e4)
g.AddEdge(v3, v2, e5)
g.AddEdge(v3, v1, e6)
g.DeleteEdge(e1)
g.DeleteEdge(e2)
g.DeleteEdge(e3)
g.DeleteEdge(e3)
if g.NumEdges() != 3 {
t.Errorf("expected number of edges: 3, instead of: %d", g.NumEdges())
}
}
func TestDeleteEdge2(t *testing.T) {
g, _ := NewGraph("g")
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
v4 := NV("v4")
e1 := NE("e1")
e2 := NE("e2")
e3 := NE("e3")
e4 := NE("e4")
e5 := NE("e5")
e6 := NE("e6")
e7 := NE("e7")
g.AddEdge(v1, v2, e1)
g.AddEdge(v1, v2, e2)
g.AddEdge(v1, v3, e3)
g.AddEdge(v1, v4, e4)
g.AddEdge(v2, v1, e5)
g.AddEdge(v3, v1, e6)
g.AddEdge(v4, v1, e7)
g.DeleteEdge(e1)
g.DeleteEdge(e2)
g.DeleteEdge(e3)
g.DeleteEdge(e5)
g.DeleteEdge(e6)
ie := g.IncomingGraphEdges(v1)
oe := g.OutgoingGraphEdges(v1)
if !reflect.DeepEqual(ie, []Edge{e7}) {
res := ""
for _, e := range ie {
res += e.String() + " "
}
t.Errorf("expected incoming graph edges for vertex v1: e7, instead of: %s", res)
}
if !reflect.DeepEqual(oe, []Edge{e4}) {
res := ""
for _, e := range oe {
res += e.String() + " "
}
t.Errorf("expected outgoing graph edges for vertex v1: e4, instead of: %s", res)
}
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgraph

View File

@@ -3,58 +3,25 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package pgraph
import (
"fmt"
"testing"
)
// TODO: unify with the other function like this...
// TODO: where should we put our test helpers?
func runGraphCmp(t *testing.T, g1, g2 *Graph) {
err := g1.GraphCmp(g2, vertexCmpFn, edgeCmpFn)
if err != nil {
t.Logf(" actual (g1): %v%v", g1, fullPrint(g1))
t.Logf("expected (g2): %v%v", g2, fullPrint(g2))
t.Logf("Cmp error:")
t.Errorf("%v", err)
}
}
// TODO: unify with the other function like this...
func fullPrint(g *Graph) (str string) {
str += "\n"
for v := range g.Adjacency() {
str += fmt.Sprintf("* v: %s\n", v)
}
for v1 := range g.Adjacency() {
for v2, e := range g.Adjacency()[v1] {
str += fmt.Sprintf("* e: %s -> %s # %s\n", v1, v2, e)
}
}
return
}
// edgeGenFn generates unique edges for each vertex pair, assuming unique
// vertices.
func edgeGenFn(v1, v2 Vertex) Edge {
return NE(fmt.Sprintf("%s,%s", v1, v2))
}
func TestPgraphAddEdgeGraph1(t *testing.T) {
func TestAddEdgeGraph1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -82,10 +49,12 @@ func TestPgraphAddEdgeGraph1(t *testing.T) {
//expected.AddEdge(v3, v4, NE("v3,v4"))
//expected.AddEdge(v3, v5, NE("v3,v5"))
runGraphCmp(t, g, expected)
if s := runGraphCmp(t, g, expected); s != "" {
t.Errorf("%s", s)
}
}
func TestPgraphAddEdgeVertexGraph1(t *testing.T) {
func TestAddEdgeVertexGraph1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -113,10 +82,12 @@ func TestPgraphAddEdgeVertexGraph1(t *testing.T) {
expected.AddEdge(v3, v4, NE("v3,v4"))
expected.AddEdge(v3, v5, NE("v3,v5"))
runGraphCmp(t, g, expected)
if s := runGraphCmp(t, g, expected); s != "" {
t.Errorf("%s", s)
}
}
func TestPgraphAddEdgeGraphVertex1(t *testing.T) {
func TestAddEdgeGraphVertex1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -144,10 +115,12 @@ func TestPgraphAddEdgeGraphVertex1(t *testing.T) {
expected.AddEdge(v4, v3, NE("v4,v3"))
expected.AddEdge(v5, v3, NE("v5,v3"))
runGraphCmp(t, g, expected)
if s := runGraphCmp(t, g, expected); s != "" {
t.Errorf("%s", s)
}
}
func TestPgraphAddEdgeVertexGraphLight1(t *testing.T) {
func TestAddEdgeVertexGraphLight1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -175,10 +148,12 @@ func TestPgraphAddEdgeVertexGraphLight1(t *testing.T) {
expected.AddEdge(v3, v4, NE("v3,v4"))
//expected.AddEdge(v3, v5, NE("v3,v5")) // not needed with light
runGraphCmp(t, g, expected)
if s := runGraphCmp(t, g, expected); s != "" {
t.Errorf("%s", s)
}
}
func TestPgraphAddEdgeGraphVertexLight1(t *testing.T) {
func TestAddEdgeGraphVertexLight1(t *testing.T) {
v1 := NV("v1")
v2 := NV("v2")
v3 := NV("v3")
@@ -206,5 +181,7 @@ func TestPgraphAddEdgeGraphVertexLight1(t *testing.T) {
//expected.AddEdge(v4, v3, NE("v4,v3")) // not needed with light
expected.AddEdge(v5, v3, NE("v5,v3"))
runGraphCmp(t, g, expected)
if s := runGraphCmp(t, g, expected); s != "" {
t.Errorf("%s", s)
}
}

93
pgraph/util_test.go Normal file
View File

@@ -0,0 +1,93 @@
// Mgmt
// Copyright (C) 2013-2017+ 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 pgraph
import (
"fmt"
"testing"
)
// vertex is a test struct to test the library.
type vertex struct {
name string
}
// String is a required method of the Vertex interface that we must fulfill.
func (v *vertex) String() string {
return v.name
}
// NV is a helper function to make testing easier. It creates a new noop vertex.
func NV(s string) Vertex {
return &vertex{s}
}
// edge is a test struct to test the library.
type edge struct {
name string
}
// String is a required method of the Edge interface that we must fulfill.
func (e *edge) String() string {
return e.name
}
// NE is a helper function to make testing easier. It creates a new noop edge.
func NE(s string) Edge {
return &edge{s}
}
// edgeGenFn generates unique edges for each vertex pair, assuming unique
// vertices.
func edgeGenFn(v1, v2 Vertex) Edge {
return NE(fmt.Sprintf("%s,%s", v1, v2))
}
func vertexAddFn(v Vertex) error {
return nil
}
func vertexRemoveFn(v Vertex) error {
return nil
}
func runGraphCmp(t *testing.T, g1, g2 *Graph) string {
err := g1.GraphCmp(g2, strVertexCmpFn, strEdgeCmpFn)
if err != nil {
str := ""
str += fmt.Sprintf(" actual (g1): %v%s", g1, fullPrint(g1))
str += fmt.Sprintf("expected (g2): %v%s", g2, fullPrint(g2))
str += fmt.Sprintf("cmp error:")
str += fmt.Sprintf("%v", err)
return str
}
return ""
}
func fullPrint(g *Graph) (str string) {
str += "\n"
for v := range g.Adjacency() {
str += fmt.Sprintf("* v: %s\n", v)
}
for v1 := range g.Adjacency() {
for v2, e := range g.Adjacency()[v1] {
str += fmt.Sprintf("* e: %s -> %s # %s\n", v1, v2, e)
}
}
return
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package prometheus provides functions that are useful to control and manage

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package puppet

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package puppet provides the integration entrypoint for the puppet language.

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package recwatch

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package recwatch

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package recwatch provides recursive file watching events via fsnotify.

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package remote provides the remoting facilities for agentless execution.
@@ -692,7 +692,7 @@ type Remotes struct {
fileWatch chan string
cConns uint16 // number of concurrent ssh connections, zero means unlimited
interactive bool // allow interactive prompting
sshPrivIdRsa string // path to ~/.ssh/id_rsa
sshPrivIDRsa string // path to ~/.ssh/id_rsa
caching bool // whether to try and cache the copy of the binary
depth uint16 // depth of this node in the remote execution hierarchy
prefix string // folder prefix to use for misc storage
@@ -702,6 +702,7 @@ type Remotes struct {
wg sync.WaitGroup // keep track of each running SSH connection
lock sync.Mutex // mutex for access to sshmap
sshmap map[string]*SSH // map to each SSH struct with the remote as the key
running chan struct{} // closes when main loop is running
exiting bool // flag to let us know if we're exiting
exitChan chan struct{} // closes when we should exit
semaphore *semaphore.Semaphore // counting semaphore to limit concurrent connections
@@ -714,7 +715,7 @@ type Remotes struct {
}
// NewRemotes builds a Remotes struct.
func NewRemotes(clientURLs, remoteURLs []string, noop bool, remotes []string, fileWatch chan string, cConns uint16, interactive bool, sshPrivIdRsa string, caching bool, depth uint16, prefix string, converger cv.Converger, convergerCb func(func(map[string]bool) error) (func(), error), flags Flags) *Remotes {
func NewRemotes(clientURLs, remoteURLs []string, noop bool, remotes []string, fileWatch chan string, cConns uint16, interactive bool, sshPrivIDRsa string, caching bool, depth uint16, prefix string, converger cv.Converger, convergerCb func(func(map[string]bool) error) (func(), error), flags Flags) *Remotes {
return &Remotes{
clientURLs: clientURLs,
remoteURLs: remoteURLs,
@@ -723,13 +724,14 @@ func NewRemotes(clientURLs, remoteURLs []string, noop bool, remotes []string, fi
fileWatch: fileWatch,
cConns: cConns,
interactive: interactive,
sshPrivIdRsa: sshPrivIdRsa,
sshPrivIDRsa: sshPrivIDRsa,
caching: caching,
depth: depth,
prefix: prefix,
converger: converger,
convergerCb: convergerCb,
sshmap: make(map[string]*SSH),
running: make(chan struct{}),
exitChan: make(chan struct{}),
semaphore: semaphore.NewSemaphore(int(cConns)),
hostnames: make([]string, len(remotes)),
@@ -830,18 +832,18 @@ func (obj *Remotes) NewSSH(file string) (*SSH, error) {
// sshKeyAuth is a helper function to get the ssh key auth struct needed
func (obj *Remotes) sshKeyAuth() (ssh.AuthMethod, error) {
if obj.sshPrivIdRsa == "" {
if obj.sshPrivIDRsa == "" {
return nil, fmt.Errorf("empty path specified")
}
p := ""
// TODO: this doesn't match strings of the form: ~james/.ssh/id_rsa
if strings.HasPrefix(obj.sshPrivIdRsa, "~/") {
if strings.HasPrefix(obj.sshPrivIDRsa, "~/") {
usr, err := user.Current()
if err != nil {
log.Printf("Remote: Can't find home directory automatically.")
return nil, err
}
p = path.Join(usr.HomeDir, obj.sshPrivIdRsa[len("~/"):])
p = path.Join(usr.HomeDir, obj.sshPrivIDRsa[len("~/"):])
}
if p == "" {
return nil, fmt.Errorf("empty path specified")
@@ -1022,11 +1024,17 @@ func (obj *Remotes) Run() {
}(sshobj, f)
obj.lock.Unlock()
}
close(obj.running) // notify
}
// Ready closes its returned channel when the Run method is up and ready. It is
// useful to know when ready, since we often execute Run in a go routine.
func (obj *Remotes) Ready() <-chan struct{} { return obj.running }
// Exit causes as much of the Remotes struct to shutdown as quickly and as
// cleanly as possible. It only returns once everything is shutdown.
func (obj *Remotes) Exit() error {
<-obj.running // wait for Run to be finished before we exit!
obj.lock.Lock()
obj.exiting = true // don't spawn new ones once this flag is set!
obj.lock.Unlock()

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,23 +3,23 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !noaugeas
package resources
import (
"encoding/gob"
"fmt"
"log"
"os"
@@ -39,7 +39,6 @@ const (
)
func init() {
gob.Register(&AugeasRes{})
RegisterResource("augeas", func() Res { return &AugeasRes{} })
}
@@ -94,7 +93,6 @@ func (obj *AugeasRes) Validate() error {
// Init initiates the resource.
func (obj *AugeasRes) Init() error {
obj.BaseRes.Kind = "augeas"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,17 +3,18 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build noaugeas
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -108,6 +108,7 @@ func NewNoopResTest(name string) *NoopResTest {
NoopRes: NoopRes{
BaseRes: BaseRes{
Name: name,
Kind: "noop",
MetaParams: MetaParams{
AutoGroup: true, // always autogroup
},

653
resources/aws_ec2.go Normal file
View File

@@ -0,0 +1,653 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
errwrap "github.com/pkg/errors"
)
func init() {
RegisterResource("aws:ec2", func() Res { return &AwsEc2Res{} })
}
// AwsPrefix is a const which gets prepended onto the instance name.
const AwsPrefix = "mgmt:"
// AwsRegions is a list of all AWS regions generated using ec2.DescribeRegions.
// cn-north-1 and us-gov-west-1 are not returned, probably due to security.
// List available at http://docs.aws.amazon.com/general/latest/gr/rande.html
var AwsRegions = []string{
"ap-northeast-1",
"ap-northeast-2",
"ap-south-1",
"ap-southeast-1",
"ap-southeast-2",
"ca-central-1",
"cn-north-1",
"eu-central-1",
"eu-west-1",
"eu-west-2",
"sa-east-1",
"us-east-1",
"us-east-2",
"us-gov-west-1",
"us-west-1",
"us-west-2",
}
// AwsEc2Res is an AWS EC2 resource. In order to create a client session, your
// AWS credentials must be present in ~/.aws - For detailed instructions see
// http://docs.aws.amazon.com/cli/latest/userguide/cli-config-files.html
type AwsEc2Res struct {
BaseRes `yaml:",inline"`
State string `yaml:"state"` // state: running, stopped, terminated
Region string `yaml:"region"`
Type string `yaml:"type"` // the ec2 instance type, ie. t2.micro
ImageID string `yaml:"imageid"`
client *ec2.EC2 // client session for AWS API calls
}
// Default returns some sensible defaults for this resource.
func (obj *AwsEc2Res) Default() Res {
return &AwsEc2Res{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
}
// Validate if the params passed in are valid data.
func (obj *AwsEc2Res) Validate() error {
if obj.State != "running" && obj.State != "stopped" && obj.State != "terminated" {
return fmt.Errorf("state must be 'running', 'stopped' or 'terminated'")
}
// compare obj.Region to the list of available AWS endpoints.
validRegion := false
for _, region := range AwsRegions {
if obj.Region == region {
validRegion = true
break
}
}
if !validRegion {
return fmt.Errorf("region must be a valid AWS endpoint")
}
// check the instance type
// there is currently no api call to enumerate available instance types
if obj.Type == "" {
return fmt.Errorf("no instance type specified")
}
// check imageId against a list of available images
sess, err := session.NewSession(&aws.Config{
Region: aws.String(obj.Region),
})
if err != nil {
return errwrap.Wrapf(err, "error creating session")
}
client := ec2.New(sess)
imagesInput := &ec2.DescribeImagesInput{}
images, err := client.DescribeImages(imagesInput)
if err != nil {
return errwrap.Wrapf(err, "error describing images")
}
validImage := false
for _, image := range images.Images {
if obj.ImageID == *image.ImageId {
validImage = true
break
}
}
if !validImage {
return fmt.Errorf("imageid must be a valid ami available in the specified region")
}
return obj.BaseRes.Validate()
}
// Init initializes the resource.
func (obj *AwsEc2Res) Init() error {
// create a client session for the AWS API
sess, err := session.NewSession(&aws.Config{
Region: aws.String(obj.Region),
})
if err != nil {
return errwrap.Wrapf(err, "error creating session")
}
obj.client = ec2.New(sess)
return obj.BaseRes.Init() // call base init, b/c we're overriding
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *AwsEc2Res) Watch() error {
send := false
var exit *error
if err := obj.Running(); err != nil {
return err
}
type chanStruct struct {
str string
err error
}
awsChan := make(chan *chanStruct)
closeChan := make(chan struct{})
ctx, cancel := context.WithCancel(context.TODO())
wg := &sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
select {
case <-closeChan:
cancel()
}
}()
wg.Add(1)
defer wg.Wait()
go func() {
defer wg.Done()
defer close(awsChan)
for {
diInput := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:Name"),
Values: []*string{aws.String(obj.prependName())},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String("pending"),
aws.String("running"),
aws.String("stopping"),
aws.String("stopped"),
},
},
},
}
diOutput, err := obj.client.DescribeInstances(diInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
if obj.State == "running" {
stoppedInput := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:Name"),
Values: []*string{aws.String(obj.prependName())},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String("stopped"),
},
},
},
}
stoppedOutput, err := obj.client.DescribeInstances(stoppedInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
if len(diOutput.Reservations) == 1 && len(stoppedOutput.Reservations) == 0 {
waitInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{diOutput.Reservations[0].Instances[0].InstanceId},
Filters: []*ec2.Filter{
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String("stopped"),
aws.String("terminated"),
},
},
},
}
log.Printf("%s: Watching: %s", obj, *diOutput.Reservations[0].Instances[0].InstanceId)
if err := obj.client.WaitUntilInstanceStoppedWithContext(ctx, waitInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
log.Printf("%s: Request cancelled", obj)
}
}
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "unknown error waiting for instance to stop"),
}:
case <-closeChan:
}
return
}
stateOutput, err := obj.client.DescribeInstances(diInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
var stateName string
if len(stateOutput.Reservations) == 1 {
stateName = *stateOutput.Reservations[0].Instances[0].State.Name
}
if len(stateOutput.Reservations) == 0 || (len(stateOutput.Reservations) == 1 && stateName != "running") {
select {
case awsChan <- &chanStruct{
str: "stopped",
}:
case <-closeChan:
return
}
}
}
}
if obj.State == "stopped" {
runningInput := &ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:Name"),
Values: []*string{aws.String(obj.prependName())},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String("running"),
},
},
},
}
runningOutput, err := obj.client.DescribeInstances(runningInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
if len(diOutput.Reservations) == 1 && len(runningOutput.Reservations) == 0 {
waitInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{diOutput.Reservations[0].Instances[0].InstanceId},
Filters: []*ec2.Filter{
{
Name: aws.String("instance-state-name"),
Values: []*string{aws.String("running")},
},
},
}
log.Printf("%s: watching: %s", obj, *diOutput.Reservations[0].Instances[0].InstanceId)
if err := obj.client.WaitUntilInstanceRunningWithContext(ctx, waitInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
log.Printf("%s: Request cancelled", obj)
}
}
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "unknown error waiting for instance to start"),
}:
case <-closeChan:
}
return
}
stateOutput, err := obj.client.DescribeInstances(diInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
var stateName string
if len(stateOutput.Reservations) == 1 {
stateName = *stateOutput.Reservations[0].Instances[0].State.Name
}
if len(stateOutput.Reservations) == 0 || (len(stateOutput.Reservations) == 1 && stateName != "stopped") {
select {
case awsChan <- &chanStruct{
str: "running",
}:
case <-closeChan:
return
}
}
}
}
if obj.State == "terminated" {
obj.client.WaitUntilInstanceExistsWithContext(ctx, diInput)
if err := obj.client.WaitUntilInstanceExistsWithContext(ctx, diInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
log.Printf("%s: Request cancelled", obj)
}
}
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "unknown error waiting for instance to exist"),
}:
case <-closeChan:
}
return
}
stateOutput, err := obj.client.DescribeInstances(diInput)
if err != nil {
select {
case awsChan <- &chanStruct{
err: errwrap.Wrapf(err, "error describing instances"),
}:
case <-closeChan:
}
return
}
if len(stateOutput.Reservations) == 1 {
{
select {
case awsChan <- &chanStruct{
str: "exists",
}:
case <-closeChan:
return
}
}
}
}
select {
case <-closeChan:
return
default:
}
}
}()
for {
select {
case event := <-obj.Events():
if exit, send = obj.ReadEvent(event); exit != nil {
close(closeChan)
return *exit
}
case msg, ok := <-awsChan:
if !ok {
return *exit
}
if err := msg.err; err != nil {
return err
}
log.Printf("%s: State: %s", obj, msg.str)
obj.StateOK(false)
send = true
}
if send {
send = false
obj.Event()
}
}
}
// CheckApply method for AwsEc2 resource.
func (obj *AwsEc2Res) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply)
const waitTimeout = 400
diInput := ec2.DescribeInstancesInput{
Filters: []*ec2.Filter{
{
Name: aws.String("tag:Name"),
Values: []*string{aws.String(obj.prependName())},
},
{
Name: aws.String("instance-state-name"),
Values: []*string{
aws.String("running"),
aws.String("pending"),
aws.String("stopped"),
aws.String("stopping"),
},
},
},
}
diOutput, err := obj.client.DescribeInstances(&diInput)
if err != nil {
return false, errwrap.Wrapf(err, "error describing instances")
}
if len(diOutput.Reservations) < 1 && obj.State == "terminated" {
return true, nil
}
if len(diOutput.Reservations) == 1 && *diOutput.Reservations[0].Instances[0].State.Name == obj.State {
return true, nil
}
if !apply {
return false, nil
}
if len(diOutput.Reservations) > 1 {
return false, fmt.Errorf("too many reservations")
}
ctx, cancel := context.WithTimeout(context.TODO(), waitTimeout*time.Second)
defer cancel()
if len(diOutput.Reservations) == 1 {
instanceID := diOutput.Reservations[0].Instances[0].InstanceId
describeInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{instanceID},
}
if len(diOutput.Reservations[0].Instances) > 1 {
return false, fmt.Errorf("more than one instance was returned")
}
if obj.State == "running" {
startInput := &ec2.StartInstancesInput{
InstanceIds: []*string{instanceID},
}
_, err := obj.client.StartInstances(startInput)
if err != nil {
return false, errwrap.Wrapf(err, "error starting instance")
}
if err := obj.client.WaitUntilInstanceRunningWithContext(ctx, describeInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
return false, errwrap.Wrapf(err, "timeout while waiting for instance to start")
}
}
return false, errwrap.Wrapf(err, "unknown error waiting for instance to start")
}
log.Printf("%s: instance running", obj)
}
if obj.State == "stopped" {
stopInput := &ec2.StopInstancesInput{
InstanceIds: []*string{instanceID},
}
_, err := obj.client.StopInstances(stopInput)
if err != nil {
return false, errwrap.Wrapf(err, "error stopping instance")
}
if err := obj.client.WaitUntilInstanceStoppedWithContext(ctx, describeInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
return false, errwrap.Wrapf(err, "timeout while waiting for instance to stop")
}
}
return false, errwrap.Wrapf(err, "unknown error waiting for instance to stop")
}
log.Printf("%s: instance stopped", obj)
}
if obj.State == "terminated" {
terminateInput := &ec2.TerminateInstancesInput{
InstanceIds: []*string{instanceID},
}
_, err := obj.client.TerminateInstances(terminateInput)
if err != nil {
return false, errwrap.Wrapf(err, "error terminating instance")
}
if err := obj.client.WaitUntilInstanceTerminatedWithContext(ctx, describeInput); err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
return false, errwrap.Wrapf(err, "timeout while waiting for instance to terminate")
}
}
return false, errwrap.Wrapf(err, "unknown error waiting for instance to terminate")
}
log.Printf("%s: instance terminated", obj)
}
}
if len(diOutput.Reservations) < 1 && obj.State == "running" {
runParams := &ec2.RunInstancesInput{
ImageId: aws.String(obj.ImageID),
InstanceType: aws.String(obj.Type),
}
runParams.SetMinCount(1)
runParams.SetMaxCount(1)
runResult, err := obj.client.RunInstances(runParams)
if err != nil {
return false, errwrap.Wrapf(err, "could not create instance")
}
_, err = obj.client.CreateTags(&ec2.CreateTagsInput{
Resources: []*string{runResult.Instances[0].InstanceId},
Tags: []*ec2.Tag{
&ec2.Tag{
Key: aws.String("Name"),
Value: aws.String(obj.prependName()),
},
},
})
if err != nil {
return false, errwrap.Wrapf(err, "could not create tags for instance")
}
describeInput := &ec2.DescribeInstancesInput{
InstanceIds: []*string{runResult.Instances[0].InstanceId},
}
err = obj.client.WaitUntilInstanceRunningWithContext(ctx, describeInput)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
if aerr.Code() == request.CanceledErrorCode {
return false, errwrap.Wrapf(err, "timeout while waiting for instance to start")
}
}
return false, errwrap.Wrapf(err, "unknown error waiting for instance to start")
}
log.Printf("%s: instance running", obj)
}
return false, nil
}
// AwsEc2UID is the UID struct for AwsEc2Res.
type AwsEc2UID struct {
BaseUID
name string
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *AwsEc2Res) UIDs() []ResUID {
x := &AwsEc2UID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *AwsEc2Res) GroupCmp(r Res) bool {
_, ok := r.(*AwsEc2Res)
if !ok {
return false
}
return false
}
// Compare two resources and return if they are equivalent.
func (obj *AwsEc2Res) Compare(r Res) bool {
// we can only compare AwsEc2Res to others of the same resource kind
res, ok := r.(*AwsEc2Res)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if obj.Region != res.Region {
return false
}
if obj.Type != res.Type {
return false
}
if obj.ImageID != res.ImageID {
return false
}
return true
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *AwsEc2Res) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes AwsEc2Res // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*AwsEc2Res) // put in the right format
if !ok {
return fmt.Errorf("could not convert to AwsEc2Res")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = AwsEc2Res(raw) // restore from indirection with type conversion!
return nil
}
func (obj *AwsEc2Res) prependName() string {
return AwsPrefix + obj.GetName()
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -20,7 +20,6 @@ package resources
import (
"bufio"
"bytes"
"encoding/gob"
"fmt"
"log"
"os/exec"
@@ -34,7 +33,6 @@ import (
)
func init() {
gob.Register(&ExecRes{})
RegisterResource("exec", func() Res { return &ExecRes{} })
}
@@ -73,7 +71,6 @@ func (obj *ExecRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *ExecRes) Init() error {
obj.BaseRes.Kind = "exec"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -24,14 +24,18 @@ import (
func TestExecSendRecv1(t *testing.T) {
r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
//MetaParams: MetaParams,
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world",
Shell: "/bin/bash",
}
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err)
}
defer func() {
if err := r1.Close(); err != nil {
t.Errorf("close failed with: %v", err)
@@ -70,14 +74,18 @@ func TestExecSendRecv1(t *testing.T) {
func TestExecSendRecv2(t *testing.T) {
r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
//MetaParams: MetaParams,
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world 1>&2", // to stderr
Shell: "/bin/bash",
}
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err)
}
defer func() {
if err := r1.Close(); err != nil {
t.Errorf("close failed with: %v", err)
@@ -116,14 +124,18 @@ func TestExecSendRecv2(t *testing.T) {
func TestExecSendRecv3(t *testing.T) {
r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
//MetaParams: MetaParams,
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
Shell: "/bin/bash",
}
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err)
}
defer func() {
if err := r1.Close(); err != nil {
t.Errorf("close failed with: %v", err)

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -20,7 +20,6 @@ package resources
import (
"bytes"
"crypto/sha256"
"encoding/gob"
"encoding/hex"
"fmt"
"io"
@@ -41,7 +40,6 @@ import (
)
func init() {
gob.Register(&FileRes{})
RegisterResource("file", func() Res { return &FileRes{} })
}
@@ -148,7 +146,6 @@ func (obj *FileRes) Init() error {
obj.path = obj.GetPath() // compute once
obj.isDir = strings.HasSuffix(obj.path, "/") // dirs have trailing slashes
obj.BaseRes.Kind = "file"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build go1.7

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !go1.7

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"github.com/purpleidea/mgmt/pgraph"
@@ -29,7 +28,6 @@ import (
func init() {
RegisterResource("graph", func() Res { return &GraphRes{} })
gob.Register(&GraphRes{})
}
// GraphRes is a resource that recursively runs a sub graph of resources.
@@ -89,7 +87,6 @@ func (obj *GraphRes) Init() error {
}
}
obj.BaseRes.Kind = "graph"
return obj.BaseRes.Init() // call base init, b/c we're overrriding
}

314
resources/group.go Normal file
View File

@@ -0,0 +1,314 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"fmt"
"io/ioutil"
"log"
"os/exec"
"os/user"
"strconv"
"syscall"
"github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors"
)
func init() {
RegisterResource("group", func() Res { return &GroupRes{} })
}
const groupFile = "/etc/group"
// GroupRes is a user group resource.
type GroupRes struct {
BaseRes `yaml:",inline"`
State string `yaml:"state"` // state: exists, absent
GID *uint32 `yaml:"gid"` // the group's gid
recWatcher *recwatch.RecWatcher
}
// Default returns some sensible defaults for this resource.
func (obj *GroupRes) Default() Res {
return &GroupRes{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
}
// Validate if the params passed in are valid data.
func (obj *GroupRes) Validate() error {
if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("State must be 'exists' or 'absent'")
}
return obj.BaseRes.Validate()
}
// Init initializes the resource.
func (obj *GroupRes) Init() error {
return obj.BaseRes.Init() // call base init, b/c we're overriding
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *GroupRes) Watch() error {
var err error
obj.recWatcher, err = recwatch.NewRecWatcher(groupFile, false)
if err != nil {
return err
}
defer obj.recWatcher.Close()
// notify engine that we're running
if err := obj.Running(); err != nil {
return err // bubble up a NACK...
}
var send = false // send event?
var exit *error
for {
if obj.debug {
log.Printf("%s: Watching: %s", obj, groupFile) // attempting to watch...
}
select {
case event, ok := <-obj.recWatcher.Events():
if !ok { // channel shutdown
return nil
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
}
if obj.debug { // don't access event.Body if event.Error isn't nil
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op)
}
send = true
obj.StateOK(false) // dirty
case event := <-obj.Events():
if exit, send = obj.ReadEvent(event); exit != nil {
return *exit // exit
}
//obj.StateOK(false) // dirty // these events don't invalidate state
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.Event()
}
}
}
// CheckApply method for Group resource.
func (obj *GroupRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply)
// check if the group exists
exists := true
group, err := user.LookupGroup(obj.GetName())
if err != nil {
if _, ok := err.(user.UnknownGroupError); !ok {
return false, errwrap.Wrapf(err, "error looking up group")
}
exists = false
}
// if the group doesn't exist and should be absent, we are done
if obj.State == "absent" && !exists {
return true, nil
}
// if the group exists and no GID is specified, we are done
if obj.State == "exists" && exists && obj.GID == nil {
return true, nil
}
if exists && obj.GID != nil {
// check if GID is taken
lookupGID, err := user.LookupGroupId(strconv.Itoa(int(*obj.GID)))
if err != nil {
if _, ok := err.(user.UnknownGroupIdError); !ok {
return false, errwrap.Wrapf(err, "error looking up GID")
}
}
if lookupGID != nil && lookupGID.Name != obj.GetName() {
return false, fmt.Errorf("the requested GID belongs to another group")
}
// get the existing group's GID
existingGID, err := strconv.ParseUint(group.Gid, 10, 32)
if err != nil {
return false, errwrap.Wrapf(err, "error casting existing GID")
}
// check if existing group has the wrong GID
// if it is wrong groupmod will change it to the desired value
if *obj.GID != uint32(existingGID) {
log.Printf("%s: Inconsistent GID: %s", obj, obj.GetName())
}
// if the group exists and has the correct GID, we are done
if obj.State == "exists" && *obj.GID == uint32(existingGID) {
return true, nil
}
}
if !apply {
return false, nil
}
var cmdName string
args := []string{obj.GetName()}
if obj.State == "exists" {
if exists {
log.Printf("%s: Modifying group: %s", obj, obj.GetName())
cmdName = "groupmod"
} else {
log.Printf("%s: Adding group: %s", obj, obj.GetName())
cmdName = "groupadd"
}
if obj.GID != nil {
args = append(args, "-g", fmt.Sprintf("%d", *obj.GID))
}
}
if obj.State == "absent" && exists {
log.Printf("%s: Deleting group: %s", obj, obj.GetName())
cmdName = "groupdel"
}
cmd := exec.Command(cmdName, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
// open a pipe to get error messages from os/exec
stderr, err := cmd.StderrPipe()
if err != nil {
return false, errwrap.Wrapf(err, "failed to initialize stderr pipe")
}
// start the command
if err := cmd.Start(); err != nil {
return false, errwrap.Wrapf(err, "cmd failed to start")
}
// capture any error messages
slurp, err := ioutil.ReadAll(stderr)
if err != nil {
return false, errwrap.Wrapf(err, "error slurping error message")
}
// wait until cmd exits and return error message if any
if err := cmd.Wait(); err != nil {
return false, errwrap.Wrapf(err, "%s", slurp)
}
return false, nil
}
// GroupUID is the UID struct for GroupRes.
type GroupUID struct {
BaseUID
name string
gid *uint32
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *GroupUID) IFF(uid ResUID) bool {
res, ok := uid.(*GroupUID)
if !ok {
return false
}
if obj.gid != nil && res.gid != nil {
if *obj.gid != *res.gid {
return false
}
}
if obj.name != "" && res.name != "" {
if obj.name != res.name {
return false
}
}
return true
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *GroupRes) UIDs() []ResUID {
x := &GroupUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
gid: obj.GID,
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *GroupRes) GroupCmp(r Res) bool {
_, ok := r.(*GroupRes)
if !ok {
return false
}
return false
}
// Compare two resources and return if they are equivalent.
func (obj *GroupRes) Compare(r Res) bool {
// we can only compare GroupRes to others of the same resource kind
res, ok := r.(*GroupRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if (obj.GID == nil) != (res.GID == nil) {
return false
}
if obj.GID != nil && res.GID != nil {
if *obj.GID != *res.GID {
return false
}
}
return true
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *GroupRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes GroupRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*GroupRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to GroupRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = GroupRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"errors"
"fmt"
"log"
@@ -36,7 +35,6 @@ var ErrResourceInsufficientParameters = errors.New(
func init() {
RegisterResource("hostname", func() Res { return &HostnameRes{} })
gob.Register(&HostnameRes{})
}
const (
@@ -88,7 +86,6 @@ func (obj *HostnameRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *HostnameRes) Init() error {
obj.BaseRes.Kind = "hostname"
if obj.PrettyHostname == "" {
obj.PrettyHostname = obj.Hostname
}

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"log"
"strconv"
@@ -28,7 +27,6 @@ import (
func init() {
RegisterResource("kv", func() Res { return &KVRes{} })
gob.Register(&KVRes{})
}
// KVResSkipCmpStyle represents the different styles of comparison when using SkipLessThan.
@@ -89,7 +87,6 @@ func (obj *KVRes) Validate() error {
// Init initializes the resource.
func (obj *KVRes) Init() error {
obj.BaseRes.Kind = "kv"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources // TODO: can this be a separate package or will it break the dag?

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"log"
"regexp"
@@ -29,7 +28,6 @@ import (
func init() {
RegisterResource("msg", func() Res { return &MsgRes{} })
gob.Register(&MsgRes{})
}
// MsgRes is a resource that writes messages to logs.
@@ -71,12 +69,23 @@ func (obj *MsgRes) Validate() error {
return fmt.Errorf("fields cannot begin with _")
}
}
switch obj.Priority {
case "Emerg":
case "Alert":
case "Crit":
case "Err":
case "Warning":
case "Notice":
case "Info":
case "Debug":
default:
return fmt.Errorf("invalid Priority '%s'", obj.Priority)
}
return obj.BaseRes.Validate()
}
// Init runs some startup code for this resource.
func (obj *MsgRes) Init() error {
obj.BaseRes.Kind = "msg"
return obj.BaseRes.Init() // call base init, b/c we're overrriding
}
@@ -97,7 +106,6 @@ func (obj *MsgRes) updateStateOK() {
}
// JournalPriority converts a string description to a numeric priority.
// XXX: Have Validate() make sure it actually is one of these.
func (obj *MsgRes) journalPriority() journal.Priority {
switch obj.Priority {
case "Emerg":

54
resources/msg_test.go Normal file
View File

@@ -0,0 +1,54 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"testing"
)
func TestMsgValidate1(t *testing.T) {
r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "Debug",
}
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err)
}
}
func TestMsgValidate2(t *testing.T) {
r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "UnrealPriority",
}
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err == nil {
t.Errorf("validation error is nil")
}
}

View File

@@ -3,29 +3,27 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"log"
)
func init() {
RegisterResource("noop", func() Res { return &NoopRes{} })
gob.Register(&NoopRes{})
}
// NoopRes is a no-op resource that does nothing.
@@ -50,7 +48,6 @@ func (obj *NoopRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *NoopRes) Init() error {
obj.BaseRes.Kind = "noop"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,32 +3,34 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"errors"
"fmt"
"log"
"strconv"
"unicode"
"github.com/purpleidea/mgmt/util"
systemdDbus "github.com/coreos/go-systemd/dbus"
machined "github.com/coreos/go-systemd/machine1"
systemdUtil "github.com/coreos/go-systemd/util"
"github.com/godbus/dbus"
errwrap "github.com/pkg/errors"
machined "github.com/purpleidea/go-systemd/machine1"
)
const (
@@ -42,15 +44,14 @@ const (
func init() {
RegisterResource("nspawn", func() Res { return &NspawnRes{} })
gob.Register(&NspawnRes{})
}
// NspawnRes is an nspawn container resource.
type NspawnRes struct {
BaseRes `yaml:",inline"`
State string `yaml:"state"`
// We're using the svc resource to start the machine because that's
// what machinectl does. We're not using svc.Watch because then we
// We're using the svc resource to start and stop the machine because
// that's what machinectl does. We're not using svc.Watch because then we
// would have two watches potentially racing each other and producing
// potentially unexpected results. We get everything we need to monitor
// the machine state changes from the org.freedesktop.machine1 object.
@@ -67,33 +68,90 @@ func (obj *NspawnRes) Default() Res {
}
}
// makeComposite creates a pointer to a SvcRes. The pointer is used to
// validate and initialize the nested svc.
func (obj *NspawnRes) makeComposite() (*SvcRes, error) {
res, err := NewNamedResource("svc", fmt.Sprintf(nspawnServiceTmpl, obj.GetName()))
if err != nil {
return nil, err
}
svc := res.(*SvcRes)
svc.State = obj.State
return svc, nil
}
// systemdVersion uses dbus to check which version of systemd is installed.
func systemdVersion() (uint16, error) {
// check if systemd is running
if !systemdUtil.IsRunningSystemd() {
return 0, fmt.Errorf("systemd is not running")
}
bus, err := systemdDbus.NewSystemdConnection()
if err != nil {
return 0, errwrap.Wrapf(err, "failed to connect to bus")
}
defer bus.Close()
// get the systemd version
verString, err := bus.GetManagerProperty("Version")
if err != nil {
return 0, errwrap.Wrapf(err, "could not get version property")
}
// lose the surrounding quotes
verNum, err := strconv.Unquote(verString)
if err != nil {
return 0, errwrap.Wrapf(err, "error unquoting version number")
}
// cast to uint16
ver, err := strconv.ParseUint(verNum, 10, 16)
if err != nil {
return 0, errwrap.Wrapf(err, "error casting systemd version number")
}
return uint16(ver), nil
}
// Validate if the params passed in are valid data.
func (obj *NspawnRes) Validate() error {
// TODO: validStates should be an enum!
validStates := map[string]struct{}{
stopped: {},
running: {},
if len(obj.GetName()) > 64 {
return fmt.Errorf("name must be 64 characters or less")
}
if _, exists := validStates[obj.State]; !exists {
// check if systemd version is higher than 231 to allow non-alphanumeric
// machine names, as previous versions would error in such cases
ver, err := systemdVersion()
if err != nil {
return err
}
if ver < 231 {
for _, char := range obj.GetName() {
if !unicode.IsLetter(char) && !unicode.IsNumber(char) {
return fmt.Errorf("name must only contain alphanumeric characters for systemd versions < 231")
}
}
}
if obj.State != running && obj.State != stopped {
return fmt.Errorf("invalid state: %s", obj.State)
}
if err := obj.svc.Validate(); err != nil { // composite resource
return errwrap.Wrapf(err, "validate failed for embedded svc")
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in validate")
}
if err := svc.Validate(); err != nil { // composite resource
return errwrap.Wrapf(err, "validate failed for embedded svc: %s", obj.svc)
}
return obj.BaseRes.Validate()
}
// Init runs some startup code for this resource.
func (obj *NspawnRes) Init() error {
var serviceName = fmt.Sprintf(nspawnServiceTmpl, obj.GetName())
obj.svc = &SvcRes{}
obj.svc.Name = serviceName
obj.svc.State = obj.State
svc, err := obj.makeComposite()
if err != nil {
return errwrap.Wrapf(err, "makeComposite failed in init")
}
obj.svc = svc
if err := obj.svc.Init(); err != nil {
return err
}
obj.BaseRes.Kind = "nspawn"
return obj.BaseRes.Init()
}
@@ -118,8 +176,10 @@ func (obj *NspawnRes) Watch() error {
if err := call.Err; err != nil {
return err
}
buschan := make(chan *dbus.Signal, 10)
bus.Signal(buschan)
// TODO: verify that implementation doesn't deadlock if there are unread
// messages left in the channel
busChan := make(chan *dbus.Signal, 10)
bus.Signal(busChan)
// notify engine that we're running
if err := obj.Running(); err != nil {
@@ -129,9 +189,12 @@ func (obj *NspawnRes) Watch() error {
var send = false
var exit *error
defer close(busChan)
defer bus.Close()
defer bus.RemoveSignal(busChan)
for {
select {
case event := <-buschan:
case event := <-busChan:
// process org.freedesktop.machine1 events for this resource's name
if event.Body[0] == obj.GetName() {
log.Printf("%s: Event received: %v", obj, event.Name)
@@ -178,7 +241,7 @@ func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
// compare the current state with the desired state and perform the
// appropriate action
var exists = true
properties, err := conn.GetProperties(obj.GetName())
properties, err := conn.DescribeMachine(obj.GetName())
if err != nil {
if err, ok := err.(dbus.Error); ok && err.Name !=
"org.freedesktop.machine1.NoSuchMachine" {
@@ -211,25 +274,10 @@ func (obj *NspawnRes) CheckApply(apply bool) (checkOK bool, err error) {
return false, nil
}
if obj.debug {
log.Printf("%s: CheckApply() applying '%s' state", obj, obj.State)
}
if obj.State == running {
// start the machine using svc resource
log.Printf("%s: Starting machine", obj)
// assume state had to be changed at this point, ignore checkOK
if _, err := obj.svc.CheckApply(apply); err != nil {
return false, errwrap.Wrapf(err, "nested svc failed")
}
}
if obj.State == stopped {
// terminate the machine with
// org.freedesktop.machine1.Manager.KillMachine
log.Printf("%s: Stopping machine", obj)
if err := conn.TerminateMachine(obj.GetName()); err != nil {
return false, errwrap.Wrapf(err, "failed to stop machine")
}
log.Printf("%s: CheckApply() applying '%s' state", obj, obj.State)
// use the embedded svc to apply the correct state
if _, err := obj.svc.CheckApply(apply); err != nil {
return false, errwrap.Wrapf(err, "nested svc failed")
}
return false, nil
@@ -287,6 +335,9 @@ func (obj *NspawnRes) Compare(r Res) bool {
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if !obj.svc.Compare(res.svc) {
return false

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package packagekit provides an interface to interact with packagekit.
@@ -33,8 +33,8 @@ import (
// global tweaks of verbosity and code path
const (
PK_DEBUG = false
PARANOID = false // enable if you see any ghosts
Debug = false
Paranoid = false // enable if you see any ghosts
)
// constants which might need to be tweaked or which contain special dbus strings.
@@ -74,76 +74,76 @@ var (
//type enum_filter uint64
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
const ( //static const PkEnumMatch enum_filter[]
PK_FILTER_ENUM_UNKNOWN uint64 = 1 << iota // "unknown"
PK_FILTER_ENUM_NONE // "none"
PK_FILTER_ENUM_INSTALLED // "installed"
PK_FILTER_ENUM_NOT_INSTALLED // "~installed"
PK_FILTER_ENUM_DEVELOPMENT // "devel"
PK_FILTER_ENUM_NOT_DEVELOPMENT // "~devel"
PK_FILTER_ENUM_GUI // "gui"
PK_FILTER_ENUM_NOT_GUI // "~gui"
PK_FILTER_ENUM_FREE // "free"
PK_FILTER_ENUM_NOT_FREE // "~free"
PK_FILTER_ENUM_VISIBLE // "visible"
PK_FILTER_ENUM_NOT_VISIBLE // "~visible"
PK_FILTER_ENUM_SUPPORTED // "supported"
PK_FILTER_ENUM_NOT_SUPPORTED // "~supported"
PK_FILTER_ENUM_BASENAME // "basename"
PK_FILTER_ENUM_NOT_BASENAME // "~basename"
PK_FILTER_ENUM_NEWEST // "newest"
PK_FILTER_ENUM_NOT_NEWEST // "~newest"
PK_FILTER_ENUM_ARCH // "arch"
PK_FILTER_ENUM_NOT_ARCH // "~arch"
PK_FILTER_ENUM_SOURCE // "source"
PK_FILTER_ENUM_NOT_SOURCE // "~source"
PK_FILTER_ENUM_COLLECTIONS // "collections"
PK_FILTER_ENUM_NOT_COLLECTIONS // "~collections"
PK_FILTER_ENUM_APPLICATION // "application"
PK_FILTER_ENUM_NOT_APPLICATION // "~application"
PK_FILTER_ENUM_DOWNLOADED // "downloaded"
PK_FILTER_ENUM_NOT_DOWNLOADED // "~downloaded"
PkFilterEnumUnknown uint64 = 1 << iota // "unknown"
PkFilterEnumNone // "none"
PkFilterEnumInstalled // "installed"
PkFilterEnumNotInstalled // "~installed"
PkFilterEnumDevelopment // "devel"
PkFilterEnumNotDevelopment // "~devel"
PkFilterEnumGui // "gui"
PkFilterEnumNotGui // "~gui"
PkFilterEnumFree // "free"
PkFilterEnumNotFree // "~free"
PkFilterEnumVisible // "visible"
PkFilterEnumNotVisible // "~visible"
PkFilterEnumSupported // "supported"
PkFilterEnumNotSupported // "~supported"
PkFilterEnumBasename // "basename"
PkFilterEnumNotBasename // "~basename"
PkFilterEnumNewest // "newest"
PkFilterEnumNotNewest // "~newest"
PkFilterEnumArch // "arch"
PkFilterEnumNotArch // "~arch"
PkFilterEnumSource // "source"
PkFilterEnumNotSource // "~source"
PkFilterEnumCollections // "collections"
PkFilterEnumNotCollections // "~collections"
PkFilterEnumApplication // "application"
PkFilterEnumNotApplication // "~application"
PkFilterEnumDownloaded // "downloaded"
PkFilterEnumNotDownloaded // "~downloaded"
)
// constants from packagekit c library.
const ( //static const PkEnumMatch enum_transaction_flag[]
PK_TRANSACTION_FLAG_ENUM_NONE uint64 = 1 << iota // "none"
PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED // "only-trusted"
PK_TRANSACTION_FLAG_ENUM_SIMULATE // "simulate"
PK_TRANSACTION_FLAG_ENUM_ONLY_DOWNLOAD // "only-download"
PK_TRANSACTION_FLAG_ENUM_ALLOW_REINSTALL // "allow-reinstall"
PK_TRANSACTION_FLAG_ENUM_JUST_REINSTALL // "just-reinstall"
PK_TRANSACTION_FLAG_ENUM_ALLOW_DOWNGRADE // "allow-downgrade"
PkTransactionFlagEnumNone uint64 = 1 << iota // "none"
PkTransactionFlagEnumOnlyTrusted // "only-trusted"
PkTransactionFlagEnumSimulate // "simulate"
PkTransactionFlagEnumOnlyDownload // "only-download"
PkTransactionFlagEnumAllowReinstall // "allow-reinstall"
PkTransactionFlagEnumJustReinstall // "just-reinstall"
PkTransactionFlagEnumAllowDowngrade // "allow-downgrade"
)
// constants from packagekit c library.
const ( //typedef enum
PK_INFO_ENUM_UNKNOWN uint64 = 1 << iota
PK_INFO_ENUM_INSTALLED
PK_INFO_ENUM_AVAILABLE
PK_INFO_ENUM_LOW
PK_INFO_ENUM_ENHANCEMENT
PK_INFO_ENUM_NORMAL
PK_INFO_ENUM_BUGFIX
PK_INFO_ENUM_IMPORTANT
PK_INFO_ENUM_SECURITY
PK_INFO_ENUM_BLOCKED
PK_INFO_ENUM_DOWNLOADING
PK_INFO_ENUM_UPDATING
PK_INFO_ENUM_INSTALLING
PK_INFO_ENUM_REMOVING
PK_INFO_ENUM_CLEANUP
PK_INFO_ENUM_OBSOLETING
PK_INFO_ENUM_COLLECTION_INSTALLED
PK_INFO_ENUM_COLLECTION_AVAILABLE
PK_INFO_ENUM_FINISHED
PK_INFO_ENUM_REINSTALLING
PK_INFO_ENUM_DOWNGRADING
PK_INFO_ENUM_PREPARING
PK_INFO_ENUM_DECOMPRESSING
PK_INFO_ENUM_UNTRUSTED
PK_INFO_ENUM_TRUSTED
PK_INFO_ENUM_UNAVAILABLE
PK_INFO_ENUM_LAST
PkInfoEnumUnknown uint64 = 1 << iota
PkInfoEnumInstalled
PkInfoEnumAvailable
PkInfoEnumLow
PkInfoEnumEnhancement
PkInfoEnumNormal
PkInfoEnumBugfix
PkInfoEnumImportant
PkInfoEnumSecurity
PkInfoEnumBlocked
PkInfoEnumDownloading
PkInfoEnumUpdating
PkInfoEnumInstalling
PkInfoEnumRemoving
PkInfoEnumCleanup
PkInfoEnumObsoleting
PkInfoEnumCollectionInstalled
PkInfoEnumCollectionAvailable
PkInfoEnumFinished
PkInfoEnumReinstalling
PkInfoEnumDowngrading
PkInfoEnumPreparing
PkInfoEnumDecompressing
PkInfoEnumUntrusted
PkInfoEnumTrusted
PkInfoEnumUnavailable
PkInfoEnumLast
)
// Conn is a wrapper struct so we can pass bus connection around in the struct.
@@ -184,7 +184,7 @@ func (bus *Conn) Close() error {
// internal helper to add signal matches to the bus, should only be called once
func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error {
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals)
}
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
@@ -224,7 +224,7 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
if err != nil {
return nil, err
}
if PARANOID { // TODO: this filtering might not be necessary anymore...
if Paranoid { // TODO: this filtering might not be necessary anymore...
// try to handle the filtering inside this function!
rch := make(chan *dbus.Signal)
go func() {
@@ -257,7 +257,7 @@ func (bus *Conn) WatchChanges() (chan *dbus.Signal, error) {
// CreateTransaction creates and returns a transaction path.
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
if PK_DEBUG {
if Debug {
log.Println("PackageKit: CreateTransaction()")
}
var interfacePath dbus.ObjectPath
@@ -266,7 +266,7 @@ func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
if call != nil {
return "", call
}
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath)
}
return interfacePath, nil
@@ -284,12 +284,12 @@ func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, er
// add signal matches for Package and Finished which will always be last
var signals = []string{"Package", "Finished", "Error", "Destroy"}
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath)
}
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
if PK_DEBUG {
if Debug {
log.Println("PackageKit: ResolvePackages(): Call: Success!")
}
if call.Err != nil {
@@ -300,7 +300,7 @@ loop:
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal)
}
if signal.Path != interfacePath {
@@ -338,8 +338,8 @@ loop:
// IsInstalledList queries a list of packages to see if they are installed.
func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
var filter uint64 // initializes at the "zero" value of 0
filter += PK_FILTER_ENUM_ARCH // always search in our arch
var filter uint64 // initializes at the "zero" value of 0
filter += PkFilterEnumArch // always search in our arch
packageIDs, e := bus.ResolvePackages(packages, filter)
if e != nil {
return nil, fmt.Errorf("ResolvePackages error: %v", e)
@@ -396,7 +396,11 @@ func (bus *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) e
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
call := obj.Call(FmtTransactionMethod("RefreshCache"), 0, false)
if call.Err != nil {
return call.Err
}
call = obj.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
if call.Err != nil {
return call.Err
}
@@ -593,7 +597,7 @@ loop:
// GetUpdates gets a list of packages that are installed and which can be updated, mod filter.
func (bus *Conn) GetUpdates(filter uint64) ([]string, error) {
if PK_DEBUG {
if Debug {
log.Println("PackageKit: GetUpdates()")
}
packageIDs := []string{}
@@ -664,11 +668,11 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
count++
}
if !(filter&PK_FILTER_ENUM_ARCH == PK_FILTER_ENUM_ARCH) {
filter += PK_FILTER_ENUM_ARCH // always search in our arch
if !(filter&PkFilterEnumArch == PkFilterEnumArch) {
filter += PkFilterEnumArch // always search in our arch
}
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: PackagesToPackageIDs(): %v", strings.Join(packages, ", "))
}
resolved, e := bus.ResolvePackages(packages, filter)
@@ -771,7 +775,7 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
// this check is for packages that need to verify their "newest" status
// we need to know this so we can install the correct newest packageID!
recursion := make(map[string]*PkPackageIDActionData)
if !(filter&PK_FILTER_ENUM_NEWEST == PK_FILTER_ENUM_NEWEST) {
if !(filter&PkFilterEnumNewest == PkFilterEnumNewest) {
checkPackages := []string{}
filteredPackageMap := make(map[string]string)
for index, pkg := range packages {
@@ -788,13 +792,13 @@ func (bus *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint6
}
// we _could_ do a second resolve and then parse like this...
//resolved, e := bus.ResolvePackages(..., filter+PK_FILTER_ENUM_NEWEST)
//resolved, e := bus.ResolvePackages(..., filter+PkFilterEnumNewest)
// but that's basically what recursion here could do too!
if len(checkPackages) > 0 {
if PK_DEBUG {
if Debug {
log.Printf("PackageKit: PackagesToPackageIDs(): Recurse: %v", strings.Join(checkPackages, ", "))
}
recursion, e = bus.PackagesToPackageIDs(filteredPackageMap, filter+PK_FILTER_ENUM_NEWEST)
recursion, e = bus.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
if e != nil {
return nil, fmt.Errorf("Recursion error: %v", e)
}

View File

@@ -3,23 +3,22 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"crypto/rand"
"encoding/gob"
"fmt"
"io/ioutil"
"log"
@@ -35,7 +34,6 @@ import (
func init() {
RegisterResource("password", func() Res { return &PasswordRes{} })
gob.Register(&PasswordRes{})
}
const (
@@ -74,7 +72,6 @@ func (obj *PasswordRes) Validate() error {
// Init generates a new password for this resource if one was not provided. It
// will save this into a local file. It will load it back in from previous runs.
func (obj *PasswordRes) Init() error {
obj.BaseRes.Kind = "password" // must be set before using VarDir
dir, err := obj.VarDir("")
if err != nil {

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"log"
"path"
@@ -32,7 +31,6 @@ import (
func init() {
RegisterResource("pkg", func() Res { return &PkgRes{} })
gob.Register(&PkgRes{})
}
// PkgRes is a package resource for packagekit.
@@ -67,7 +65,6 @@ func (obj *PkgRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *PkgRes) Init() error {
obj.BaseRes.Kind = "pkg"
if err := obj.BaseRes.Init(); err != nil { // call base init, b/c we're overriding
return err
}
@@ -180,22 +177,22 @@ func (obj *PkgRes) groupMappingHelper() map[string]string {
}
func (obj *PkgRes) pkgMappingHelper(bus *packagekit.Conn) (map[string]*packagekit.PkPackageIDActionData, error) {
packageMap := obj.groupMappingHelper() // get the grouped values
packageMap[obj.Name] = obj.State // key is pkg name, value is pkg state
var filter uint64 // initializes at the "zero" value of 0
filter += packagekit.PK_FILTER_ENUM_ARCH // always search in our arch (optional!)
packageMap := obj.groupMappingHelper() // get the grouped values
packageMap[obj.Name] = obj.State // key is pkg name, value is pkg state
var filter uint64 // initializes at the "zero" value of 0
filter += packagekit.PkFilterEnumArch // always search in our arch (optional!)
// we're requesting latest version, or to narrow down install choices!
if obj.State == "newest" || obj.State == "installed" {
// if we add this, we'll still see older packages if installed
// this is an optimization, and is *optional*, this logic is
// handled inside of PackagesToPackageIDs now automatically!
filter += packagekit.PK_FILTER_ENUM_NEWEST // only search for newest packages
filter += packagekit.PkFilterEnumNewest // only search for newest packages
}
if !obj.AllowNonFree {
filter += packagekit.PK_FILTER_ENUM_FREE
filter += packagekit.PkFilterEnumFree
}
if !obj.AllowUnsupported {
filter += packagekit.PK_FILTER_ENUM_SUPPORTED
filter += packagekit.PkFilterEnumSupported
}
result, err := bus.PackagesToPackageIDs(packageMap, filter)
if err != nil {
@@ -301,7 +298,7 @@ func (obj *PkgRes) CheckApply(apply bool) (checkOK bool, err error) {
var transactionFlags uint64 // initializes at the "zero" value of 0
if !obj.AllowUntrusted { // allow
transactionFlags += packagekit.PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED
transactionFlags += packagekit.PkTransactionFlagEnumOnlyTrusted
}
// apply correct state!
log.Printf("%s: Set: %v...", obj.fmtNames(util.StrListIntersection(applyPackages, obj.getNames())), obj.State)

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,22 +3,23 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package resources provides the resource framework and idempotent primitives.
package resources
import (
"encoding/gob"
"fmt"
"log"
"math"
@@ -43,18 +44,40 @@ var registeredResources = map[string]func() Res{}
// RegisterResource registers a new resource by providing a constructor
// function that returns a resource object ready to be unmarshalled from YAML.
func RegisterResource(name string, creator func() Res) {
registeredResources[name] = creator
func RegisterResource(kind string, fn func() Res) {
if _, ok := registeredResources[kind]; ok {
panic(fmt.Sprintf("a resource kind of %s is already registered", kind))
}
gob.Register(fn())
registeredResources[kind] = fn
}
// NewEmptyNamedResource returns an empty resource object from a registered
// type, ready to be unmarshalled.
func NewEmptyNamedResource(name string) (Res, error) {
fn, ok := registeredResources[name]
// NewResource returns an empty resource object from a registered kind. It
// errors if the resource kind doesn't exist.
func NewResource(kind string) (Res, error) {
fn, ok := registeredResources[kind]
if !ok {
return nil, fmt.Errorf("no resource named %s available", name)
return nil, fmt.Errorf("no resource kind `%s` available", kind)
}
return fn(), nil
res := fn().Default()
res.SetKind(kind)
//*res.Meta() = DefaultMetaParams // TODO: centralize this here?
return res, nil
}
// NewNamedResource returns an empty resource object from a registered kind. It
// also sets the name. It is a wrapper around NewResource. It also errors if the
// name is empty.
func NewNamedResource(kind, name string) (Res, error) {
if name == "" {
return nil, fmt.Errorf("resource name is empty")
}
res, err := NewResource(kind)
if err != nil {
return nil, err
}
res.SetName(name)
return res, nil
}
//go:generate stringer -type=ResState -output=resstate_stringer.go
@@ -108,11 +131,13 @@ type ResData struct {
// The Base interface is everything that is common to all resources.
// Everything here only needs to be implemented once, in the BaseRes.
type Base interface {
fmt.Stringer // String() string
GetName() string // can't be named "Name()" because of struct field
SetName(string)
SetKind(string)
GetKind() string
String() string
Meta() *MetaParams
Events() chan *event.Event
Data() *ResData
@@ -130,6 +155,7 @@ type Base interface {
Refresh() bool // is there a pending refresh to run?
SetRefresh(bool) // set the refresh state of this resource
SendRecv(Res) (map[string]bool, error) // send->recv data passing function
SetRecv(map[string]*Send)
IsStateOK() bool
StateOK(b bool)
GroupCmp(Res) bool // TODO: is there a better name for this?
@@ -520,6 +546,19 @@ func (obj *BaseRes) Compare(res Res) bool {
if !cmpSlices(obj.Meta().Sema, res.Meta().Sema) {
return false
}
// if resources are grouped, are the groups the same?
if i, j := obj.GetGroup(), res.GetGroup(); len(i) != len(j) {
return false
} else if len(i) > 0 { // trick the golinter
ix, jx := Sort(i), Sort(j)
for k := range ix {
if !ix[k].Compare(jx[k]) { // compare sub resources
return false
}
}
}
return true
}

View File

@@ -3,24 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"bytes"
"encoding/base64"
"encoding/gob"
"testing"
)
@@ -42,6 +39,7 @@ func TestCompare2(t *testing.T) {
r1 := &NoopRes{
BaseRes: BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: MetaParams{
Noop: true,
},
@@ -49,7 +47,8 @@ func TestCompare2(t *testing.T) {
}
r2 := &NoopRes{
BaseRes: BaseRes{
Name: "noop1", // same nampe
Name: "noop1", // same name
Kind: "noop",
MetaParams: MetaParams{
Noop: false, // different noop
},
@@ -65,86 +64,6 @@ func TestCompare2(t *testing.T) {
}
}
func TestMiscEncodeDecode1(t *testing.T) {
var err error
//gob.Register( &NoopRes{} ) // happens in noop.go : init()
//gob.Register( &FileRes{} ) // happens in file.go : init()
// ...
// encode
var input interface{} = &FileRes{}
b1 := bytes.Buffer{}
e := gob.NewEncoder(&b1)
err = e.Encode(&input) // pass with &
if err != nil {
t.Errorf("Gob failed to Encode: %v", err)
}
str := base64.StdEncoding.EncodeToString(b1.Bytes())
// decode
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Errorf("Base64 failed to Decode: %v", err)
}
b2 := bytes.NewBuffer(bb)
d := gob.NewDecoder(b2)
err = d.Decode(&output) // pass with &
if err != nil {
t.Errorf("Gob failed to Decode: %v", err)
}
res1, ok := input.(Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if !res1.Compare(res2) {
t.Error("The input and output Res values do not match!")
}
}
func TestMiscEncodeDecode2(t *testing.T) {
var err error
//gob.Register( &NoopRes{} ) // happens in noop.go : init()
//gob.Register( &FileRes{} ) // happens in file.go : init()
// ...
// encode
var input Res = &FileRes{}
b64, err := ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
return
}
output, err := B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
return
}
res1, ok := input.(Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if !res1.Compare(res2) {
t.Error("The input and output Res values do not match!")
}
}
func TestIFF(t *testing.T) {
uid := &BaseUID{Name: "/tmp/unit-test"}
same := &BaseUID{Name: "/tmp/unit-test"}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -41,6 +41,7 @@ func NewNoopResTestSema(name string, semas []string) *NoopResTest {
NoopRes: NoopRes{
BaseRes: BaseRes{
Name: name,
Kind: "noop",
MetaParams: MetaParams{
AutoGroup: true, // always autogroup
Sema: semas,

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -173,6 +173,11 @@ type Send struct {
Changed bool // set to true if this key was updated, read only!
}
// SetRecv sets the Res Recv field to given map of Send structs.
func (obj *BaseRes) SetRecv(recv map[string]*Send) {
obj.Recv = recv
}
// SendRecv pulls in the sent values into the receive slots. It is called by the
// receiver and must be given as input the full resource struct to receive on.
func (obj *BaseRes) SendRecv(res Res) (map[string]bool, error) {

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// DOCS: https://godoc.org/github.com/coreos/go-systemd/dbus
@@ -20,7 +20,6 @@
package resources
import (
"encoding/gob"
"fmt"
"log"
@@ -34,7 +33,6 @@ import (
func init() {
RegisterResource("svc", func() Res { return &SvcRes{} })
gob.Register(&SvcRes{})
}
// SvcRes is a service resource for systemd units.
@@ -67,7 +65,6 @@ func (obj *SvcRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *SvcRes) Init() error {
obj.BaseRes.Kind = "svc"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}

View File

@@ -3,22 +3,21 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"encoding/gob"
"fmt"
"log"
"time"
@@ -26,7 +25,6 @@ import (
func init() {
RegisterResource("timer", func() Res { return &TimerRes{} })
gob.Register(&TimerRes{})
}
// TimerRes is a timer resource for time based events.
@@ -59,7 +57,6 @@ func (obj *TimerRes) Validate() error {
// Init runs some startup code for this resource.
func (obj *TimerRes) Init() error {
obj.BaseRes.Kind = "timer"
return obj.BaseRes.Init() // call base init, b/c we're overrriding
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources

451
resources/user.go Normal file
View File

@@ -0,0 +1,451 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"fmt"
"io/ioutil"
"log"
"os/exec"
"os/user"
"sort"
"strconv"
"strings"
"syscall"
"github.com/purpleidea/mgmt/recwatch"
errwrap "github.com/pkg/errors"
)
func init() {
RegisterResource("user", func() Res { return &UserRes{} })
}
const passwdFile = "/etc/passwd"
// UserRes is a user account resource.
type UserRes struct {
BaseRes `yaml:",inline"`
State string `yaml:"state"` // state: exists, absent
UID *uint32 `yaml:"uid"` // uid must be unique unless AllowDuplicateUID is true
GID *uint32 `yaml:"gid"` // gid of the user's primary group
Group *string `yaml:"group"` // name of the user's primary group
Groups []string `yaml:"groups"` // list of supplemental groups
HomeDir *string `yaml:"homedir"` // path to the user's home directory
AllowDuplicateUID bool `yaml:"allowduplicateuid"` // allow duplicate uid
recWatcher *recwatch.RecWatcher
}
// Default returns some sensible defaults for this resource.
func (obj *UserRes) Default() Res {
return &UserRes{
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
}
// Validate if the params passed in are valid data.
func (obj *UserRes) Validate() error {
const whitelist string = "_abcdefghijklmnopqrstuvwxyz0123456789"
if obj.State != "exists" && obj.State != "absent" {
return fmt.Errorf("state must be 'exists' or 'absent'")
}
if obj.GID != nil && obj.Group != nil {
return fmt.Errorf("cannot use both GID and Group")
}
if obj.Group != nil {
if *obj.Group == "" {
return fmt.Errorf("group cannot be empty string")
}
for _, char := range *obj.Group {
if !strings.Contains(whitelist, string(char)) {
return fmt.Errorf("group contains invalid character(s)")
}
}
}
if obj.Groups != nil {
for _, group := range obj.Groups {
if group == "" {
return fmt.Errorf("group cannot be empty string")
}
for _, char := range group {
if !strings.Contains(whitelist, string(char)) {
return fmt.Errorf("groups list contains invalid character(s)")
}
}
}
}
return obj.BaseRes.Validate()
}
// Init initializes the resource.
func (obj *UserRes) Init() error {
return obj.BaseRes.Init() // call base init, b/c we're overriding
}
// Watch is the primary listener for this resource and it outputs events.
func (obj *UserRes) Watch() error {
var err error
obj.recWatcher, err = recwatch.NewRecWatcher(passwdFile, false)
if err != nil {
return err
}
defer obj.recWatcher.Close()
// notify engine that we're running
if err := obj.Running(); err != nil {
return err // bubble up a NACK...
}
var send = false // send event?
var exit *error
for {
if obj.debug {
log.Printf("%s: Watching: %s", obj, passwdFile) // attempting to watch...
}
select {
case event, ok := <-obj.recWatcher.Events():
if !ok { // channel shutdown
return nil
}
if err := event.Error; err != nil {
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
}
if obj.debug { // don't access event.Body if event.Error isn't nil
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op)
}
send = true
obj.StateOK(false) // dirty
case event := <-obj.Events():
if exit, send = obj.ReadEvent(event); exit != nil {
return *exit // exit
}
//obj.StateOK(false) // dirty // these events don't invalidate state
}
// do all our event sending all together to avoid duplicate msgs
if send {
send = false
obj.Event()
}
}
}
// CheckApply method for User resource.
func (obj *UserRes) CheckApply(apply bool) (checkOK bool, err error) {
log.Printf("%s: CheckApply(%t)", obj, apply)
var exists = true
usr, err := user.Lookup(obj.GetName())
if err != nil {
if _, ok := err.(user.UnknownUserError); !ok {
return false, errwrap.Wrapf(err, "error looking up user")
}
exists = false
}
if obj.AllowDuplicateUID == false && obj.UID != nil {
existingUID, err := user.LookupId(strconv.Itoa(int(*obj.UID)))
if err != nil {
if _, ok := err.(user.UnknownUserIdError); !ok {
return false, errwrap.Wrapf(err, "error looking up UID")
}
} else if existingUID.Username != obj.GetName() {
return false, fmt.Errorf("the requested UID is already taken")
}
}
if obj.State == "absent" && !exists {
return true, nil
}
if usercheck := true; exists && obj.State == "exists" {
intUID, err := strconv.Atoi(usr.Uid)
if err != nil {
return false, errwrap.Wrapf(err, "error casting UID to int")
}
intGID, err := strconv.Atoi(usr.Gid)
if err != nil {
return false, errwrap.Wrapf(err, "error casting GID to int")
}
if obj.UID != nil && int(*obj.UID) != intUID {
usercheck = false
}
if obj.GID != nil && int(*obj.GID) != intGID {
usercheck = false
}
if obj.HomeDir != nil && *obj.HomeDir != usr.HomeDir {
usercheck = false
}
if usercheck {
return true, nil
}
}
if !apply {
return false, nil
}
var cmdName string
var args []string
if obj.State == "exists" {
if exists {
cmdName = "usermod"
log.Printf("%s: Modifying user: %s", obj, obj.GetName())
} else {
cmdName = "useradd"
log.Printf("%s: Adding user: %s", obj, obj.GetName())
}
if obj.AllowDuplicateUID {
args = append(args, "--non-unique")
}
if obj.UID != nil {
args = append(args, "-u", fmt.Sprintf("%d", *obj.UID))
}
if obj.GID != nil {
args = append(args, "-g", fmt.Sprintf("%d", *obj.GID))
}
if obj.Group != nil {
args = append(args, "-g", *obj.Group)
}
if obj.Groups != nil {
args = append(args, "-G", strings.Join(obj.Groups, ","))
}
if obj.HomeDir != nil {
args = append(args, "-d", *obj.HomeDir)
}
}
if obj.State == "absent" {
cmdName = "userdel"
log.Printf("%s: Deleting user: %s", obj, obj.GetName())
}
args = append(args, obj.GetName())
cmd := exec.Command(cmdName, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Setpgid: true,
Pgid: 0,
}
// open a pipe to get error messages from os/exec
stderr, err := cmd.StderrPipe()
if err != nil {
return false, errwrap.Wrapf(err, "failed to initialize stderr pipe")
}
// start the command
if err := cmd.Start(); err != nil {
return false, errwrap.Wrapf(err, "cmd failed to start")
}
// capture any error messages
slurp, err := ioutil.ReadAll(stderr)
if err != nil {
return false, errwrap.Wrapf(err, "error slurping error message")
}
// wait until cmd exits and return error message if any
if err := cmd.Wait(); err != nil {
return false, errwrap.Wrapf(err, "%s", slurp)
}
return false, nil
}
// UserUID is the UID struct for UserRes.
type UserUID struct {
BaseUID
name string
}
// UserResAutoEdges holds the state of the auto edge generator.
type UserResAutoEdges struct {
UIDs []ResUID
pointer int
}
// AutoEdges returns edges from the user resource to each group found in
// its definition. The groups can be in any of the three applicable fields
// (GID, Group and Groups.) If the user exists, reversed ensures the edge
// goes from group to user, and if the user is absent the edge goes from
// user to group. This ensures that we don't add users to groups that
// don't exist or delete groups before we delete their members.
func (obj *UserRes) AutoEdges() (AutoEdge, error) {
var result []ResUID
var reversed bool
if obj.State == "exists" {
reversed = true
}
if obj.GID != nil {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
gid: obj.GID,
})
}
if obj.Group != nil {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
name: *obj.Group,
})
}
for _, group := range obj.Groups {
result = append(result, &GroupUID{
BaseUID: BaseUID{
Reversed: &reversed,
},
name: group,
})
}
return &UserResAutoEdges{
UIDs: result,
pointer: 0,
}, nil
}
// Next returns the next automatic edge.
func (obj *UserResAutoEdges) Next() []ResUID {
if len(obj.UIDs) == 0 {
return nil
}
value := obj.UIDs[obj.pointer]
obj.pointer++
return []ResUID{value}
}
// Test gets results of the earlier Next() call, & returns if we should continue.
func (obj *UserResAutoEdges) Test(input []bool) bool {
if len(obj.UIDs) <= obj.pointer {
return false
}
if len(input) != 1 { // in case we get given bad data
log.Fatal("Expecting a single value!")
}
return true // keep going
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one, although some resources can return multiple.
func (obj *UserRes) UIDs() []ResUID {
x := &UserUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *UserRes) GroupCmp(r Res) bool {
_, ok := r.(*UserRes)
if !ok {
return false
}
return false
}
// Compare two resources and return if they are equivalent.
func (obj *UserRes) Compare(r Res) bool {
// we can only compare UserRes to others of the same resource kind
res, ok := r.(*UserRes)
if !ok {
return false
}
if !obj.BaseRes.Compare(res) { // call base Compare
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State {
return false
}
if (obj.UID == nil) != (res.UID == nil) {
return false
}
if obj.UID != nil && res.UID != nil {
if *obj.UID != *res.UID {
return false
}
}
if (obj.GID == nil) != (res.GID == nil) {
return false
}
if obj.GID != nil && res.GID != nil {
if *obj.GID != *res.GID {
return false
}
}
if (obj.Groups == nil) != (res.Groups == nil) {
return false
}
if obj.Groups != nil && res.Groups != nil {
if len(obj.Groups) != len(res.Groups) {
return false
}
objGroups := obj.Groups
resGroups := res.Groups
sort.Strings(objGroups)
sort.Strings(resGroups)
for i := range objGroups {
if objGroups[i] != resGroups[i] {
return false
}
}
}
if (obj.HomeDir == nil) != (res.HomeDir == nil) {
return false
}
if obj.HomeDir != nil && res.HomeDir != nil {
if *obj.HomeDir != *obj.HomeDir {
return false
}
}
if obj.AllowDuplicateUID != res.AllowDuplicateUID {
return false
}
return true
}
// UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults.
func (obj *UserRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
type rawRes UserRes // indirection to avoid infinite recursion
def := obj.Default() // get the default
res, ok := def.(*UserRes) // put in the right format
if !ok {
return fmt.Errorf("could not convert to UserRes")
}
raw := rawRes(*res) // convert; the defaults go here
if err := unmarshal(&raw); err != nil {
return err
}
*obj = UserRes(raw) // restore from indirection with type conversion!
return nil
}

View File

@@ -3,16 +3,16 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
@@ -22,10 +22,37 @@ import (
"encoding/base64"
"encoding/gob"
"fmt"
"reflect"
"sort"
"strings"
errwrap "github.com/pkg/errors"
)
const (
// StructTag is the key we use in struct field names for key mapping.
StructTag = "lang"
)
// ResourceSlice is a linear list of resources. It can be sorted.
type ResourceSlice []Res
func (rs ResourceSlice) Len() int { return len(rs) }
func (rs ResourceSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs ResourceSlice) Less(i, j int) bool { return rs[i].String() < rs[j].String() }
// Sort the list of resources and return a copy without modifying the input.
func Sort(rs []Res) []Res {
resources := []Res{}
for _, r := range rs { // copy
resources = append(resources, r)
}
sort.Sort(ResourceSlice(resources))
return resources
// sort.Sort(ResourceSlice(rs)) // this is wrong, it would modify input!
//return rs
}
// ResToB64 encodes a resource to a base64 encoded string (after serialization).
func ResToB64(res Res) (string, error) {
b := bytes.Buffer{}
@@ -52,8 +79,57 @@ func B64ToRes(str string) (Res, error) {
}
res, ok := output.(Res)
if !ok {
return nil, fmt.Errorf("Output %v is not a Res", res)
return nil, fmt.Errorf("output `%v` is not a Res", output)
}
return res, nil
}
// StructTagToFieldName returns a mapping from recommended alias to actual field
// name. It returns an error if it finds a collision. It uses the `lang` tags.
func StructTagToFieldName(res Res) (map[string]string, error) {
// TODO: fallback to looking up yaml tags, although harder to parse
result := make(map[string]string) // `lang` field tag -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
name := field.Name
// TODO: golang 1.7+
// if !ok, then nothing is found
//if alias, ok := field.Tag.Lookup(StructTag); ok { // golang 1.7+
if alias := field.Tag.Get(StructTag); alias != "" { // golang 1.6
if val, exists := result[alias]; exists {
return nil, fmt.Errorf("field `%s` uses the same key `%s` as field `%s`", name, alias, val)
}
// empty string ("") is a valid value
if alias != "" {
result[alias] = name
}
}
}
return result, nil
}
// LowerStructFieldNameToFieldName returns a mapping from the lower case version
// of each field name to the actual field name. It only returns public fields.
// It returns an error if it finds a collision.
func LowerStructFieldNameToFieldName(res Res) (map[string]string, error) {
result := make(map[string]string) // lower field name -> field name
st := reflect.TypeOf(res).Elem() // elem for ptr to res
for i := 0; i < st.NumField(); i++ {
field := st.Field(i)
name := field.Name
if strings.Title(name) != name { // must have been a priv field
continue
}
if alias := strings.ToLower(name); alias != "" {
if val, exists := result[alias]; exists {
return nil, fmt.Errorf("field `%s` uses the same key `%s` as field `%s`", name, alias, val)
}
result[alias] = name
}
}
return result, nil
}

238
resources/util_test.go Normal file
View File

@@ -0,0 +1,238 @@
// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"bytes"
"encoding/base64"
"encoding/gob"
"reflect"
"testing"
)
func TestSort0(t *testing.T) {
rs := []Res{}
s := Sort(rs)
if !reflect.DeepEqual(s, []Res{}) {
t.Errorf("sort failed!")
if s == nil {
t.Logf("output is nil!")
} else {
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
}
}
func TestSort1(t *testing.T) {
r1, _ := NewNamedResource("noop", "noop1")
r2, _ := NewNamedResource("noop", "noop2")
r3, _ := NewNamedResource("noop", "noop3")
r4, _ := NewNamedResource("noop", "noop4")
r5, _ := NewNamedResource("noop", "noop5")
r6, _ := NewNamedResource("noop", "noop6")
rs := []Res{r3, r2, r6, r1, r5, r4}
s := Sort(rs)
if !reflect.DeepEqual(s, []Res{r1, r2, r3, r4, r5, r6}) {
t.Errorf("sort failed!")
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
if !reflect.DeepEqual(rs, []Res{r3, r2, r6, r1, r5, r4}) {
t.Errorf("sort modified input!")
str := "Got:"
for _, r := range rs {
str += " " + r.String()
}
t.Errorf(str)
}
}
func TestMiscEncodeDecode1(t *testing.T) {
var err error
// encode
var input interface{} = &FileRes{}
b1 := bytes.Buffer{}
e := gob.NewEncoder(&b1)
err = e.Encode(&input) // pass with &
if err != nil {
t.Errorf("Gob failed to Encode: %v", err)
}
str := base64.StdEncoding.EncodeToString(b1.Bytes())
// decode
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Errorf("Base64 failed to Decode: %v", err)
}
b2 := bytes.NewBuffer(bb)
d := gob.NewDecoder(b2)
err = d.Decode(&output) // pass with &
if err != nil {
t.Errorf("Gob failed to Decode: %v", err)
}
res1, ok := input.(Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if !res1.Compare(res2) {
t.Error("The input and output Res values do not match!")
}
}
func TestMiscEncodeDecode2(t *testing.T) {
var err error
// encode
input, err := NewNamedResource("file", "file1")
if err != nil {
t.Errorf("Can't create: %v", err)
return
}
b64, err := ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
return
}
output, err := B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
return
}
res1, ok := input.(Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if !res1.Compare(res2) {
t.Error("The input and output Res values do not match!")
}
}
func TestStructTagToFieldName0(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool `lang:"alpha" yaml:"nope"`
Beta string `yaml:"beta"`
Gamma string
Delta int `lang:"surprise"`
}
mapping, err := StructTagToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"alpha": "Alpha",
"surprise": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName0(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := LowerStructFieldNameToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"noopres": "NoopRes", // TODO: switch this to TestRes when it is in git master
"alpha": "Alpha",
//"skipme": "skipMe",
"beta": "Beta",
"iamacamel": "IAmACamel",
//"pass": "pass",
"gamma": "Gamma",
"delta": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName1(t *testing.T) {
type TestStruct struct {
// TODO: switch this to TestRes when it is in git master
NoopRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
// these two should collide
DoubleWord bool
Doubleword string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := LowerStructFieldNameToFieldName(&TestStruct{})
if err == nil {
t.Errorf("expected failure, but passed with: %+v", mapping)
return
}
}

View File

@@ -3,29 +3,30 @@
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// 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 Affero General Public License for more details.
// GNU General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !novirt
package resources
import (
"encoding/gob"
"fmt"
"log"
"math/rand"
"net/url"
"os/user"
"path"
"regexp"
"strings"
"sync"
"time"
@@ -38,7 +39,6 @@ import (
func init() {
RegisterResource("virt", func() Res { return &VirtRes{} })
gob.Register(&VirtRes{})
}
const (
@@ -192,7 +192,6 @@ func (obj *VirtRes) Init() error {
}
}
obj.wg = &sync.WaitGroup{}
obj.BaseRes.Kind = "virt"
return obj.BaseRes.Init() // call base init, b/c we're overriding
}
@@ -1000,7 +999,7 @@ func (d *diskDevice) GetXML(idx int) string {
b += "<disk type='file' device='disk'>"
b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type)
b += fmt.Sprintf("<source file='%s'/>", source)
b += fmt.Sprintf("<target dev='vd%s' bus='virtio'/>", (string)(idx+97)) // TODO: 26, 27... should be 'aa', 'ab'...
b += fmt.Sprintf("<target dev='vd%s' bus='virtio'/>", numToAlpha(idx))
b += "</disk>"
return b
}
@@ -1011,7 +1010,7 @@ func (d *cdRomDevice) GetXML(idx int) string {
b += "<disk type='file' device='cdrom'>"
b += fmt.Sprintf("<driver name='qemu' type='%s'/>", d.Type)
b += fmt.Sprintf("<source file='%s'/>", source)
b += fmt.Sprintf("<target dev='hd%s' bus='ide'/>", (string)(idx+97)) // TODO: 26, 27... should be 'aa', 'ab'...
b += fmt.Sprintf("<target dev='hd%s' bus='ide'/>", numToAlpha(idx))
b += "<readonly/>"
b += "</disk>"
return b
@@ -1171,15 +1170,38 @@ func isNotFound(err error) bool {
return false // some other error
}
// expandHome does a simple expansion of the tilde into your $HOME value.
// expandHome does an expansion of ~/ or ~james/ into user's home dir value.
func expandHome(p string) (string, error) {
// TODO: this doesn't match strings of the form: ~james/...
if !strings.HasPrefix(p, "~/") {
return p, nil
if strings.HasPrefix(p, "~/") {
usr, err := user.Current()
if err != nil {
return p, fmt.Errorf("can't expand ~ into home directory")
}
return path.Join(usr.HomeDir, p[len("~/"):]), nil
}
usr, err := user.Current()
// check if provided path is in format ~username and keep track of provided username
r, err := regexp.Compile("~([^/]+)/")
if err != nil {
return p, fmt.Errorf("can't expand ~ into home directory")
return p, errwrap.Wrapf(err, "can't compile regexp")
}
return path.Join(usr.HomeDir, p[len("~/"):]), nil
if match := r.FindStringSubmatch(p); match != nil {
username := match[len(match)-1]
usr, err := user.Lookup(username)
if err != nil {
return p, fmt.Errorf("can't expand %s into home directory", match[0])
}
return path.Join(usr.HomeDir, p[len(match[0]):]), nil
}
return p, nil
}
func numToAlpha(idx int) string {
var mod = idx % 26
var div = idx / 26
if div > 0 {
return numToAlpha(div-1) + string(rune(mod+int('a')))
}
return string(rune(mod + int('a')))
}

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