Compare commits
203 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71a82b0a34 | ||
|
|
b7bd2d2664 | ||
|
|
cd26a0770d | ||
|
|
46893e84c3 | ||
|
|
567dcaf79d | ||
|
|
9368c7e05f | ||
|
|
654b3e9dbe | ||
|
|
f09db490f0 | ||
|
|
30d93cfde7 | ||
|
|
41b3db7d6b | ||
|
|
2a60debceb | ||
|
|
eb30642b6f | ||
|
|
ea85e2af6b | ||
|
|
ef979a0839 | ||
|
|
e0107b1dda | ||
|
|
ccc00f913d | ||
|
|
ad3c6bdc88 | ||
|
|
8fe3891ea9 | ||
|
|
63f21952f4 | ||
|
|
361d643ce7 | ||
|
|
abe1ffaab6 | ||
|
|
fc24c91dde | ||
|
|
53cabd5ee4 | ||
|
|
2b1e8cdbee | ||
|
|
9715146495 | ||
|
|
22b0b89949 | ||
|
|
2ebc23a777 | ||
|
|
0199285319 | ||
|
|
277ab2fe44 | ||
|
|
8a96dfdc8a | ||
|
|
66fbbb940a | ||
|
|
716ea1bb3c | ||
|
|
3d701d3daa | ||
|
|
598c74657c | ||
|
|
4bd53d5ab0 | ||
|
|
70f8d54a31 | ||
|
|
4ef25a33fc | ||
|
|
f5dd90a8dd | ||
|
|
a84defd689 | ||
|
|
1cf88d9540 | ||
|
|
644a0ee8c8 | ||
|
|
e9d5dc8fee | ||
|
|
8003202beb | ||
|
|
b46432b5b6 | ||
|
|
5e3f03df06 | ||
|
|
8ab8e6679a | ||
|
|
786b896018 | ||
|
|
40723f8705 | ||
|
|
2a0721bddf | ||
|
|
ff01e4a5e7 | ||
|
|
6794aff77c | ||
|
|
636f2a36b1 | ||
|
|
eee652cefe | ||
|
|
6d45cd45d1 | ||
|
|
f5fb135793 | ||
|
|
6bf32c978a | ||
|
|
8d3011fb9c | ||
|
|
9260066fa3 | ||
|
|
5e45c5805b | ||
|
|
db4de12767 | ||
|
|
d429795737 | ||
|
|
276219a691 | ||
|
|
03c1df98f4 | ||
|
|
79ba750dd5 | ||
|
|
1d0e187838 | ||
|
|
ad1e48aa2d | ||
|
|
7032eea045 | ||
|
|
bdb970203c | ||
|
|
fa4f5abc78 | ||
|
|
0c7b05b233 | ||
|
|
4ca98b5f17 | ||
|
|
4e00c78410 | ||
|
|
17adb19c0d | ||
|
|
1db936e253 | ||
|
|
7194ba7e0e | ||
|
|
59b9b6f091 | ||
|
|
c1ec8d15f3 | ||
|
|
24ba6abc6b | ||
|
|
f6c1bba3b6 | ||
|
|
a606961a22 | ||
|
|
cafe0e4ec2 | ||
|
|
e28c1266cf | ||
|
|
c1605a4f22 | ||
|
|
7aeb55de70 | ||
|
|
8ca65f9fda | ||
|
|
94524d1156 | ||
|
|
a1ed03478b | ||
|
|
402a6379b9 | ||
|
|
5d45bcd552 | ||
|
|
f1fa64c170 | ||
|
|
50fc78564c | ||
|
|
3e5863dc8a | ||
|
|
94b447a9c5 | ||
|
|
78d769797f | ||
|
|
672baae126 | ||
|
|
e942d71ed2 | ||
|
|
f5d24cf86c | ||
|
|
f63b1cd56d | ||
|
|
66719b3cda | ||
|
|
a5e9f6a6fc | ||
|
|
f821afdf3e | ||
|
|
2c61de83c6 | ||
|
|
6da6f75b88 | ||
|
|
a55807a708 | ||
|
|
fce86b0d08 | ||
|
|
d26b503dca | ||
|
|
5363839ac8 | ||
|
|
715a4bf393 | ||
|
|
8f83ecee65 | ||
|
|
2eed4bda42 | ||
|
|
f4e1e24ca7 | ||
|
|
05c540e6cc | ||
|
|
9656390c87 | ||
|
|
4b6470d1e1 | ||
|
|
56471c2fe4 | ||
|
|
9f56e4a582 | ||
|
|
12ea860eba | ||
|
|
b876c29862 | ||
|
|
6bbce039aa | ||
|
|
1584f20220 | ||
|
|
dcad5abc1c | ||
|
|
ab73261fd4 | ||
|
|
05b75c0a44 | ||
|
|
ba7ef0788e | ||
|
|
3aaa80974e | ||
|
|
995ca32eee | ||
|
|
bf5f48b85b | ||
|
|
d6e386a555 | ||
|
|
a0a71f683c | ||
|
|
7adf88b55b | ||
|
|
8a9d47fc4b | ||
|
|
2a0a69c917 | ||
|
|
aeab8f55bd | ||
|
|
9407050598 | ||
|
|
b99da63306 | ||
|
|
f0d6cfaae4 | ||
|
|
3120628d8a | ||
|
|
2654384461 | ||
|
|
eac3b25dc9 | ||
|
|
7788f91dd5 | ||
|
|
d0c9b7170c | ||
|
|
d84caa5528 | ||
|
|
2ab72bdf94 | ||
|
|
f6833fde29 | ||
|
|
fa8a50b525 | ||
|
|
d80c6bbf1d | ||
|
|
6f3ac4bf2a | ||
|
|
a6dc81a38e | ||
|
|
81c5ce40d4 | ||
|
|
c59f45a37b | ||
|
|
1b01f908e3 | ||
|
|
9720812a78 | ||
|
|
05b4066ba6 | ||
|
|
50c458b6cc | ||
|
|
2ab6d61a61 | ||
|
|
b77a39bdff | ||
|
|
d3f7432861 | ||
|
|
7f3ef5bf85 | ||
|
|
659fb3eb82 | ||
|
|
d1315bb092 | ||
|
|
b4ac0e2e7c | ||
|
|
bfe619272e | ||
|
|
963f025011 | ||
|
|
b8cdcaeb75 | ||
|
|
6b6dc75152 | ||
|
|
23647445d7 | ||
|
|
e60dda5027 | ||
|
|
f39551952f | ||
|
|
a9538052bf | ||
|
|
267d5179f5 | ||
|
|
10b8c93da4 | ||
|
|
c999f0c2cd | ||
|
|
54615dc03b | ||
|
|
9aea95ce85 | ||
|
|
80f48291f3 | ||
|
|
1a164cee3e | ||
|
|
da494cdc7c | ||
|
|
06635dfa75 | ||
|
|
a56fb3c8cd | ||
|
|
2dc3c62bbd | ||
|
|
0339d0caa8 | ||
|
|
3b5678dd91 | ||
|
|
82ff34234d | ||
|
|
f3d1369764 | ||
|
|
ed61444d82 | ||
|
|
ce0d68a8ba | ||
|
|
74aadbadb8 | ||
|
|
58f41eddd9 | ||
|
|
4726445ec4 | ||
|
|
3a85384377 | ||
|
|
d20b529508 | ||
|
|
7199f558e8 | ||
|
|
674cb24a1a | ||
|
|
02c7336315 | ||
|
|
cde052d819 | ||
|
|
989cc8d236 | ||
|
|
1186d63653 | ||
|
|
6e68d6dda0 | ||
|
|
7d876701b3 | ||
|
|
dbbb483853 | ||
|
|
85e9473d56 | ||
|
|
427d424707 | ||
|
|
f90c5fafa4 |
1
.dockerignore
Normal file
@@ -0,0 +1 @@
|
||||
docker
|
||||
19
.editorconfig
Normal file
@@ -0,0 +1,19 @@
|
||||
; This file is for unifying the coding style for different editors and IDEs.
|
||||
; Plugins are available for notepad++, emacs, vim, gedit,
|
||||
; textmate, visual studio, and more.
|
||||
;
|
||||
; See http://editorconfig.org for details.
|
||||
|
||||
# Top-most EditorConfig file.
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
1
.gitignore
vendored
@@ -6,4 +6,5 @@ old/
|
||||
tmp/
|
||||
*_stringer.go
|
||||
mgmt
|
||||
mgmt.static
|
||||
rpmbuild/
|
||||
|
||||
12
.gitmodules
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
[submodule "vendor/github.com/coreos/etcd"]
|
||||
path = vendor/github.com/coreos/etcd
|
||||
url = https://github.com/coreos/etcd/
|
||||
[submodule "vendor/google.golang.org/grpc"]
|
||||
path = vendor/google.golang.org/grpc
|
||||
url = https://github.com/grpc/grpc-go
|
||||
[submodule "vendor/github.com/grpc-ecosystem/grpc-gateway"]
|
||||
path = vendor/github.com/grpc-ecosystem/grpc-gateway
|
||||
url = https://github.com/grpc-ecosystem/grpc-gateway
|
||||
[submodule "vendor/gopkg.in/fsnotify.v1"]
|
||||
path = vendor/gopkg.in/fsnotify.v1
|
||||
url = https://gopkg.in/fsnotify.v1
|
||||
10
.travis.yml
@@ -1,15 +1,17 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.4.3
|
||||
- 1.5.2
|
||||
- 1.6
|
||||
- 1.7
|
||||
- tip
|
||||
dist: trusty
|
||||
sudo: required
|
||||
sudo: false
|
||||
before_install: 'git fetch --unshallow'
|
||||
install: 'make deps'
|
||||
script: 'make test'
|
||||
matrix:
|
||||
fast_finish: true
|
||||
allow_failures:
|
||||
- go: tip
|
||||
- go: 1.7
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
|
||||
1
AUTHORS
@@ -5,3 +5,4 @@ For a more exhaustive list please run: git log --format='%aN' | sort -u
|
||||
This list is sorted alphabetically by first name.
|
||||
|
||||
James Shubin
|
||||
Paul Morgan
|
||||
|
||||
401
DOCUMENTATION.md
@@ -30,18 +30,26 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
1. [Overview](#overview)
|
||||
2. [Project description - What the project does](#project-description)
|
||||
3. [Setup - Getting started with mgmt](#setup)
|
||||
4. [Usage/FAQ - Notes on usage and frequently asked questions](#usage-and-frequently-asked-questions)
|
||||
5. [Reference - Detailed reference](#reference)
|
||||
* [graph.yaml](#graph.yaml)
|
||||
4. [Features - All things mgmt can do](#features)
|
||||
* [Autoedges - Automatic resource relationships](#autoedges)
|
||||
* [Autogrouping - Automatic resource grouping](#autogrouping)
|
||||
* [Automatic clustering - Automatic cluster management](#automatic-clustering)
|
||||
* [Remote mode - Remote "agent-less" execution](#remote-agent-less-mode)
|
||||
* [Puppet support - write manifest code for mgmt](#puppet-support)
|
||||
5. [Resources - All built-in primitives](#resources)
|
||||
6. [Usage/FAQ - Notes on usage and frequently asked questions](#usage-and-frequently-asked-questions)
|
||||
7. [Reference - Detailed reference](#reference)
|
||||
* [Meta parameters](#meta-parameters)
|
||||
* [Graph definition file](#graph-definition-file)
|
||||
* [Command line](#command-line)
|
||||
6. [Examples - Example configurations](#examples)
|
||||
7. [Development - Background on module development and reporting bugs](#development)
|
||||
8. [Authors - Authors and contact information](#authors)
|
||||
8. [Examples - Example configurations](#examples)
|
||||
9. [Development - Background on module development and reporting bugs](#development)
|
||||
10. [Authors - Authors and contact information](#authors)
|
||||
|
||||
##Overview
|
||||
|
||||
The `mgmt` tool is a research prototype to demonstrate next generation config
|
||||
management techniques. Hopefully it will evolve into a useful, robust tool.
|
||||
The `mgmt` tool is a next generation config management prototype. It's not yet
|
||||
ready for production, but we hope to get there soon. Get involved today!
|
||||
|
||||
##Project Description
|
||||
|
||||
@@ -49,6 +57,16 @@ The mgmt tool is a distributed, event driven, config management tool, that
|
||||
supports parallel execution, and librarification to be used as the management
|
||||
foundation in and for, new and existing software.
|
||||
|
||||
For more information, you may like to read some blog posts from the author:
|
||||
|
||||
* [Next generation config mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* [Automatic edges in mgmt](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
* [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
* [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
|
||||
There is also an [introductory video](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) available.
|
||||
Older videos and other material [is available](https://github.com/purpleidea/mgmt/#on-the-web).
|
||||
|
||||
##Setup
|
||||
|
||||
During this prototype phase, the tool can be run out of the source directory.
|
||||
@@ -57,6 +75,217 @@ get started. Beware that this _can_ cause data loss. Understand what you're
|
||||
doing first, or perform these actions in a virtual environment such as the one
|
||||
provided by [Oh-My-Vagrant](https://github.com/purpleidea/oh-my-vagrant).
|
||||
|
||||
##Features
|
||||
|
||||
This section details the numerous features of mgmt and some caveats you might
|
||||
need to be aware of.
|
||||
|
||||
###Autoedges
|
||||
|
||||
Automatic edges, or AutoEdges, is the mechanism in mgmt by which it will
|
||||
automatically create dependencies for you between resources. For example,
|
||||
since mgmt can discover which files are installed by a package it will
|
||||
automatically ensure that any file resource you declare that matches a
|
||||
file installed by your package resource will only be processed after the
|
||||
package is installed.
|
||||
|
||||
####Controlling autoedges
|
||||
|
||||
Though autoedges is likely to be very helpful and avoid you having to declare
|
||||
all dependencies explicitly, there are cases where this behaviour is
|
||||
undesirable.
|
||||
|
||||
Some distributions allow package installations to automatically start the
|
||||
service they ship. This can be problematic in the case of packages like MySQL
|
||||
as there are configuration options that need to be set before MySQL is ever
|
||||
started for the first time (or you'll need to wipe the data directory). In
|
||||
order to handle this situation you can disable autoedges per resource and
|
||||
explicitly declare that you want `my.cnf` to be written to disk before the
|
||||
installation of the `mysql-server` package.
|
||||
|
||||
You can disable autoedges for a resource by setting the `autoedge` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
|
||||
###Autogrouping
|
||||
|
||||
Automatic grouping or AutoGroup is the mechanism in mgmt by which it will
|
||||
automatically group multiple resource vertices into a single one. This is
|
||||
particularly useful for grouping multiple package resources into a single
|
||||
resource, since the multiple installations can happen together in a single
|
||||
transaction, which saves a lot of time because package resources typically have
|
||||
a large fixed cost to running (downloading and verifying the package repo) and
|
||||
if they are grouped they share this fixed cost. This grouping feature can be
|
||||
used for other use cases too.
|
||||
|
||||
You can disable autogrouping for a resource by setting the `autogroup` key on
|
||||
the meta attributes of that resource to `false`.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
|
||||
###Automatic clustering
|
||||
|
||||
Automatic clustering is a feature by which mgmt automatically builds, scales,
|
||||
and manages the embedded etcd cluster which is compiled into mgmt itself. It is
|
||||
quite helpful for rapidly bootstrapping clusters and avoiding the extra work to
|
||||
setup etcd.
|
||||
|
||||
If you prefer to avoid this feature. you can always opt to use an existing etcd
|
||||
cluster that is managed separately from mgmt by pointing your mgmt agents at it
|
||||
with the `--seeds` variable.
|
||||
|
||||
####Blog post
|
||||
|
||||
You can read the introductory blog post about this topic here:
|
||||
[https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
|
||||
###Remote ("agent-less") mode
|
||||
|
||||
Remote mode is a special mode that lets you kick off mgmt runs on one or more
|
||||
remote machines which are only accessible via SSH. In this mode the initiating
|
||||
host connects over SSH, copies over the `mgmt` binary, opens an SSH tunnel, and
|
||||
runs the remote program while simultaneously passing the etcd traffic back
|
||||
through the tunnel so that the initiators etcd cluster can be used to exchange
|
||||
resource data.
|
||||
|
||||
The interesting benefit of this architecture is that multiple hosts which can't
|
||||
connect directly use the initiator to pass the important traffic through to each
|
||||
other. Once the cluster has converged all the remote programs can shutdown
|
||||
leaving no residual agent.
|
||||
|
||||
This mode can also be useful for bootstrapping a new host where you'd like to
|
||||
have the service run continuously and as part of an mgmt cluster normally.
|
||||
|
||||
In particular, when combined with the `--converged-timeout` parameter, the
|
||||
entire set of running mgmt agents will need to all simultaneously converge for
|
||||
the group to exit. This is particularly useful for bootstrapping new clusters
|
||||
which need to exchange information that is only available at run time.
|
||||
|
||||
####Blog post
|
||||
|
||||
An introductory blog post about this topic will follow soon.
|
||||
|
||||
###Puppet support
|
||||
|
||||
You can supply a Puppet manifest instead of creating the (YAML) graph manually.
|
||||
Puppet must be installed and in `mgmt`'s search path. You also need the
|
||||
[ffrank-mgmtgraph Puppet module](https://forge.puppet.com/ffrank/mgmtgraph).
|
||||
|
||||
Invoke `mgmt` with the `--puppet` switch, which supports 3 variants:
|
||||
|
||||
1. Request the configuration from the Puppet Master (like `puppet agent` does)
|
||||
|
||||
mgmt run --puppet agent
|
||||
|
||||
2. Compile a local manifest file (like `puppet apply`)
|
||||
|
||||
mgmt run --puppet /path/to/my/manifest.pp
|
||||
|
||||
3. Compile an ad hoc manifest from the commandline (like `puppet apply -e`)
|
||||
|
||||
mgmt run --puppet 'file { "/etc/ntp.conf": ensure => file }'
|
||||
|
||||
For more details and caveats see [Puppet.md](Puppet.md).
|
||||
|
||||
####Blog post
|
||||
|
||||
An introductory post on the Puppet support is on
|
||||
[Felix's blog](http://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/).
|
||||
|
||||
##Resources
|
||||
|
||||
This section lists all the built-in resources and their properties. The
|
||||
resource primitives in `mgmt` are typically more powerful than resources in
|
||||
other configuration management systems because they can be event based which
|
||||
lets them respond in real-time to converge to the desired state. This property
|
||||
allows you to build more complex resources that you probably hadn't considered
|
||||
in the past.
|
||||
|
||||
In addition to the resource specific properties, there are resource properties
|
||||
(otherwise known as parameters) which can apply to every resource. These are
|
||||
called [meta parameters](#meta-parameters) and are listed separately. Certain
|
||||
meta parameters aren't very useful when combined with certain resources, but
|
||||
in general, it should be fairly obvious, such as when combining the `noop` meta
|
||||
parameter with the [Noop](#Noop) resource.
|
||||
|
||||
* [Exec](#Exec): Execute shell commands on the system.
|
||||
* [File](#File): Manage files and directories.
|
||||
* [Msg](#Msg): Send log messages.
|
||||
* [Noop](#Noop): A simple resource that does nothing.
|
||||
* [Pkg](#Pkg): Manage system packages with PackageKit.
|
||||
* [Svc](#Svc): Manage system systemd services.
|
||||
* [Timer](#Timer): Manage system systemd services.
|
||||
|
||||
###Exec
|
||||
|
||||
The exec resource can execute commands on your system.
|
||||
|
||||
###File
|
||||
|
||||
The file resource manages files and directories. In `mgmt`, directories are
|
||||
identified by a trailing slash in their path name. File have no such slash.
|
||||
|
||||
####Path
|
||||
|
||||
The path property specifies the file or directory that we are managing.
|
||||
|
||||
####Content
|
||||
|
||||
The content property is a string that specifies the desired file contents.
|
||||
|
||||
####Source
|
||||
|
||||
The source property points to a source file or directory path that we wish to
|
||||
copy over and use as the desired contents for our resource.
|
||||
|
||||
####State
|
||||
|
||||
The state property describes the action we'd like to apply for the resource. The
|
||||
possible values are: `exists` and `absent`.
|
||||
|
||||
####Recurse
|
||||
|
||||
The recurse property limits whether file resource operations should recurse into
|
||||
and monitor directory contents with a depth greater than one.
|
||||
|
||||
####Force
|
||||
|
||||
The force property is required if we want the file resource to be able to change
|
||||
a file into a directory or vice-versa. If such a change is needed, but the force
|
||||
property is not set to `true`, then this file resource will error.
|
||||
|
||||
###Msg
|
||||
|
||||
The msg resource sends messages to the main log, or an external service such
|
||||
as systemd's journal.
|
||||
|
||||
###Noop
|
||||
|
||||
The noop resource does absolutely nothing. It does have some utility in testing
|
||||
`mgmt` and also as a placeholder in the resource graph.
|
||||
|
||||
###Pkg
|
||||
|
||||
The pkg resource is used to manage system packages. This resource works on many
|
||||
different distributions because it uses the underlying packagekit facility which
|
||||
supports different backends for different environments. This ensures that we
|
||||
have great Debian (deb/dpkg) and Fedora (rpm/dnf) support simultaneously.
|
||||
|
||||
###Svc
|
||||
|
||||
The service resource is still very WIP. Please help us my improving it!
|
||||
|
||||
###Timer
|
||||
|
||||
This resource needs better documentation. Please help us my improving it!
|
||||
|
||||
##Usage and frequently asked questions
|
||||
(Send your questions as a patch to this FAQ! I'll review it, merge it, and
|
||||
respond by commit with the answer.)
|
||||
@@ -75,6 +304,58 @@ chosen, but it was also somewhat arbitrary. If there is available interest,
|
||||
good reasoning, *and* patches, then we would consider either switching or
|
||||
supporting both, but this is not a high priority at this time.
|
||||
|
||||
###Can I use an existing etcd cluster instead of the automatic embedded servers?
|
||||
|
||||
Yes, it's possible to use an existing etcd cluster instead of the automatic,
|
||||
elastic embedded etcd servers. To do so, simply point to the cluster with the
|
||||
`--seeds` variable, the same way you would if you were seeding a new member to
|
||||
an existing mgmt cluster.
|
||||
|
||||
The downside to this approach is that you won't benefit from the automatic
|
||||
elastic nature of the embedded etcd servers, and that you're responsible if you
|
||||
accidentally break your etcd cluster, or if you use an unsupported version.
|
||||
|
||||
###What does the error message about an inconsistent dataDir mean?
|
||||
|
||||
If you get an error message similar to:
|
||||
|
||||
```
|
||||
Etcd: Connect: CtxError...
|
||||
Etcd: CtxError: Reason: CtxDelayErr(5s): No endpoints available yet!
|
||||
Etcd: Connect: Endpoints: []
|
||||
Etcd: The dataDir (/var/lib/mgmt/etcd) might be inconsistent or corrupt.
|
||||
```
|
||||
|
||||
This happens when there are a series of fatal connect errors in a row. This can
|
||||
happen when you start `mgmt` using a dataDir that doesn't correspond to the
|
||||
current cluster view. As a result, the embedded etcd server never finishes
|
||||
starting up, and as a result, a default endpoint never gets added. The solution
|
||||
is to either reconcile the mistake, and if there is no important data saved, you
|
||||
can remove the etcd dataDir. This is typically `/var/lib/mgmt/etcd/member/`.
|
||||
|
||||
###Why do resources have both a `Compare` method and an `IFF` (on the UUID) method?
|
||||
|
||||
The `Compare()` methods are for determining if two resources are effectively the
|
||||
same, which is used to make graph change delta's efficient. This is when we want
|
||||
to change from the current running graph to a new graph, but preserve the common
|
||||
vertices. Since we want to make this process efficient, we only update the parts
|
||||
that are different, and leave everything else alone. This `Compare()` method can
|
||||
tell us if two resources are the same.
|
||||
|
||||
The `IFF()` method is part of the whole UUID system, which is for discerning if
|
||||
a resource meets the requirements another expects for an automatic edge. This is
|
||||
because the automatic edge system assumes a unified UUID pattern to test for
|
||||
equality. In the future it might be helpful or sane to merge the two similar
|
||||
comparison functions although for now they are separate because they are
|
||||
actually answer different questions.
|
||||
|
||||
###Did you know that there is a band named `MGMT`?
|
||||
|
||||
I didn't realize this when naming the project, and it is accidental. After much
|
||||
anguishing, I chose the name because it was short and I thought it was
|
||||
appropriately descriptive. If you need a less ambiguous search term or phrase,
|
||||
you can try using `mgmtconfig` or `mgmt config`.
|
||||
|
||||
###You didn't answer my question, or I have a question!
|
||||
|
||||
It's best to ask on [IRC](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
@@ -92,11 +373,42 @@ information on these options, please view the source at:
|
||||
If you feel that a well used option needs documenting here, please patch it!
|
||||
|
||||
###Overview of reference
|
||||
* [graph.yaml](#graph.yaml): Main graph definition file.
|
||||
* [Meta parameters](#meta-parameters): List of available resource meta parameters.
|
||||
* [Graph definition file](#graph-definition-file): Main graph definition file.
|
||||
* [Command line](#command-line): Command line parameters.
|
||||
|
||||
###graph.yaml
|
||||
This is the compiled graph definition file. The format is currently
|
||||
###Meta parameters
|
||||
These meta parameters are special parameters (or properties) which can apply to
|
||||
any resource. The usefulness of doing so will depend on the particular meta
|
||||
parameter and resource combination.
|
||||
|
||||
####AutoEdge
|
||||
Boolean. Should we generate auto edges for this resource?
|
||||
|
||||
####AutoGroup
|
||||
Boolean. Should we attempt to automatically group this resource with others?
|
||||
|
||||
####Noop
|
||||
Boolean. Should the Apply portion of the CheckApply method of the resource
|
||||
make any changes? Noop is a concatenation of no-operation.
|
||||
|
||||
####Retry
|
||||
Integer. The number of times to retry running the resource on error. Use -1 for
|
||||
infinite. This currently applies for both the Watch operation (which can fail)
|
||||
and for the CheckApply operation. While they could have separate values, I've
|
||||
decided to use the same ones for both until there's a proper reason to want to
|
||||
do something differently for the Watch errors.
|
||||
|
||||
####Delay
|
||||
Integer. Number of milliseconds to wait between retries. The same value is
|
||||
shared between the Watch and CheckApply retries. This currently applies for both
|
||||
the Watch operation (which can fail) and for the CheckApply operation. While
|
||||
they could have separate values, I've decided to use the same ones for both
|
||||
until there's a proper reason to want to do something differently for the Watch
|
||||
errors.
|
||||
|
||||
###Graph definition file
|
||||
graph.yaml is the compiled graph definition file. The format is currently
|
||||
undocumented, but by looking through the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
you can probably figure out most of it, as it's fairly intuitive.
|
||||
|
||||
@@ -114,12 +426,79 @@ Exit if the machine has converged for approximately this many seconds.
|
||||
Exit when the agent has run for approximately this many seconds. This is not
|
||||
generally recommended, but may be useful for users who know what they're doing.
|
||||
|
||||
####`--noop`
|
||||
Globally force all resources into no-op mode. This also disables the export to
|
||||
etcd functionality, but does not disable resource collection, however all
|
||||
resources that are collected will have their individual noop settings set.
|
||||
|
||||
####`--remote <graph.yaml>`
|
||||
Point to a graph file to run on the remote host specified within. This parameter
|
||||
can be used multiple times if you'd like to remotely run on multiple hosts in
|
||||
parallel.
|
||||
|
||||
####`--allow-interactive`
|
||||
Allow interactive prompting for SSH passwords if there is no authentication
|
||||
method that works.
|
||||
|
||||
####`--ssh-priv-id-rsa`
|
||||
Specify the path for finding SSH keys. This defaults to `~/.ssh/id_rsa`. To
|
||||
never use this method of authentication, set this to the empty string.
|
||||
|
||||
####`--cconns`
|
||||
The maximum number of concurrent remote ssh connections to run. This defaults
|
||||
to `0`, which means unlimited.
|
||||
|
||||
####`--no-caching`
|
||||
Don't allow remote caching of the remote execution binary. This will require
|
||||
the binary to be copied over for every remote execution, but it limits the
|
||||
likelihood that there is leftover information from the configuration process.
|
||||
|
||||
####`--prefix <path>`
|
||||
Specify a path to a custom working directory prefix. This directory will get
|
||||
created if it does not exist. This usually defaults to `/var/lib/mgmt/`. This
|
||||
can't be combined with the `--tmp-prefix` option. It can be combined with the
|
||||
`--allow-tmp-prefix` option.
|
||||
|
||||
####`--tmp-prefix`
|
||||
If this option is specified, a temporary prefix will be used instead of the
|
||||
default prefix. This can't be combined with the `--prefix` option.
|
||||
|
||||
####`--allow-tmp-prefix`
|
||||
If this option is specified, we will attempt to fall back to a temporary prefix
|
||||
if the primary prefix couldn't be created. This is useful for avoiding failures
|
||||
in environments where the primary prefix may or may not be available, but you'd
|
||||
like to try. The canonical example is when running `mgmt` with `--remote` there
|
||||
might be a cached copy of the binary in the primary prefix, but in case there's
|
||||
no binary available continue working in a temporary directory to avoid failure.
|
||||
|
||||
##Examples
|
||||
For example configurations, please consult the [examples/](https://github.com/purpleidea/mgmt/tree/master/examples) directory in the git
|
||||
source repository. It is available from:
|
||||
|
||||
[https://github.com/purpleidea/mgmt/tree/master/examples](https://github.com/purpleidea/mgmt/tree/master/examples)
|
||||
|
||||
### Systemd:
|
||||
See [`misc/mgmt.service`](misc/mgmt.service) for a sample systemd unit file.
|
||||
This unit file is part of the RPM.
|
||||
|
||||
To specify your custom options for `mgmt` on a systemd distro:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/systemd/system/mgmt.service.d/
|
||||
|
||||
cat > /etc/systemd/system/mgmt.service.d/env.conf <<EOF
|
||||
# Environment variables:
|
||||
MGMT_SEEDS=http://127.0.0.1:2379
|
||||
MGMT_CONVERGED_TIMEOUT=-1
|
||||
MGMT_MAX_RUNTIME=0
|
||||
|
||||
# Other CLI options if necessary.
|
||||
#OPTS="--max-runtime=0"
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
##Development
|
||||
|
||||
This is a project that I started in my free time in 2013. Development is driven
|
||||
|
||||
134
Makefile
@@ -15,29 +15,66 @@
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
SHELL = /bin/bash
|
||||
.PHONY: all version program path deps run race build clean test format docs rpmbuild rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
|
||||
SHELL = /usr/bin/env bash
|
||||
.PHONY: all art cleanart version program path deps run race generate build clean test gofmt yamlfmt format docs rpmbuild mkdirs rpm srpm spec tar upload upload-sources upload-srpms upload-rpms copr
|
||||
.SILENT: clean
|
||||
|
||||
SVERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always)
|
||||
VERSION := $(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0)
|
||||
PROGRAM := $(shell basename --suffix=-$(VERSION) $(notdir $(CURDIR)))
|
||||
SVERSION := $(or $(SVERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --dirty --always))
|
||||
VERSION := $(or $(VERSION),$(shell git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --tags --abbrev=0))
|
||||
PROGRAM := $(shell echo $(notdir $(CURDIR)) | cut -f1 -d"-")
|
||||
OLDGOLANG := $(shell go version | grep -E 'go1.3|go1.4')
|
||||
ifeq ($(VERSION),$(SVERSION))
|
||||
RELEASE = 1
|
||||
else
|
||||
RELEASE = untagged
|
||||
endif
|
||||
ARCH = $(shell arch)
|
||||
SPEC = rpmbuild/SPECS/mgmt.spec
|
||||
SOURCE = rpmbuild/SOURCES/mgmt-$(VERSION).tar.bz2
|
||||
SRPM = rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).src.rpm
|
||||
SRPM_BASE = mgmt-$(VERSION)-$(RELEASE).src.rpm
|
||||
RPM = rpmbuild/RPMS/mgmt-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
ARCH = $(uname -m)
|
||||
SPEC = rpmbuild/SPECS/$(PROGRAM).spec
|
||||
SOURCE = rpmbuild/SOURCES/$(PROGRAM)-$(VERSION).tar.bz2
|
||||
SRPM = rpmbuild/SRPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).src.rpm
|
||||
SRPM_BASE = $(PROGRAM)-$(VERSION)-$(RELEASE).src.rpm
|
||||
RPM = rpmbuild/RPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).$(ARCH).rpm
|
||||
USERNAME := $(shell cat ~/.config/copr 2>/dev/null | grep username | awk -F '=' '{print $$2}' | tr -d ' ')
|
||||
SERVER = 'dl.fedoraproject.org'
|
||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/mgmt'
|
||||
REMOTE_PATH = 'pub/alt/$(USERNAME)/$(PROGRAM)'
|
||||
|
||||
all: docs
|
||||
#
|
||||
# art
|
||||
#
|
||||
art: art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
|
||||
|
||||
cleanart:
|
||||
rm -f art/mgmt_logo_default_symbol.png art/mgmt_logo_default_tall.png art/mgmt_logo_default_wide.png art/mgmt_logo_reversed_symbol.png art/mgmt_logo_reversed_tall.png art/mgmt_logo_reversed_wide.png art/mgmt_logo_white_symbol.png art/mgmt_logo_white_tall.png art/mgmt_logo_white_wide.png
|
||||
|
||||
# NOTE: the widths are arbitrary
|
||||
art/mgmt_logo_default_symbol.png: art/mgmt_logo_default_symbol.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_default_tall.png: art/mgmt_logo_default_tall.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_default_wide.png: art/mgmt_logo_default_wide.svg
|
||||
inkscape --export-background='#ffffff' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_symbol.png: art/mgmt_logo_reversed_symbol.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_tall.png: art/mgmt_logo_reversed_tall.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_reversed_wide.png: art/mgmt_logo_reversed_wide.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_symbol.png: art/mgmt_logo_white_symbol.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 300 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_tall.png: art/mgmt_logo_white_tall.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 400 $(@:png=svg)
|
||||
|
||||
art/mgmt_logo_white_wide.png: art/mgmt_logo_white_wide.svg
|
||||
inkscape --export-background='#231f20' --without-gui --export-png "$@" --export-width 800 $(@:png=svg)
|
||||
|
||||
all: docs $(PROGRAM).static
|
||||
|
||||
# show the current version
|
||||
version:
|
||||
@@ -53,39 +90,56 @@ deps:
|
||||
./misc/make-deps.sh
|
||||
|
||||
run:
|
||||
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
|
||||
# include race flag
|
||||
race:
|
||||
find -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
find . -maxdepth 1 -type f -name '*.go' -not -name '*_test.go' | xargs go run -race -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)"
|
||||
|
||||
build: mgmt
|
||||
|
||||
mgmt: main.go
|
||||
@echo "Building: $(PROGRAM), version: $(SVERSION)."
|
||||
generate:
|
||||
go generate
|
||||
# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
if go version | grep -qE 'go1.3|go1.4'; then \
|
||||
go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o mgmt; \
|
||||
else \
|
||||
go build -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o mgmt; \
|
||||
fi
|
||||
|
||||
build: $(PROGRAM)
|
||||
|
||||
$(PROGRAM): main.go
|
||||
@echo "Building: $(PROGRAM), version: $(SVERSION)..."
|
||||
ifneq ($(OLDGOLANG),)
|
||||
@# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
time go build -ldflags "-X main.program $(PROGRAM) -X main.version $(SVERSION)" -o $(PROGRAM);
|
||||
else
|
||||
time go build -ldflags "-X main.program=$(PROGRAM) -X main.version=$(SVERSION)" -o $(PROGRAM);
|
||||
endif
|
||||
|
||||
$(PROGRAM).static: main.go
|
||||
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..."
|
||||
go generate
|
||||
ifneq ($(OLDGOLANG),)
|
||||
@# avoid equals sign in old golang versions eg in: -X foo=bar
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program $(PROGRAM) -X main.version $(SVERSION)' -o $(PROGRAM).static;
|
||||
else
|
||||
go build -a -installsuffix cgo -tags netgo -ldflags '-extldflags "-static" -X main.program=$(PROGRAM) -X main.version=$(SVERSION)' -o $(PROGRAM).static;
|
||||
endif
|
||||
|
||||
clean:
|
||||
[ ! -e mgmt ] || rm mgmt
|
||||
[ ! -e $(PROGRAM) ] || rm $(PROGRAM)
|
||||
rm -f *_stringer.go # generated by `go generate`
|
||||
rm -f *_mock.go # generated by `go generate`
|
||||
|
||||
test:
|
||||
./test.sh
|
||||
|
||||
format:
|
||||
find -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
|
||||
find -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \;
|
||||
gofmt:
|
||||
find . -maxdepth 3 -type f -name '*.go' -not -path './old/*' -not -path './tmp/*' -exec gofmt -w {} \;
|
||||
|
||||
docs: mgmt-documentation.pdf
|
||||
yamlfmt:
|
||||
find . -maxdepth 3 -type f -name '*.yaml' -not -path './old/*' -not -path './tmp/*' -not -path './omv.yaml' -exec ruby -e "require 'yaml'; x=YAML.load_file('{}').to_yaml.each_line.map(&:rstrip).join(10.chr)+10.chr; File.open('{}', 'w').write x" \;
|
||||
|
||||
mgmt-documentation.pdf: DOCUMENTATION.md
|
||||
pandoc DOCUMENTATION.md -o 'mgmt-documentation.pdf'
|
||||
format: gofmt yamlfmt
|
||||
|
||||
docs: $(PROGRAM)-documentation.pdf
|
||||
|
||||
$(PROGRAM)-documentation.pdf: DOCUMENTATION.md
|
||||
pandoc DOCUMENTATION.md -o '$(PROGRAM)-documentation.pdf'
|
||||
|
||||
#
|
||||
# build aliases
|
||||
@@ -116,21 +170,21 @@ upload: upload-sources upload-srpms upload-rpms
|
||||
$(RPM): $(SPEC) $(SOURCE)
|
||||
@echo Running rpmbuild -bb...
|
||||
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bb $(SPEC) && \
|
||||
mv rpmbuild/RPMS/$(ARCH)/mgmt-$(VERSION)-$(RELEASE).*.rpm $(RPM)
|
||||
mv rpmbuild/RPMS/$(ARCH)/$(PROGRAM)-$(VERSION)-$(RELEASE).*.rpm $(RPM)
|
||||
|
||||
$(SRPM): $(SPEC) $(SOURCE)
|
||||
@echo Running rpmbuild -bs...
|
||||
rpmbuild --define '_topdir $(shell pwd)/rpmbuild' -bs $(SPEC)
|
||||
# renaming is not needed because we aren't using the dist variable
|
||||
#mv rpmbuild/SRPMS/mgmt-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM)
|
||||
#mv rpmbuild/SRPMS/$(PROGRAM)-$(VERSION)-$(RELEASE).*.src.rpm $(SRPM)
|
||||
|
||||
#
|
||||
# spec
|
||||
#
|
||||
$(SPEC): rpmbuild/ mgmt.spec.in
|
||||
$(SPEC): rpmbuild/ spec.in
|
||||
@echo Running templater...
|
||||
#cat mgmt.spec.in > $(SPEC)
|
||||
sed -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < mgmt.spec.in > $(SPEC)
|
||||
#cat spec.in > $(SPEC)
|
||||
sed -e s/__PROGRAM__/$(PROGRAM)/ -e s/__VERSION__/$(VERSION)/ -e s/__RELEASE__/$(RELEASE)/ < spec.in > $(SPEC)
|
||||
# append a changelog to the .spec file
|
||||
git log --format="* %cd %aN <%aE>%n- (%h) %s%d%n" --date=local | sed -r 's/[0-9]+:[0-9]+:[0-9]+ //' >> $(SPEC)
|
||||
|
||||
@@ -140,7 +194,7 @@ $(SPEC): rpmbuild/ mgmt.spec.in
|
||||
$(SOURCE): rpmbuild/
|
||||
@echo Running git archive...
|
||||
# use HEAD if tag doesn't exist yet, so that development is easier...
|
||||
git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist. Using HEAD instead.' && git archive --prefix=mgmt-$(VERSION)/ -o $(SOURCE) HEAD)
|
||||
git archive --prefix=$(PROGRAM)-$(VERSION)/ -o $(SOURCE) $(VERSION) 2> /dev/null || (echo 'Warning: $(VERSION) does not exist. Using HEAD instead.' && git archive --prefix=$(PROGRAM)-$(VERSION)/ -o $(SOURCE) HEAD)
|
||||
# TODO: if git archive had a --submodules flag this would easier!
|
||||
@echo Running git archive submodules...
|
||||
# i thought i would need --ignore-zeros, but it doesn't seem necessary!
|
||||
@@ -149,14 +203,14 @@ $(SOURCE): rpmbuild/
|
||||
temp="$${temp#\'}"; \
|
||||
path=$$temp; \
|
||||
[ "$$path" = "" ] && continue; \
|
||||
(cd $$path && git archive --prefix=mgmt-$(VERSION)/$$path/ HEAD > $$p/rpmbuild/tmp.tar && tar --concatenate --file=$$p/$(SOURCE) $$p/rpmbuild/tmp.tar && rm $$p/rpmbuild/tmp.tar); \
|
||||
(cd $$path && git archive --prefix=$(PROGRAM)-$(VERSION)/$$path/ HEAD > $$p/rpmbuild/tmp.tar && tar --concatenate --file=$$p/$(SOURCE) $$p/rpmbuild/tmp.tar && rm $$p/rpmbuild/tmp.tar); \
|
||||
done
|
||||
|
||||
# TODO: ensure that each sub directory exists
|
||||
rpmbuild/:
|
||||
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
rpmbuild:
|
||||
mkdirs:
|
||||
mkdir -p rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS}
|
||||
|
||||
#
|
||||
|
||||
163
Puppet.md
Normal file
@@ -0,0 +1,163 @@
|
||||
#mgmt Puppet support
|
||||
|
||||
1. [Prerequisites](#prerequisites)
|
||||
* [Testing the Puppet side](#testing-the-puppet-side)
|
||||
2. [Writing a suitable manifest](#writing-a-suitable-manifest)
|
||||
* [Unsupported attributes](#unsupported-attributes)
|
||||
* [Unsupported resources](#unsupported-resources)
|
||||
* [Avoiding common warnings](#avoiding-common-warnings)
|
||||
3. [Configuring Puppet](#configuring-puppet)
|
||||
4. [Caveats](#caveats)
|
||||
|
||||
`mgmt` can use Puppet as its source for the configuration graph.
|
||||
This document goes into detail on how this works, and lists
|
||||
some pitfalls and limitations.
|
||||
|
||||
For basic instructions on how to use the Puppet support, see
|
||||
the [main documentation](DOCUMENTATION.md#puppet-support).
|
||||
|
||||
##Prerequisites
|
||||
|
||||
You need Puppet installed in your system. It is not important how you
|
||||
get it. On the most common Linux distributions, you can use packages
|
||||
from the OS maintainer, or upstream Puppet repositories. An alternative
|
||||
that will also work on OSX is the `puppet` Ruby gem. It also has the
|
||||
advantage that you can install any desired version in your home directory
|
||||
or any other location.
|
||||
|
||||
Any release of Puppet's 3.x and 4.x series should be suitable for use with
|
||||
`mgmt`. Most importantly, make sure to install the `ffrank-mgmtgraph` Puppet
|
||||
module (referred to below as "the translator module").
|
||||
|
||||
puppet module install ffrank-mgmtgraph
|
||||
|
||||
Please note that the module is not required on your Puppet master (if you
|
||||
use a master/agent setup). It's needed on the machine that runs `mgmt`.
|
||||
You can install the module on the master anyway, so that it gets distributed
|
||||
to your agents through Puppet's `pluginsync` mechanism.
|
||||
|
||||
###Testing the Puppet side
|
||||
|
||||
The following command should run successfully and print a YAML hash on your
|
||||
terminal:
|
||||
|
||||
```puppet
|
||||
puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": ensure => present }'
|
||||
```
|
||||
|
||||
You can use this CLI to test any manifests before handing them straight
|
||||
to `mgmt`.
|
||||
|
||||
##Writing a suitable manifest
|
||||
|
||||
###Unsupported attributes
|
||||
|
||||
`mgmt` inherited its resource module from Puppet, so by and large, it's quite
|
||||
possible to express `mgmt` graphs in terms of Puppet manifests. However,
|
||||
there isn't (and likely never will be) full feature parity between the
|
||||
respective resource types. In consequence, a manifest can have semantics that
|
||||
cannot be transferred to `mgmt`.
|
||||
|
||||
For example, at the time of writing this, the `file` type in `mgmt` had no
|
||||
notion of permissions (the file `mode`) yet. This lead to the following
|
||||
warning (among others that will be discussed below):
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/foo": mode => "0600" }'
|
||||
Warning: cannot translate: File[/tmp/foo] { mode => "600" } (attribute is ignored)
|
||||
|
||||
This is a heads-up for the user, because the resulting `mgmt` graph will
|
||||
in fact not pass this information to the `/tmp/foo` file resource, and
|
||||
`mgmt` will ignore this file's permissions. Including such attributes in
|
||||
manifests that are written expressly for `mgmt` is not sensible and should
|
||||
be avoided.
|
||||
|
||||
###Unsupported resources
|
||||
|
||||
Puppet has a fairly large number of
|
||||
[built-in types](https://docs.puppet.com/puppet/latest/reference/type.html),
|
||||
and countless more are available through
|
||||
[modules](https://forge.puppet.com/). It's unlikely that all of them will
|
||||
eventually receive native counterparts in `mgmt`.
|
||||
|
||||
When encountering an unknown resource, the translator module will replace
|
||||
it with an `exec` resource in its output. This resource will run the equivalent
|
||||
of a `puppet resource` command to make Puppet apply the original resource
|
||||
itself. This has quite abysmal performance, because processing such a
|
||||
resource requires the forking of at least one Puppet process (two if it
|
||||
is found to be out of sync). This comes with considerable overhead.
|
||||
On most systems, starting up any Puppet command takes several seconds.
|
||||
Compared to the split second that the actual work usually takes,
|
||||
this overhead can amount to several orders of magnitude.
|
||||
|
||||
Avoid Puppet types that `mgmt` does not implement (yet).
|
||||
|
||||
###Avoiding common warnings
|
||||
|
||||
Many resource parameters in Puppet take default values. For the most part,
|
||||
the translator module just ignores them. However, there are cases in which
|
||||
Puppet will default to convenient behavior that `mgmt` cannot quite replicate.
|
||||
For example, translating a plain `file` resource will lead to a warning message:
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": }'
|
||||
Warning: File[/tmp/mgmt-test] uses the 'puppet' file bucket, which mgmt cannot do. There will be no backup copies!
|
||||
|
||||
The reason is that per default, Puppet assumes the following parameter value
|
||||
(among others)
|
||||
|
||||
```puppet
|
||||
file { "/tmp/mgmt-test":
|
||||
backup => 'puppet',
|
||||
}
|
||||
```
|
||||
|
||||
To avoid this, specify the parameter explicitly:
|
||||
|
||||
$ puppet mgmtgraph print --code 'file { "/tmp/mgmt-test": backup => false }'
|
||||
|
||||
This is tedious in a more complex manifest. A good simplification is the
|
||||
following [resource default](https://docs.puppet.com/puppet/latest/reference/lang_defaults.html)
|
||||
anywhere on the top scope of your manifest:
|
||||
|
||||
```puppet
|
||||
File { backup => false }
|
||||
```
|
||||
|
||||
If you encounter similar warnings from other types and/or parameters,
|
||||
use the same approach to silence them if possible.
|
||||
|
||||
##Configuring Puppet
|
||||
|
||||
Since `mgmt` uses an actual Puppet CLI behind the scenes, you might
|
||||
need to tweak some of Puppet's runtime options in order to make it
|
||||
do what you want. Reasons for this could be among the following:
|
||||
|
||||
* You use the `--puppet agent` variant and need to configure
|
||||
`servername`, `certname` and other master/agent-related options.
|
||||
* You don't want runtime information to end up in the `vardir`
|
||||
that is used by your regular `puppet agent`.
|
||||
* You install specific Puppet modules for `mgmt` in a non-standard
|
||||
location.
|
||||
|
||||
`mgmt` exposes only one Puppet option in order to allow you to
|
||||
control all of them, through its `--puppet-conf` option. It allows
|
||||
you to specify which `puppet.conf` file should be used during
|
||||
translation.
|
||||
|
||||
mgmt run --puppet /opt/my-manifest.pp --puppet-conf /etc/mgmt/puppet.conf
|
||||
|
||||
Within this file, you can just specify any needed options in the
|
||||
`[main]` section:
|
||||
|
||||
[main]
|
||||
server=mgmt-master.example.net
|
||||
vardir=/var/lib/mgmt/puppet
|
||||
|
||||
##Caveats
|
||||
|
||||
Please see the [README](https://github.com/ffrank/puppet-mgmtgraph/blob/master/README.md)
|
||||
of the translator module for the current state of supported and unsupported
|
||||
language features.
|
||||
|
||||
You should probably make sure to always use the latest release of
|
||||
both `ffrank-mgmtgraph` and `ffrank-yamlresource` (the latter is
|
||||
getting pulled in as a dependency of the former).
|
||||
48
README.md
@@ -1,24 +1,36 @@
|
||||
# *mgmt*: This is: mgmt!
|
||||
# *mgmt*: next generation config management!
|
||||
|
||||
[](art/)
|
||||
|
||||
[](https://goreportcard.com/report/github.com/purpleidea/mgmt)
|
||||
[](http://travis-ci.org/purpleidea/mgmt)
|
||||
[](DOCUMENTATION.md)
|
||||
[](https://godoc.org/github.com/purpleidea/mgmt)
|
||||
[](https://webchat.freenode.net/?channels=#mgmtconfig)
|
||||
[](https://ci.centos.org/job/purpleidea-mgmt/)
|
||||
[](https://copr.fedoraproject.org/coprs/purpleidea/mgmt/)
|
||||
[](https://aur.archlinux.org/packages/mgmt/)
|
||||
|
||||
## Community:
|
||||
Come join us on IRC in [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) on Freenode!
|
||||
You may like the [#mgmtconfig](https://twitter.com/hashtag/mgmtconfig) hashtag if you're on [Twitter](https://twitter.com/#!/purpleidea).
|
||||
|
||||
## Status:
|
||||
Mgmt is a fairly new project.
|
||||
We're working towards being minimally useful for production environments.
|
||||
We aren't feature complete for what we'd consider a 1.x release yet.
|
||||
With your help you'll be able to influence our design and get us there sooner!
|
||||
|
||||
## Questions:
|
||||
Please join the [#mgmtconfig](https://webchat.freenode.net/?channels=#mgmtconfig) IRC community!
|
||||
If you have a well phrased question that might benefit others, consider asking it by sending a patch to the documentation [FAQ](https://github.com/purpleidea/mgmt/blob/master/DOCUMENTATION.md#usage-and-frequently-asked-questions) section. I'll merge your question, and a patch with the answer!
|
||||
|
||||
## Quick start:
|
||||
* Either get the golang dependencies on your own, or run `make deps` if you're comfortable with how we install them.
|
||||
* Run `make build` to get a fresh built `mgmt` binary.
|
||||
* Run `cd $(mktemp --tmpdir -d tmp.XXX) && etcd` to get etcd running. The `mgmt` software will do this automatically for you in the future.
|
||||
* Run `time ./mgmt run --file examples/graph0.yaml --converged-timeout=1` to try out a very simple example!
|
||||
* Make sure you have golang version 1.6 or greater installed.
|
||||
* Clone the repository recursively, eg: `git clone --recursive https://github.com/purpleidea/mgmt/`.
|
||||
* Get the remaining golang dependencies on your own, or run `make deps` if you're comfortable with how we install them.
|
||||
* Run `make build` to get a freshly built `mgmt` binary.
|
||||
* Run `time ./mgmt run --file examples/graph0.yaml --converged-timeout=5 --tmp-prefix` to try out a very simple example!
|
||||
* To run continuously in the default mode of operation, omit the `--converged-timeout` option.
|
||||
* Have fun hacking on our future technology!
|
||||
|
||||
@@ -31,21 +43,24 @@ Please see: [DOCUMENTATION.md](DOCUMENTATION.md) or [PDF](https://pdfdoc-purplei
|
||||
## Roadmap:
|
||||
Please see: [TODO.md](TODO.md) for a list of upcoming work and TODO items.
|
||||
Please get involved by working on one of these items or by suggesting something else!
|
||||
Feel free to grab one of the straightforward [#mgmtlove](https://github.com/purpleidea/mgmt/labels/mgmtlove) issues if you're a first time contributor to the project or if you're unsure about what to hack on!
|
||||
|
||||
## Bugs:
|
||||
Please set the `DEBUG` constant in [main.go](https://github.com/purpleidea/mgmt/blob/master/main.go) to `true`, and post the logs when you report the [issue](https://github.com/purpleidea/mgmt/issues).
|
||||
Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/master/test/shell) or [OMV](https://github.com/purpleidea/mgmt/tree/master/test/omv) reproducible test case.
|
||||
Feel free to read my article on [debugging golang programs](https://ttboj.wordpress.com/2016/02/15/debugging-golang-programs/).
|
||||
|
||||
## Dependencies:
|
||||
* golang 1.4 or higher (required, available in most distros)
|
||||
* golang 1.6 or higher (required, available in most distros)
|
||||
* golang libraries (required, available with `go get`)
|
||||
|
||||
go get github.com/coreos/etcd/client
|
||||
go get gopkg.in/yaml.v2
|
||||
go get gopkg.in/fsnotify.v1
|
||||
go get github.com/codegangsta/cli
|
||||
go get github.com/urfave/cli
|
||||
go get github.com/coreos/go-systemd/dbus
|
||||
go get github.com/coreos/go-systemd/util
|
||||
go get github.com/coreos/pkg/capnslog
|
||||
|
||||
* stringer (required for building), available as a package on some platforms, otherwise via `go get`
|
||||
|
||||
@@ -58,8 +73,23 @@ Bonus points if you provide a [shell](https://github.com/purpleidea/mgmt/tree/ma
|
||||
We'd love to have your patches! Please send them by email, or as a pull request.
|
||||
|
||||
## On the web:
|
||||
* Introductory blog post: [https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* Julian Dunn at Cfgmgmtcamp 2016 [https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1)
|
||||
* James Shubin; blog: [Next generation configuration mgmt](https://ttboj.wordpress.com/2016/01/18/next-generation-configuration-mgmt/)
|
||||
* James Shubin; video: [Introductory recording from DevConf.cz 2016](https://www.youtube.com/watch?v=GVhpPF0j-iE&html5=1)
|
||||
* James Shubin; video: [Introductory recording from CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=fNeooSiIRnA&html5=1)
|
||||
* Julian Dunn; video: [On mgmt at CfgMgmtCamp.eu 2016](https://www.youtube.com/watch?v=kfF9IATUask&t=1949&html5=1)
|
||||
* Walter Heck; slides: [On mgmt at CfgMgmtCamp.eu 2016](http://www.slideshare.net/olindata/configuration-management-time-for-a-4th-generation/3)
|
||||
* Marco Marongiu; blog: [On mgmt](http://syslog.me/2016/02/15/leap-or-die/)
|
||||
* Felix Frank; blog: [From Catalog To Mgmt (on puppet to mgmt "transpiling")](https://ffrank.github.io/features/2016/02/18/from-catalog-to-mgmt/)
|
||||
* James Shubin; blog: [Automatic edges in mgmt (...and the pkg resource)](https://ttboj.wordpress.com/2016/03/14/automatic-edges-in-mgmt/)
|
||||
* James Shubin; blog: [Automatic grouping in mgmt](https://ttboj.wordpress.com/2016/03/30/automatic-grouping-in-mgmt/)
|
||||
* John Arundel; tweet: [“Puppet’s days are numbered.”](https://twitter.com/bitfield/status/732157519142002688)
|
||||
* Felix Frank; blog: [Puppet, Meet Mgmt (on puppet to mgmt internals)](https://ffrank.github.io/features/2016/06/12/puppet,-meet-mgmt/)
|
||||
* Felix Frank; blog: [Puppet Powered Mgmt (puppet to mgmt tl;dr)](https://ffrank.github.io/features/2016/06/19/puppet-powered-mgmt/)
|
||||
* James Shubin; blog: [Automatic clustering in mgmt](https://ttboj.wordpress.com/2016/06/20/automatic-clustering-in-mgmt/)
|
||||
* James Shubin; video: [Recording from CoreOSFest 2016](https://www.youtube.com/watch?v=KVmDCUA42wc&html5=1)
|
||||
* James Shubin; video: [Recording from DebConf16](http://meetings-archive.debian.net/pub/debian-meetings/2016/debconf16/Next_Generation_Config_Mgmt.webm) ([Slides](https://annex.debconf.org//debconf-share/debconf16/slides/15-next-generation-config-mgmt.pdf))
|
||||
* Felix Frank; blog: [Edging It All In (puppet and mgmt edges)](https://ffrank.github.io/features/2016/07/12/edging-it-all-in/)
|
||||
* Felix Frank; blog: [Translating All The Things (puppet to mgmt translation warnings)](https://ffrank.github.io/features/2016/08/19/translating-all-the-things/)
|
||||
|
||||
##
|
||||
|
||||
|
||||
6
THANKS
@@ -9,10 +9,16 @@ Chris Wright - For encouraging me to continue work on my prototype.
|
||||
|
||||
Daniel Riek - For supporting and sheltering this project from bureaucracy.
|
||||
|
||||
Diego Ongaro - For good chats, particularly around distributed systems.
|
||||
|
||||
Felix Frank - For taking a difficult problem and building an inspiring solution.
|
||||
|
||||
Ira Cooper - For having an algorithmic design discussion with me.
|
||||
|
||||
Jeff Darcy - For some algorithm recommendations, and NACKing my TopoSort idea!
|
||||
|
||||
Red Hat, inc. - For paying my salary, thus financially supporting my hacking.
|
||||
|
||||
Samuel Gélineau - For help with programming language theory and design.
|
||||
|
||||
And many others...
|
||||
|
||||
48
TODO.md
@@ -3,27 +3,54 @@ If you're looking for something to do, look here!
|
||||
Let us know if you're working on one of the items.
|
||||
|
||||
## Package resource
|
||||
- [ ] base type [bug](https://github.com/purpleidea/mgmt/issues/11)
|
||||
- [ ] getfiles support on debian [bug](https://github.com/hughsie/PackageKit/issues/118)
|
||||
- [ ] directory info on fedora [bug](https://github.com/hughsie/PackageKit/issues/117)
|
||||
- [ ] dnf blocker [bug](https://github.com/hughsie/PackageKit/issues/110)
|
||||
- [ ] install signal blocker [bug](https://github.com/hughsie/PackageKit/issues/109)
|
||||
|
||||
## File resource
|
||||
- [ ] ability to make/delete folders
|
||||
- [ ] recursive argument (can recursively watch/modify contents)
|
||||
- [ ] force argument (can cause switch from file <-> folder)
|
||||
## File resource [bug](https://github.com/purpleidea/mgmt/issues/13) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] chown/chmod support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] user/group support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] recurse limit support [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] fanotify support [bug](https://github.com/go-fsnotify/fsnotify/issues/114)
|
||||
|
||||
## Svc resource
|
||||
- [ ] base resource improvements
|
||||
|
||||
## Exec resource
|
||||
- [ ] base resource improvements
|
||||
|
||||
## Timer resource
|
||||
- [ ] base resource
|
||||
- [ ] reset on recompile
|
||||
- [ ] increment algorithm (linear, exponential, etc...)
|
||||
- [ ] increment algorithm (linear, exponential, etc...) [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## User/Group resource
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
- [ ] automatic edges to file resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Virt (libvirt) resource
|
||||
- [ ] base resource [bug](https://github.com/purpleidea/mgmt/issues/25)
|
||||
|
||||
## Net (systemd-networkd) resource
|
||||
- [ ] base resource
|
||||
|
||||
## Nspawn (systemd-nspawn) resource
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Mount (systemd-mount) resource
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Cron (systemd-timer) resource
|
||||
- [ ] base resource [:heart:](https://github.com/purpleidea/mgmt/labels/mgmtlove)
|
||||
|
||||
## Http resource
|
||||
- [ ] base resource
|
||||
|
||||
## Etcd improvements
|
||||
- [ ] embedded etcd master
|
||||
- [ ] capnslog fixes [bug](https://github.com/coreos/etcd/issues/4115)
|
||||
- [ ] fix embedded etcd master race
|
||||
|
||||
## Torrent/dht file transfer
|
||||
- [ ] base plumbing
|
||||
|
||||
## Language improvements
|
||||
- [ ] language design
|
||||
@@ -35,9 +62,6 @@ Let us know if you're working on one of the items.
|
||||
|
||||
## Other
|
||||
- [ ] better error/retry handling
|
||||
- [ ] resource grouping
|
||||
- [ ] automatic dependency adding (eg: packagekit file dependencies)
|
||||
- [ ] rpm package target in Makefile
|
||||
- [ ] deb package target in Makefile
|
||||
- [ ] reproducible builds
|
||||
- [ ] add your suggestions!
|
||||
|
||||
2
art/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*.png
|
||||
misc/
|
||||
BIN
art/mgmt.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
94
art/mgmt_logo_default_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st9" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st9" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st9" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st9" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st10" x1="97.3" y1="36.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="94.6,39.4 94.5,31.7 101.2,35.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st4" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st7" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st8" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st6" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_default_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 131.6" style="enable-background:new 0 0 168.3 131.6;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M4.7,105l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V124h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V124H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V124H0v-19H4.7z"/>
|
||||
<path class="st3" d="M26.4,113.9c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2L37,105h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V113.9z
|
||||
M31.4,115.2c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1V110c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V115.2z"/>
|
||||
<path class="st3" d="M50.1,105l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V124h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V124h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V124h-5v-19H50.1z"/>
|
||||
<path class="st3" d="M78.2,100.3v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2V105h2.2v-4.7H78.2z"/>
|
||||
<path class="st4" d="M90.6,122.6c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2
|
||||
v2.2c0,2.4,0.4,4.2,1.2,5.3S89,122.6,90.6,122.6z"/>
|
||||
<path class="st4" d="M100.5,113.6c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V113.6z M102.5,115.5c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8s1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8s-1.3,2.9-1.4,5.1V115.5z"/>
|
||||
<path class="st4" d="M121.1,105l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6V124h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2V124h-2v-19H121.1z"/>
|
||||
<path class="st4" d="M138.2,124v-17.3h-2.6V105h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7V124H138.2z"/>
|
||||
<path class="st4" d="M148,99.5c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1s-0.5,0.4-1,0.4
|
||||
s-0.7-0.1-0.9-0.4S148,99.9,148,99.5z M150.2,124h-2v-19h2V124z"/>
|
||||
<path class="st4" d="M155.3,113.6c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V113.6z M157.2,115.4
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V115.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st9" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st9" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st9" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st9" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st11" x1="115.6" y1="30.4" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st9" points="113.3,32.8 113.3,26.4 118.9,29.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st4" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st7" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st8" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st6" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_default_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st3" d="M96.7,25.7l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V32.6c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V32.6
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H96.7z"/>
|
||||
<path class="st3" d="M118.5,34.6c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V34.6z
|
||||
M123.5,35.9c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V35.9z"/>
|
||||
<path class="st3" d="M142.2,25.7l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V32.6c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V32.6
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H142.2z"/>
|
||||
<path class="st3" d="M170.3,21v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1l0,3.9
|
||||
c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V29.4h-2.2v-3.7h2.2V21H170.3z"/>
|
||||
<path class="st4" d="M182.7,43.2c1.4,0,2.4-0.4,3.1-1.1s1.1-1.8,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
S181,43.2,182.7,43.2z"/>
|
||||
<path class="st4" d="M192.6,34.2c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V34.2z M194.6,36.2c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V36.2z"/>
|
||||
<path class="st4" d="M213.2,25.7l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9V32.2
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H213.2z"/>
|
||||
<path class="st4" d="M230.3,44.7V27.4h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H230.3z"/>
|
||||
<path class="st4" d="M240.1,20.2c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,20.6,240.1,20.2z M242.3,44.7h-2v-19h2V44.7z"/>
|
||||
<path class="st4" d="M247.4,34.3c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V34.3z M249.3,36.1
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V36.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st9" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st9" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st9" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st9" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st9" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st12" x1="64.2" y1="24.2" x2="70.7" y2="33.9"/>
|
||||
<g>
|
||||
<polygon class="st9" points="62.5,26.3 62.2,21.1 66.8,23.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st4" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st7" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st8" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st6" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
94
art/mgmt_logo_reversed_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st14" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st14" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st14" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st14" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st13" x1="97.2" y1="36.3" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="94.4,39.3 94.3,31.5 101.1,35.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st4" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st7" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st8" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st6" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_reversed_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 133" style="enable-background:new 0 0 168.3 133;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M4.7,106.4l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8H0v-19H4.7z"/>
|
||||
<path class="st0" d="M26.4,115.3c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8c0.5-0.5,0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V115.3z
|
||||
M31.4,116.6c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V116.6z"/>
|
||||
<path class="st0" d="M50.1,106.4l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H50.1z"/>
|
||||
<path class="st0" d="M78.2,101.7v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2v-3.7h2.2v-4.7H78.2z"/>
|
||||
<path class="st4" d="M90.6,124c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2
|
||||
v2.2c0,2.4,0.4,4.2,1.2,5.3S89,124,90.6,124z"/>
|
||||
<path class="st4" d="M100.5,115c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V115z M102.5,116.9c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8s1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8s-1.3,2.9-1.4,5.1V116.9z"/>
|
||||
<path class="st4" d="M121.1,106.4l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H121.1z"/>
|
||||
<path class="st4" d="M138.2,125.4v-17.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3
|
||||
l-0.1,1.8c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H138.2z"/>
|
||||
<path class="st4" d="M148,100.9c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S148,101.3,148,100.9z M150.2,125.4h-2v-19h2V125.4z"/>
|
||||
<path class="st4" d="M155.3,115c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8V126c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V115z M157.2,116.8
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V116.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st14" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st14" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st14" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st14" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st16" x1="115.5" y1="30.5" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st14" points="113.2,32.9 113.1,26.5 118.8,29.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st4" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st7" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st8" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st6" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_reversed_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M96.7,27.6l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V34.4c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V34.5
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H96.7z"/>
|
||||
<path class="st0" d="M118.5,36.5c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V36.5z
|
||||
M123.5,37.7c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
c-0.5,0.8-0.7,2.1-0.7,3.9V37.7z"/>
|
||||
<path class="st0" d="M142.2,27.6l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5V34.4c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5V34.5
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H142.2z"/>
|
||||
<path class="st0" d="M170.3,22.9v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V31.3h-2.2v-3.7h2.2v-4.7H170.3z"/>
|
||||
<path class="st4" d="M182.7,45.1c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4V36c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7s-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
S181,45.1,182.7,45.1z"/>
|
||||
<path class="st4" d="M192.6,36.1c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V36.1z M194.6,38.1c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V38.1z"/>
|
||||
<path class="st4" d="M213.2,27.6l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9V34.1
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H213.2z"/>
|
||||
<path class="st4" d="M230.3,46.6V29.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H230.3z"/>
|
||||
<path class="st4" d="M240.1,22.1c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,22.5,240.1,22.1z M242.3,46.6h-2v-19h2V46.6z"/>
|
||||
<path class="st4" d="M247.4,36.2c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V36.2z M249.3,38
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V38z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st14" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st3" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st14" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st14" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st14" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st15" x1="64.8" y1="24.4" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st14" points="63,26.4 62.9,21.2 67.4,23.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st5" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st4" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st7" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st8" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st6" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
94
art/mgmt_logo_white_symbol.svg
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 120 107.1" style="enable-background:new 0 0 120 107.1;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="29.2" y1="24.1" x2="52.1" y2="42.8"/>
|
||||
<g>
|
||||
<polygon class="st0" points="27.7,27.8 24.9,20.5 32.6,21.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="16.1" cy="12.2" r="12.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="52.1" y1="42.1" x2="74.1" y2="80"/>
|
||||
<g>
|
||||
<polygon class="st0" points="70.1,80.9 76.9,84.8 76.8,77.1 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="69.4" y1="46.7" x2="95.8" y2="52.4"/>
|
||||
<g>
|
||||
<polygon class="st0" points="69.7,50.7 63.9,45.6 71.3,43.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="52.1" y1="42.8" x2="71.9" y2="30.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="73.1,34 76.6,27.1 68.9,27.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="16.8" y1="49.6" x2="34.8" y2="46.5"/>
|
||||
<g>
|
||||
<polygon class="st0" points="34.3,50.5 40.3,45.6 33,42.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="92.3" y1="79.5" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="96.1,80.6 89.3,84.3 89.5,76.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st17" x1="97.2" y1="36.6" x2="107.8" y2="54.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="94.4,39.5 94.3,31.8 101.1,35.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="52.1" cy="42.8" r="12.1"/>
|
||||
<circle class="st0" cx="12.2" cy="50.8" r="12.1"/>
|
||||
<circle class="st0" cx="87.5" cy="21.7" r="12.1"/>
|
||||
<circle class="st0" cx="83.5" cy="95" r="12.1"/>
|
||||
<circle class="st0" cx="107.8" cy="54.2" r="12.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.8 KiB |
132
art/mgmt_logo_white_tall.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 168.3 131.4" style="enable-background:new 0 0 168.3 131.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M4.7,104.8l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9H9v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8H0v-19H4.7z"/>
|
||||
<path class="st0" d="M26.4,113.8c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V113.8z M31.4,115
|
||||
c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1v-9.1c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V115z"/>
|
||||
<path class="st0" d="M50.1,104.8l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9v12.5h-5v-12.1c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6v12.9h-5v-12.1
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4v13.8h-5v-19H50.1z"/>
|
||||
<path class="st0" d="M78.2,100.2v4.7h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1
|
||||
l0,3.9c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5v-10.1h-2.2v-3.7h2.2v-4.7H78.2z"/>
|
||||
<path class="st0" d="M90.6,122.4c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.8,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6
|
||||
c-1.1,1.1-2.6,1.7-4.3,1.7c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2
|
||||
c1.9,0,3.4,0.6,4.5,1.8s1.7,2.9,1.7,5H95c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7
|
||||
c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3S89,122.4,90.6,122.4z"/>
|
||||
<path class="st0" d="M100.5,113.4c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4s1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3c-2.1,0-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V113.4z M102.5,115.3c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V115.3z"/>
|
||||
<path class="st0" d="M121.1,104.8l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6v12.7h-1.9v-12.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2v13.2h-2v-19H121.1z"/>
|
||||
<path class="st0" d="M138.2,123.8v-17.3h-2.6v-1.8h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3
|
||||
l-0.1,1.8c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3v2.3h3.7v1.8h-3.7v17.3H138.2z"/>
|
||||
<path class="st0" d="M148,99.3c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1s-0.5,0.4-1,0.4
|
||||
s-0.7-0.1-0.9-0.4S148,99.7,148,99.3z M150.2,123.8h-2v-19h2V123.8z"/>
|
||||
<path class="st0" d="M155.3,113.5c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
c-1.1,1.2-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V113.5z M157.2,115.2
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V115.2z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="58.9" y1="20.1" x2="77.9" y2="35.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="57.6,23.2 55.3,17.1 61.7,18.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="48" cy="10.1" r="10.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="77.9" y1="35.1" x2="96.2" y2="66.7"/>
|
||||
<g>
|
||||
<polygon class="st0" points="93,67.5 98.6,70.7 98.6,64.2 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="92.3" y1="38.9" x2="114.4" y2="43.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="92.6,42.3 87.8,38 93.9,36 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="77.9" y1="35.6" x2="94.5" y2="25.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="95.4,28.3 98.4,22.6 91.9,22.9 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="48.5" y1="41.3" x2="63.5" y2="38.7"/>
|
||||
<g>
|
||||
<polygon class="st0" points="63.1,42.1 68.1,38 62,35.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="111.4" y1="66.3" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="114.6,67.2 109,70.2 109.1,63.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st19" x1="115.4" y1="30.4" x2="124.4" y2="45.2"/>
|
||||
<g>
|
||||
<polygon class="st0" points="113.2,32.8 113,26.4 118.7,29.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="77.9" cy="35.6" r="10.1"/>
|
||||
<circle class="st0" cx="44.6" cy="42.4" r="10.1"/>
|
||||
<circle class="st0" cx="107.4" cy="18.1" r="10.1"/>
|
||||
<circle class="st0" cx="104.1" cy="79.1" r="10.1"/>
|
||||
<circle class="st0" cx="124.4" cy="45.2" r="10.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
132
art/mgmt_logo_white_wide.svg
Normal file
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 260.4 71.4" style="enable-background:new 0 0 260.4 71.4;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{fill:#E22434;}
|
||||
.st2{display:none;}
|
||||
.st3{fill:#1B3663;}
|
||||
.st4{fill:#00B1D1;}
|
||||
.st5{fill:#BFE6EF;}
|
||||
.st6{fill:#69CBE0;}
|
||||
.st7{fill:#0080BD;}
|
||||
.st8{fill:#005DAB;}
|
||||
.st9{fill:#183660;}
|
||||
.st10{fill:none;stroke:#183660;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st11{fill:none;stroke:#183660;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st12{fill:none;stroke:#183660;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st13{fill:none;stroke:#C0E6EF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st14{fill:#C0E6EF;}
|
||||
.st15{fill:none;stroke:#C0E6EF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st16{fill:none;stroke:#C0E6EF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
.st17{fill:none;stroke:#FFFFFF;stroke-width:1.9441;stroke-miterlimit:10;}
|
||||
.st18{fill:none;stroke:#FFFFFF;stroke-width:1.2961;stroke-miterlimit:10;}
|
||||
.st19{fill:none;stroke:#FFFFFF;stroke-width:1.6201;stroke-miterlimit:10;}
|
||||
</style>
|
||||
<g id="Layer_2">
|
||||
</g>
|
||||
<g id="Layer_1">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M96.7,26l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V45h-5V32.9c0-1.1-0.2-1.9-0.5-2.4s-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V45h-5V32.9
|
||||
c0-1.1-0.1-1.9-0.4-2.4s-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V45h-5V26H96.7z"/>
|
||||
<path class="st0" d="M118.5,34.9c0-3.1,0.6-5.4,1.7-7s2.7-2.3,4.7-2.3c1.7,0,3.1,0.7,4,2l0.2-1.7h4.5v19c0,2.4-0.7,4.3-2,5.6
|
||||
c-1.4,1.3-3.3,1.9-5.9,1.9c-1,0-2.1-0.2-3.3-0.6s-2-0.9-2.6-1.6l1.7-3.4c0.5,0.5,1.1,0.9,1.8,1.2c0.7,0.3,1.5,0.5,2.1,0.5
|
||||
c1.1,0,1.9-0.3,2.4-0.8s0.7-1.4,0.7-2.6v-1.6c-0.9,1.2-2.2,1.9-3.7,1.9c-2,0-3.6-0.8-4.7-2.4s-1.7-3.8-1.7-6.7V34.9z
|
||||
M123.5,36.2c0,1.8,0.2,3,0.7,3.8s1.2,1.2,2.2,1.2c1,0,1.8-0.4,2.3-1.1V31c-0.5-0.8-1.3-1.2-2.2-1.2c-1,0-1.7,0.4-2.2,1.2
|
||||
s-0.7,2.1-0.7,3.9V36.2z"/>
|
||||
<path class="st0" d="M142.2,26l0.1,1.8c1.1-1.4,2.6-2.1,4.4-2.1c1.9,0,3.2,0.9,4,2.6c1.1-1.7,2.6-2.6,4.7-2.6
|
||||
c3.3,0,5,2.3,5.1,6.9V45h-5V32.9c0-1.1-0.2-1.9-0.5-2.4c-0.3-0.5-0.8-0.7-1.5-0.7c-0.9,0-1.6,0.6-2.1,1.7l0,0.6V45h-5V32.9
|
||||
c0-1.1-0.1-1.9-0.4-2.4c-0.3-0.5-0.8-0.7-1.6-0.7c-0.9,0-1.5,0.5-2,1.4V45h-5V26H142.2z"/>
|
||||
<path class="st0" d="M170.3,21.3V26h2.5v3.7h-2.5v9.5c0,0.8,0.1,1.3,0.3,1.5c0.2,0.3,0.6,0.4,1.2,0.4c0.5,0,0.9,0,1.2-0.1l0,3.9
|
||||
c-0.8,0.3-1.8,0.5-2.7,0.5c-3.2,0-4.8-1.8-4.9-5.5V29.7h-2.2V26h2.2v-4.7H170.3z"/>
|
||||
<path class="st0" d="M182.7,43.5c1.4,0,2.4-0.4,3.1-1.1c0.7-0.8,1.1-1.9,1.2-3.3h1.9c-0.1,1.9-0.7,3.5-1.9,4.6s-2.6,1.7-4.3,1.7
|
||||
c-2.3,0-4-0.7-5.1-2.2s-1.7-3.6-1.8-6.4v-2.3c0-2.9,0.6-5.1,1.7-6.6s2.9-2.2,5.1-2.2c1.9,0,3.4,0.6,4.5,1.8s1.7,2.8,1.7,5H187
|
||||
c-0.1-1.6-0.5-2.8-1.2-3.6s-1.8-1.3-3.1-1.3c-1.7,0-2.9,0.6-3.7,1.7c-0.8,1.1-1.2,2.9-1.2,5.2v2.2c0,2.4,0.4,4.2,1.2,5.3
|
||||
C179.8,43,181,43.5,182.7,43.5z"/>
|
||||
<path class="st0" d="M192.6,34.5c0-2.7,0.6-4.9,1.9-6.5s2.9-2.4,5.1-2.4c2.2,0,3.9,0.8,5.1,2.4c1.2,1.6,1.9,3.7,1.9,6.5v2
|
||||
c0,2.8-0.6,5-1.9,6.5s-2.9,2.3-5.1,2.3s-3.8-0.8-5-2.3s-1.9-3.6-1.9-6.3V34.5z M194.6,36.5c0,2.2,0.4,3.9,1.3,5.2
|
||||
c0.9,1.3,2.1,1.9,3.7,1.9c1.6,0,2.8-0.6,3.7-1.8c0.8-1.2,1.3-2.9,1.3-5.2v-2c0-2.2-0.4-3.9-1.3-5.2c-0.9-1.3-2.1-1.9-3.7-1.9
|
||||
c-1.5,0-2.7,0.6-3.6,1.8c-0.9,1.2-1.3,2.9-1.4,5.1V36.5z"/>
|
||||
<path class="st0" d="M213.2,26l0.1,3c0.6-1,1.3-1.9,2.2-2.5s1.9-0.9,3-0.9c3.3,0,5,2.2,5.1,6.6V45h-1.9V32.5
|
||||
c0-1.7-0.3-3-0.9-3.8c-0.6-0.8-1.5-1.2-2.8-1.2c-1,0-1.9,0.4-2.8,1.2s-1.4,1.8-1.9,3.2V45h-2V26H213.2z"/>
|
||||
<path class="st0" d="M230.3,45V27.7h-2.6V26h2.6v-2.5c0-1.9,0.5-3.3,1.3-4.3s2-1.5,3.5-1.5c0.7,0,1.3,0.1,1.9,0.3l-0.1,1.8
|
||||
c-0.5-0.1-1-0.2-1.6-0.2c-0.9,0-1.7,0.4-2.2,1.1s-0.8,1.7-0.8,3V26h3.7v1.8h-3.7V45H230.3z"/>
|
||||
<path class="st0" d="M240.1,20.5c0-0.4,0.1-0.7,0.3-1s0.5-0.4,0.9-0.4s0.7,0.1,1,0.4s0.3,0.6,0.3,1s-0.1,0.7-0.3,1
|
||||
s-0.5,0.4-1,0.4s-0.7-0.1-0.9-0.4S240.1,20.9,240.1,20.5z M242.3,45h-2V26h2V45z"/>
|
||||
<path class="st0" d="M247.4,34.6c0-3,0.5-5.2,1.5-6.7s2.6-2.2,4.6-2.2c2.2,0,3.8,1,4.9,3l0.1-2.7h1.8v19.6c0,2.3-0.6,4-1.6,5.2
|
||||
s-2.7,1.8-4.7,1.8c-1,0-2.1-0.3-3.2-0.8s-1.9-1.1-2.4-1.9l0.9-1.4c1.3,1.5,2.8,2.2,4.5,2.2c1.6,0,2.7-0.4,3.5-1.3
|
||||
c0.7-0.8,1.1-2.1,1.1-3.8v-3c-1.1,1.8-2.7,2.7-4.9,2.7c-2,0-3.5-0.7-4.5-2.2s-1.6-3.6-1.6-6.5V34.6z M249.3,36.4
|
||||
c0,2.4,0.4,4.2,1.1,5.4s1.9,1.7,3.5,1.7c2.1,0,3.6-1,4.5-3v-9.7c-0.9-2.2-2.4-3.3-4.4-3.3c-1.6,0-2.8,0.6-3.5,1.7
|
||||
s-1.1,2.9-1.1,5.3V36.4z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="19.5" y1="16" x2="34.7" y2="28.5"/>
|
||||
<g>
|
||||
<polygon class="st0" points="18.4,18.5 16.6,13.7 21.7,14.5 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="10.8" cy="8.1" r="8.1"/>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="34.7" y1="28" x2="49.4" y2="53.3"/>
|
||||
<g>
|
||||
<polygon class="st0" points="46.8,54 51.2,56.5 51.2,51.4 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="46.2" y1="31.1" x2="63.9" y2="34.9"/>
|
||||
<g>
|
||||
<polygon class="st0" points="46.4,33.8 42.6,30.4 47.5,28.8 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="34.7" y1="28.5" x2="47.9" y2="20.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="48.7,22.7 51.1,18.1 45.9,18.3 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="11.2" y1="33.1" x2="23.2" y2="31"/>
|
||||
<g>
|
||||
<polygon class="st0" points="22.9,33.7 26.8,30.4 22,28.6 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="61.5" y1="53" x2="71.9" y2="36.1"/>
|
||||
<g>
|
||||
<polygon class="st0" points="64.1,53.7 59.5,56.2 59.7,51 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<line class="st18" x1="64.7" y1="24.4" x2="71.9" y2="36.6"/>
|
||||
<g>
|
||||
<polygon class="st0" points="62.9,26.4 62.9,21.2 67.3,23.7 "/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<circle class="st0" cx="34.7" cy="28.5" r="8.1"/>
|
||||
<circle class="st0" cx="8.1" cy="33.9" r="8.1"/>
|
||||
<circle class="st0" cx="58.3" cy="14.5" r="8.1"/>
|
||||
<circle class="st0" cx="55.7" cy="63.3" r="8.1"/>
|
||||
<circle class="st0" cx="71.9" cy="36.1" r="8.1"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
229
config.go
@@ -1,229 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gopkg.in/yaml.v2"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type collectorTypeConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Pattern string `yaml:"pattern"` // XXX: Not Implemented
|
||||
}
|
||||
|
||||
type vertexConfig struct {
|
||||
Type string `yaml:"type"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type edgeConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
From vertexConfig `yaml:"from"`
|
||||
To vertexConfig `yaml:"to"`
|
||||
}
|
||||
|
||||
type GraphConfig struct {
|
||||
Graph string `yaml:"graph"`
|
||||
Types struct {
|
||||
Noop []NoopType `yaml:"noop"`
|
||||
File []FileType `yaml:"file"`
|
||||
Service []ServiceType `yaml:"service"`
|
||||
Exec []ExecType `yaml:"exec"`
|
||||
} `yaml:"types"`
|
||||
Collector []collectorTypeConfig `yaml:"collect"`
|
||||
Edges []edgeConfig `yaml:"edges"`
|
||||
Comment string `yaml:"comment"`
|
||||
}
|
||||
|
||||
func (c *GraphConfig) Parse(data []byte) error {
|
||||
if err := yaml.Unmarshal(data, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Graph == "" {
|
||||
return errors.New("Graph config: invalid `graph`")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseConfigFromFile(filename string) *GraphConfig {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("Error: Config: ParseConfigFromFile: File: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var config GraphConfig
|
||||
if err := config.Parse(data); err != nil {
|
||||
log.Printf("Error: Config: ParseConfigFromFile: Parse: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
// XXX: we need to fix this function so that it either fails without modifying
|
||||
// the graph, passes successfully and modifies it, or basically panics i guess
|
||||
// this way an invalid compilation can leave the old graph running, and we we
|
||||
// don't modify a partial graph. so we really need to validate, and then perform
|
||||
// whatever actions are necessary
|
||||
// finding some way to do this on a copy of the graph, and then do a graph diff
|
||||
// and merge the new data into the old graph would be more appropriate, in
|
||||
// particular if we can ensure the graph merge can't fail. As for the putting
|
||||
// of stuff into etcd, we should probably store the operations to complete in
|
||||
// the new graph, and keep retrying until it succeeds, thus blocking any new
|
||||
// etcd operations until that time.
|
||||
func UpdateGraphFromConfig(config *GraphConfig, hostname string, g *Graph, etcdO *EtcdWObject) bool {
|
||||
|
||||
var NoopMap = make(map[string]*Vertex)
|
||||
var FileMap = make(map[string]*Vertex)
|
||||
var ServiceMap = make(map[string]*Vertex)
|
||||
var ExecMap = make(map[string]*Vertex)
|
||||
|
||||
var lookup = make(map[string]map[string]*Vertex)
|
||||
lookup["noop"] = NoopMap
|
||||
lookup["file"] = FileMap
|
||||
lookup["service"] = ServiceMap
|
||||
lookup["exec"] = ExecMap
|
||||
|
||||
//log.Printf("%+v", config) // debug
|
||||
|
||||
g.SetName(config.Graph) // set graph name
|
||||
|
||||
var keep []*Vertex // list of vertex which are the same in new graph
|
||||
|
||||
for _, t := range config.Types.Noop {
|
||||
obj := NewNoopType(t.Name)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
NoopMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
for _, t := range config.Types.File {
|
||||
// XXX: should we export based on a @@ prefix, or a metaparam
|
||||
// like exported => true || exported => (host pattern)||(other pattern?)
|
||||
if strings.HasPrefix(t.Name, "@@") { // exported resource
|
||||
// add to etcd storage...
|
||||
t.Name = t.Name[2:] //slice off @@
|
||||
if !etcdO.EtcdPut(hostname, t.Name, "file", t) {
|
||||
log.Printf("Problem exporting file resource %v.", t.Name)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
obj := NewFileType(t.Name, t.Path, t.Dirname, t.Basename, t.Content, t.State)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
FileMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
}
|
||||
|
||||
for _, t := range config.Types.Service {
|
||||
obj := NewServiceType(t.Name, t.State, t.Startup)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
ServiceMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
for _, t := range config.Types.Exec {
|
||||
obj := NewExecType(t.Name, t.Cmd, t.Shell, t.Timeout, t.WatchCmd, t.WatchShell, t.IfCmd, t.IfShell, t.PollInt, t.State)
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
ExecMap[obj.Name] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
}
|
||||
|
||||
// lookup from etcd graph
|
||||
// do all the graph look ups in one single step, so that if the etcd
|
||||
// database changes, we don't have a partial state of affairs...
|
||||
nodes, ok := etcdO.EtcdGet()
|
||||
if ok {
|
||||
for _, t := range config.Collector {
|
||||
// XXX: use t.Type and optionally t.Pattern to collect from etcd storage
|
||||
log.Printf("Collect: %v; Pattern: %v", t.Type, t.Pattern)
|
||||
|
||||
for _, x := range etcdO.EtcdGetProcess(nodes, "file") {
|
||||
var obj *FileType
|
||||
if B64ToObj(x, &obj) != true {
|
||||
log.Printf("Collect: File: %v not collected!", x)
|
||||
continue
|
||||
}
|
||||
if t.Pattern != "" { // XXX: currently the pattern for files can only override the Dirname variable :P
|
||||
obj.Dirname = t.Pattern
|
||||
}
|
||||
|
||||
log.Printf("Collect: File: %v collected!", obj.GetName())
|
||||
|
||||
// XXX: similar to file add code:
|
||||
v := g.GetVertexMatch(obj)
|
||||
if v == nil { // no match found
|
||||
obj.Init() // initialize go channels or things won't work!!!
|
||||
v = NewVertex(obj)
|
||||
g.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
FileMap[obj.GetName()] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of any vertices we shouldn't "keep" (that aren't in new graph)
|
||||
for _, v := range g.GetVertices() {
|
||||
if !HasVertex(v, keep) {
|
||||
// wait for exit before starting new graph!
|
||||
v.Type.SendEvent(eventExit, true, false)
|
||||
g.DeleteVertex(v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range config.Edges {
|
||||
if _, ok := lookup[e.From.Type]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.To.Type]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.From.Type][e.From.Name]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup[e.To.Type][e.To.Name]; !ok {
|
||||
return false
|
||||
}
|
||||
g.AddEdge(lookup[e.From.Type][e.From.Name], lookup[e.To.Type][e.To.Name], NewEdge(e.Name))
|
||||
}
|
||||
return true
|
||||
}
|
||||
155
configwatch.go
@@ -1,155 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"gopkg.in/fsnotify.v1"
|
||||
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
||||
"log"
|
||||
"math"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// XXX: it would be great if we could reuse code between this and the file type
|
||||
// XXX: patch this to submit it as part of go-fsnotify if they're interested...
|
||||
func ConfigWatch(file string) chan bool {
|
||||
ch := make(chan bool)
|
||||
go func() {
|
||||
var safename = path.Clean(file) // no trailing slash
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
patharray := PathSplit(safename) // tokenize the path
|
||||
var index = len(patharray) // starting index
|
||||
var current string // current "watcher" location
|
||||
var deltaDepth int // depth delta between watcher and event
|
||||
var send = false // send event?
|
||||
|
||||
for {
|
||||
current = strings.Join(patharray[0:index], "/")
|
||||
if current == "" { // the empty string top is the root dir ("/")
|
||||
current = "/"
|
||||
}
|
||||
log.Printf("Watching: %v", current) // attempting to watch...
|
||||
|
||||
// initialize in the loop so that we can reset on rm-ed handles
|
||||
err = watcher.Add(current)
|
||||
if err != nil {
|
||||
if err == syscall.ENOENT {
|
||||
index-- // usually not found, move up one dir
|
||||
} else if err == syscall.ENOSPC {
|
||||
// XXX: occasionally: no space left on device,
|
||||
// XXX: probably due to lack of inotify watches
|
||||
log.Printf("Lack of watches for config(%v) error: %+v", file, err.Error) // 0x408da0
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
log.Printf("Unknown config(%v) error:", file)
|
||||
log.Fatal(err)
|
||||
}
|
||||
index = int(math.Max(1, float64(index)))
|
||||
continue
|
||||
}
|
||||
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
// the deeper you go, the bigger the deltaDepth is...
|
||||
// this is the difference between what we're watching,
|
||||
// and the event... doesn't mean we can't watch deeper
|
||||
if current == event.Name {
|
||||
deltaDepth = 0 // i was watching what i was looking for
|
||||
|
||||
} else if HasPathPrefix(event.Name, current) {
|
||||
deltaDepth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
|
||||
|
||||
} else if HasPathPrefix(current, event.Name) {
|
||||
deltaDepth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
|
||||
|
||||
} else {
|
||||
// TODO different watchers get each others events!
|
||||
// https://github.com/go-fsnotify/fsnotify/issues/95
|
||||
// this happened with two values such as:
|
||||
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
||||
continue
|
||||
}
|
||||
//log.Printf("The delta depth is: %v", deltaDepth)
|
||||
|
||||
// if we have what we wanted, awesome, send an event...
|
||||
if event.Name == safename {
|
||||
//log.Println("Event!")
|
||||
send = true
|
||||
|
||||
// file removed, move the watch upwards
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
//log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
// we must be a parent watcher, so descend in
|
||||
if deltaDepth < 0 {
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if safename starts with event.Name, we're above, and no event should be sent
|
||||
} else if HasPathPrefix(safename, event.Name) {
|
||||
//log.Println("Above!")
|
||||
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
if deltaDepth < 0 {
|
||||
log.Println("Parent!")
|
||||
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
|
||||
//send = true
|
||||
}
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if event.Name startswith safename, send event, we're already deeper
|
||||
} else if HasPathPrefix(event.Name, safename) {
|
||||
//log.Println("Event2!")
|
||||
//send = true
|
||||
}
|
||||
|
||||
case err := <-watcher.Errors:
|
||||
log.Println("error:", err)
|
||||
log.Fatal(err)
|
||||
|
||||
}
|
||||
|
||||
// do our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
ch <- true
|
||||
}
|
||||
}
|
||||
//close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
382
converger/converger.go
Normal file
@@ -0,0 +1,382 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package converger is a facility for reporting the converged state.
|
||||
package converger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
// TODO: we could make a new function that masks out the state of certain
|
||||
// UUID's, but at the moment the new Timer code has obsoleted the need...
|
||||
|
||||
// Converger is the general interface for implementing a convergence watcher
|
||||
type Converger interface { // TODO: need a better name
|
||||
Register() ConvergerUUID
|
||||
IsConverged(ConvergerUUID) bool // is the UUID converged ?
|
||||
SetConverged(ConvergerUUID, bool) error // set the converged state of the UUID
|
||||
Unregister(ConvergerUUID)
|
||||
Start()
|
||||
Pause()
|
||||
Loop(bool)
|
||||
ConvergedTimer(ConvergerUUID) <-chan time.Time
|
||||
Status() map[uint64]bool
|
||||
Timeout() int // returns the timeout that this was created with
|
||||
SetStateFn(func(bool) error) // sets the stateFn
|
||||
}
|
||||
|
||||
// ConvergerUUID is the interface resources can use to notify with if converged
|
||||
// you'll need to use part of the Converger interface to Register initially too
|
||||
type ConvergerUUID interface {
|
||||
ID() uint64 // get Id
|
||||
Name() string // get a friendly name
|
||||
SetName(string)
|
||||
IsValid() bool // has Id been initialized ?
|
||||
InvalidateID() // set Id to nil
|
||||
IsConverged() bool
|
||||
SetConverged(bool) error
|
||||
Unregister()
|
||||
ConvergedTimer() <-chan time.Time
|
||||
StartTimer() (func() error, error) // cancellable is the same as StopTimer()
|
||||
ResetTimer() error // resets counter to zero
|
||||
StopTimer() error
|
||||
}
|
||||
|
||||
// converger is an implementation of the Converger interface
|
||||
type converger struct {
|
||||
timeout int // must be zero (instant) or greater seconds to run
|
||||
stateFn func(bool) error // run on converged state changes with state bool
|
||||
converged bool // did we converge (state changes of this run Fn)
|
||||
channel chan struct{} // signal here to run an isConverged check
|
||||
control chan bool // control channel for start/pause
|
||||
mutex sync.RWMutex // used for controlling access to status and lastid
|
||||
lastid uint64
|
||||
status map[uint64]bool
|
||||
}
|
||||
|
||||
// convergerUUID is an implementation of the ConvergerUUID interface
|
||||
type convergerUUID struct {
|
||||
converger Converger
|
||||
id uint64
|
||||
name string // user defined, friendly name
|
||||
mutex sync.Mutex
|
||||
timer chan struct{}
|
||||
running bool // is the above timer running?
|
||||
}
|
||||
|
||||
// NewConverger builds a new converger struct
|
||||
func NewConverger(timeout int, stateFn func(bool) error) *converger {
|
||||
return &converger{
|
||||
timeout: timeout,
|
||||
stateFn: stateFn,
|
||||
channel: make(chan struct{}),
|
||||
control: make(chan bool),
|
||||
lastid: 0,
|
||||
status: make(map[uint64]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Register assigns a ConvergerUUID to the caller
|
||||
func (obj *converger) Register() ConvergerUUID {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
obj.lastid++
|
||||
obj.status[obj.lastid] = false // initialize as not converged
|
||||
return &convergerUUID{
|
||||
converger: obj,
|
||||
id: obj.lastid,
|
||||
name: fmt.Sprintf("%d", obj.lastid), // some default
|
||||
timer: nil,
|
||||
running: false,
|
||||
}
|
||||
}
|
||||
|
||||
// IsConverged gets the converged status of a uuid
|
||||
func (obj *converger) IsConverged(uuid ConvergerUUID) bool {
|
||||
if !uuid.IsValid() {
|
||||
panic(fmt.Sprintf("Id of ConvergerUUID(%s) is nil!", uuid.Name()))
|
||||
}
|
||||
obj.mutex.RLock()
|
||||
isConverged, found := obj.status[uuid.ID()] // lookup
|
||||
obj.mutex.RUnlock()
|
||||
if !found {
|
||||
panic("Id of ConvergerUUID is unregistered!")
|
||||
}
|
||||
return isConverged
|
||||
}
|
||||
|
||||
// SetConverged updates the converger with the converged state of the UUID
|
||||
func (obj *converger) SetConverged(uuid ConvergerUUID, isConverged bool) error {
|
||||
if !uuid.IsValid() {
|
||||
return fmt.Errorf("Id of ConvergerUUID(%s) is nil!", uuid.Name())
|
||||
}
|
||||
obj.mutex.Lock()
|
||||
if _, found := obj.status[uuid.ID()]; !found {
|
||||
panic("Id of ConvergerUUID is unregistered!")
|
||||
}
|
||||
obj.status[uuid.ID()] = isConverged // set
|
||||
obj.mutex.Unlock() // unlock *before* poke or deadlock!
|
||||
if isConverged != obj.converged { // only poke if it would be helpful
|
||||
// run in a go routine so that we never block... just queue up!
|
||||
// this allows us to send events, even if we haven't started...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isConverged returns true if *every* registered uuid has converged
|
||||
func (obj *converger) isConverged() bool {
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for _, v := range obj.status {
|
||||
if !v { // everyone must be converged for this to be true
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Unregister dissociates the ConvergedUUID from the converged checking
|
||||
func (obj *converger) Unregister(uuid ConvergerUUID) {
|
||||
if !uuid.IsValid() {
|
||||
panic(fmt.Sprintf("Id of ConvergerUUID(%s) is nil!", uuid.Name()))
|
||||
}
|
||||
obj.mutex.Lock()
|
||||
uuid.StopTimer() // ignore any errors
|
||||
delete(obj.status, uuid.ID())
|
||||
obj.mutex.Unlock()
|
||||
uuid.InvalidateID()
|
||||
}
|
||||
|
||||
// Start causes a Converger object to start or resume running
|
||||
func (obj *converger) Start() {
|
||||
obj.control <- true
|
||||
}
|
||||
|
||||
// Pause causes a Converger object to stop running temporarily
|
||||
func (obj *converger) Pause() { // FIXME: add a sync ACK on pause before return
|
||||
obj.control <- false
|
||||
}
|
||||
|
||||
// Loop is the main loop for a Converger object; it usually runs in a goroutine
|
||||
// TODO: we could eventually have each resource tell us as soon as it converges
|
||||
// and then keep track of the time delays here, to avoid callers needing select
|
||||
// NOTE: when we have very short timeouts, if we start before all the resources
|
||||
// have joined the map, then it might appears as if we converged before we did!
|
||||
func (obj *converger) Loop(startPaused bool) {
|
||||
if obj.control == nil {
|
||||
panic("Converger not initialized correctly")
|
||||
}
|
||||
if startPaused { // start paused without racing
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("Converger expected true!")
|
||||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case e := <-obj.control: // expecting "false" which means pause!
|
||||
if e {
|
||||
panic("Converger expected false!")
|
||||
}
|
||||
// now i'm paused...
|
||||
select {
|
||||
case e := <-obj.control:
|
||||
if !e {
|
||||
panic("Converger expected true!")
|
||||
}
|
||||
// restart
|
||||
// kick once to refresh the check...
|
||||
go func() { obj.channel <- struct{}{} }()
|
||||
continue
|
||||
}
|
||||
|
||||
case <-obj.channel:
|
||||
if !obj.isConverged() {
|
||||
if obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(false); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = false
|
||||
continue
|
||||
}
|
||||
|
||||
// we have converged!
|
||||
if obj.timeout >= 0 { // only run if timeout is valid
|
||||
if !obj.converged { // we're doing a state change
|
||||
if obj.stateFn != nil {
|
||||
// call an arbitrary function
|
||||
if err := obj.stateFn(true); err != nil {
|
||||
// FIXME: what to do on error ?
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
obj.converged = true
|
||||
// loop and wait again...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ConvergedTimer adds a timeout to a select call and blocks until then
|
||||
// TODO: this means we could eventually have per resource converged timeouts
|
||||
func (obj *converger) ConvergedTimer(uuid ConvergerUUID) <-chan time.Time {
|
||||
// be clever: if i'm already converged, this timeout should block which
|
||||
// avoids unnecessary new signals being sent! this avoids fast loops if
|
||||
// we have a low timeout, or in particular a timeout == 0
|
||||
if uuid.IsConverged() {
|
||||
// blocks the case statement in select forever!
|
||||
return util.TimeAfterOrBlock(-1)
|
||||
}
|
||||
return util.TimeAfterOrBlock(obj.timeout)
|
||||
}
|
||||
|
||||
// Status returns a map of the converged status of each UUID.
|
||||
func (obj *converger) Status() map[uint64]bool {
|
||||
status := make(map[uint64]bool)
|
||||
obj.mutex.RLock() // take a read lock
|
||||
defer obj.mutex.RUnlock()
|
||||
for k, v := range obj.status { // make a copy to avoid the mutex
|
||||
status[k] = v
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
// Timeout returns the timeout in seconds that converger was created with. This
|
||||
// is useful to avoid passing in the timeout value separately when you're
|
||||
// already passing in the Converger struct.
|
||||
func (obj *converger) Timeout() int {
|
||||
return obj.timeout
|
||||
}
|
||||
|
||||
// SetStateFn sets the state function to be run on change of converged state.
|
||||
func (obj *converger) SetStateFn(stateFn func(bool) error) {
|
||||
obj.stateFn = stateFn
|
||||
}
|
||||
|
||||
// Id returns the unique id of this UUID object
|
||||
func (obj *convergerUUID) ID() uint64 {
|
||||
return obj.id
|
||||
}
|
||||
|
||||
// Name returns a user defined name for the specific convergerUUID.
|
||||
func (obj *convergerUUID) Name() string {
|
||||
return obj.name
|
||||
}
|
||||
|
||||
// SetName sets a user defined name for the specific convergerUUID.
|
||||
func (obj *convergerUUID) SetName(name string) {
|
||||
obj.name = name
|
||||
}
|
||||
|
||||
// IsValid tells us if the id is valid or has already been destroyed
|
||||
func (obj *convergerUUID) IsValid() bool {
|
||||
return obj.id != 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// InvalidateID marks the id as no longer valid
|
||||
func (obj *convergerUUID) InvalidateID() {
|
||||
obj.id = 0 // an id of 0 is invalid
|
||||
}
|
||||
|
||||
// IsConverged is a helper function to the regular IsConverged method
|
||||
func (obj *convergerUUID) IsConverged() bool {
|
||||
return obj.converger.IsConverged(obj)
|
||||
}
|
||||
|
||||
// SetConverged is a helper function to the regular SetConverged notification
|
||||
func (obj *convergerUUID) SetConverged(isConverged bool) error {
|
||||
return obj.converger.SetConverged(obj, isConverged)
|
||||
}
|
||||
|
||||
// Unregister is a helper function to unregister myself
|
||||
func (obj *convergerUUID) Unregister() {
|
||||
obj.converger.Unregister(obj)
|
||||
}
|
||||
|
||||
// ConvergedTimer is a helper around the regular ConvergedTimer method
|
||||
func (obj *convergerUUID) ConvergedTimer() <-chan time.Time {
|
||||
return obj.converger.ConvergedTimer(obj)
|
||||
}
|
||||
|
||||
// StartTimer runs an invisible timer that automatically converges on timeout.
|
||||
func (obj *convergerUUID) StartTimer() (func() error, error) {
|
||||
obj.mutex.Lock()
|
||||
if !obj.running {
|
||||
obj.timer = make(chan struct{})
|
||||
obj.running = true
|
||||
} else {
|
||||
obj.mutex.Unlock()
|
||||
return obj.StopTimer, fmt.Errorf("Timer already started!")
|
||||
}
|
||||
obj.mutex.Unlock()
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
}
|
||||
obj.SetConverged(false)
|
||||
|
||||
case <-obj.ConvergedTimer():
|
||||
obj.SetConverged(true) // converged!
|
||||
select {
|
||||
case _, ok := <-obj.timer: // reset signal channel
|
||||
if !ok { // channel is closed
|
||||
return // false to exit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return obj.StopTimer, nil
|
||||
}
|
||||
|
||||
// ResetTimer resets the counter to zero if using a StartTimer internally.
|
||||
func (obj *convergerUUID) ResetTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if obj.running {
|
||||
obj.timer <- struct{}{} // send the reset message
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("Timer hasn't been started!")
|
||||
}
|
||||
|
||||
// StopTimer stops the running timer permanently until a StartTimer is run.
|
||||
func (obj *convergerUUID) StopTimer() error {
|
||||
obj.mutex.Lock()
|
||||
defer obj.mutex.Unlock()
|
||||
if !obj.running {
|
||||
return fmt.Errorf("Timer isn't running!")
|
||||
}
|
||||
close(obj.timer)
|
||||
obj.running = false
|
||||
return nil
|
||||
}
|
||||
19
doc.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package main provides the main entrypoint for using the `mgmt` software.
|
||||
package main
|
||||
22
docker/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM golang:1.6.2
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2016-05-10
|
||||
|
||||
# Update the package list to be able to use required packages
|
||||
RUN apt-get update
|
||||
|
||||
# Change the working directory
|
||||
WORKDIR /go/src/mgmt
|
||||
|
||||
# Copy all the files to the working directory
|
||||
COPY . /go/src/mgmt
|
||||
|
||||
# Install dependencies
|
||||
RUN make deps
|
||||
|
||||
# Build the binary
|
||||
RUN make build
|
||||
31
docker/Dockerfile.development
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM golang:1.6.2
|
||||
|
||||
MAINTAINER Michał Czeraszkiewicz <contact@czerasz.com>
|
||||
|
||||
# Set the reset cache variable
|
||||
# Read more here: http://czerasz.com/2014/11/13/docker-tip-and-tricks/#use-refreshedat-variable-for-better-cache-control
|
||||
ENV REFRESHED_AT 2016-05-14
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# Setup User to match Host User
|
||||
# Give the nre user superuser permissions
|
||||
ARG USER_ID=1000
|
||||
ARG GROUP_ID=1000
|
||||
ARG USER_NAME=mgmt
|
||||
ARG GROUP_NAME=$USER_NAME
|
||||
RUN groupadd --gid $GROUP_ID $GROUP_NAME && \
|
||||
useradd --create-home --home /home/$USER_NAME --uid ${USER_ID} --gid $GROUP_NAME --groups sudo $USER_NAME && \
|
||||
echo "$USER_NAME ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
|
||||
|
||||
# Copy all the files to the working directory
|
||||
COPY . /home/$USER_NAME/mgmt
|
||||
|
||||
# Change working directory
|
||||
WORKDIR /home/$USER_NAME/mgmt
|
||||
|
||||
# Install dependencies
|
||||
RUN make deps
|
||||
|
||||
# Change user
|
||||
USER ${USER_NAME}
|
||||
26
docker/scripts/build
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
script_directory="$( cd "$( dirname "$0" )" && pwd )"
|
||||
project_directory=$script_directory/../..
|
||||
|
||||
# Specify the Docker image name
|
||||
image_name='purpleidea/mgmt'
|
||||
|
||||
# Build the image which contains the compiled binary
|
||||
docker build -t $image_name \
|
||||
--file=$project_directory/docker/Dockerfile $project_directory
|
||||
|
||||
# Remove the container if it already exists
|
||||
docker rm -f mgmt-export 2> /dev/null
|
||||
|
||||
# Start the container in background so we can "copy out" the binary
|
||||
docker run -d --name=mgmt-export $image_name bash -c 'while true; sleep 1000; done'
|
||||
|
||||
# Remove the current binary
|
||||
rm $project_directory/mgmt 2> /dev/null
|
||||
|
||||
# Get the binary from the container
|
||||
docker cp mgmt-export:/go/src/mgmt/mgmt $project_directory/mgmt
|
||||
|
||||
# Remove the container
|
||||
docker rm -f mgmt-export 2> /dev/null
|
||||
14
docker/scripts/build-development
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Stop on any error
|
||||
set -e
|
||||
|
||||
script_directory="$( cd "$( dirname "$0" )" && pwd )"
|
||||
project_directory=$script_directory/../..
|
||||
|
||||
# Specify the Docker image name
|
||||
image_name='purpleidea/mgmt:development'
|
||||
|
||||
# Build the image
|
||||
docker build -t $image_name \
|
||||
--file=$project_directory/docker/Dockerfile.development $project_directory
|
||||
15
docker/scripts/run-development
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Stop on any error
|
||||
set -e
|
||||
|
||||
script_directory="$( cd "$( dirname "$0" )" && pwd )"
|
||||
project_directory=$script_directory/../..
|
||||
|
||||
# Specify the Docker image name
|
||||
image_name='purpleidea/mgmt:development'
|
||||
|
||||
# Run container in development mode
|
||||
docker run --rm --name=mgm_development --user=mgmt \
|
||||
-v $project_directory:/home/mgmt/mgmt \
|
||||
-it $image_name bash
|
||||
300
etcd.go
@@ -1,300 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
etcd "github.com/coreos/etcd/client"
|
||||
"log"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=etcdMsg -output=etcdmsg_stringer.go
|
||||
type etcdMsg int
|
||||
|
||||
const (
|
||||
etcdStart etcdMsg = iota
|
||||
etcdEvent
|
||||
etcdFoo
|
||||
etcdBar
|
||||
)
|
||||
|
||||
//go:generate stringer -type=etcdConvergedState -output=etcdconvergedstate_stringer.go
|
||||
type etcdConvergedState int
|
||||
|
||||
const (
|
||||
etcdConvergedNil etcdConvergedState = iota
|
||||
//etcdConverged
|
||||
etcdConvergedTimeout
|
||||
)
|
||||
|
||||
type EtcdWObject struct { // etcd wrapper object
|
||||
seed string
|
||||
ctimeout int
|
||||
converged chan bool
|
||||
kapi etcd.KeysAPI
|
||||
convergedState etcdConvergedState
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) GetConvergedState() etcdConvergedState {
|
||||
return etcdO.convergedState
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) SetConvergedState(state etcdConvergedState) {
|
||||
etcdO.convergedState = state
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) GetKAPI() etcd.KeysAPI {
|
||||
if etcdO.kapi != nil { // memoize
|
||||
return etcdO.kapi
|
||||
}
|
||||
|
||||
cfg := etcd.Config{
|
||||
Endpoints: []string{etcdO.seed},
|
||||
Transport: etcd.DefaultTransport,
|
||||
// set timeout per request to fail fast when the target endpoint is unavailable
|
||||
HeaderTimeoutPerRequest: time.Second,
|
||||
}
|
||||
|
||||
var c etcd.Client
|
||||
var err error
|
||||
|
||||
c, err = etcd.New(cfg)
|
||||
if err != nil {
|
||||
// XXX: not sure if this ever errors
|
||||
if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// XXX: not sure if this part ever matches
|
||||
// not running or disconnected
|
||||
if cerr == etcd.ErrClusterUnavailable {
|
||||
log.Fatal("XXX: etcd: ErrClusterUnavailable")
|
||||
} else {
|
||||
log.Fatal("XXX: etcd: Unknown")
|
||||
}
|
||||
}
|
||||
log.Fatal(err) // some unhandled error
|
||||
}
|
||||
etcdO.kapi = etcd.NewKeysAPI(c)
|
||||
return etcdO.kapi
|
||||
}
|
||||
|
||||
type EtcdChannelWatchResponse struct {
|
||||
resp *etcd.Response
|
||||
err error
|
||||
}
|
||||
|
||||
// wrap the etcd watcher.Next blocking function inside of a channel
|
||||
func (etcdO *EtcdWObject) EtcdChannelWatch(watcher etcd.Watcher, context etcd_context.Context) chan *EtcdChannelWatchResponse {
|
||||
ch := make(chan *EtcdChannelWatchResponse)
|
||||
go func() {
|
||||
for {
|
||||
resp, err := watcher.Next(context) // blocks here
|
||||
ch <- &EtcdChannelWatchResponse{resp, err}
|
||||
}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) EtcdWatch() chan etcdMsg {
|
||||
kapi := etcdO.GetKAPI()
|
||||
ctimeout := etcdO.ctimeout
|
||||
converged := etcdO.converged
|
||||
// XXX: i think we need this buffered so that when we're hanging on the
|
||||
// channel, which is inside the EtcdWatch main loop, we still want the
|
||||
// calls to Get/Set on etcd to succeed, so blocking them here would
|
||||
// kill the whole thing
|
||||
ch := make(chan etcdMsg, 1) // XXX: buffer of at least 1 is required
|
||||
go func(ch chan etcdMsg) {
|
||||
tmin := 500 // initial (min) delay in ms
|
||||
t := tmin // current time
|
||||
tmult := 2 // multiplier for exponential delay
|
||||
tmax := 16000 // max delay
|
||||
watcher := kapi.Watcher("/exported/", &etcd.WatcherOptions{Recursive: true})
|
||||
etcdch := etcdO.EtcdChannelWatch(watcher, etcd_context.Background())
|
||||
for {
|
||||
log.Printf("Etcd: Watching...")
|
||||
var resp *etcd.Response // = nil by default
|
||||
var err error
|
||||
select {
|
||||
case out := <-etcdch:
|
||||
etcdO.SetConvergedState(etcdConvergedNil)
|
||||
resp, err = out.resp, out.err
|
||||
|
||||
case _ = <-TimeAfterOrBlock(ctimeout):
|
||||
etcdO.SetConvergedState(etcdConvergedTimeout)
|
||||
converged <- true
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if err == etcd_context.Canceled {
|
||||
// ctx is canceled by another routine
|
||||
log.Fatal("Canceled")
|
||||
} else if err == etcd_context.DeadlineExceeded {
|
||||
// ctx is attached with a deadline and it exceeded
|
||||
log.Fatal("Deadline")
|
||||
} else if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// not running or disconnected
|
||||
// TODO: is there a better way to parse errors?
|
||||
for _, e := range cerr.Errors {
|
||||
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
|
||||
t = int(math.Min(float64(t*tmult), float64(tmax)))
|
||||
log.Printf("Etcd: Waiting %d ms for connection...", t)
|
||||
time.Sleep(time.Duration(t) * time.Millisecond) // sleep for t ms
|
||||
|
||||
} else if e.Error() == "unexpected EOF" {
|
||||
log.Printf("Etcd: Unexpected disconnect...")
|
||||
|
||||
} else if e.Error() == "EOF" {
|
||||
log.Printf("Etcd: Disconnected...")
|
||||
|
||||
} else if strings.HasPrefix(e.Error(), "unsupported protocol scheme") {
|
||||
// usually a bad peer endpoint value
|
||||
log.Fatal("Bad peer endpoint value?")
|
||||
|
||||
} else {
|
||||
log.Fatal("Woops: ", e.Error())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// bad cluster endpoints, which are not etcd servers
|
||||
log.Fatal("Woops: ", err)
|
||||
}
|
||||
} else {
|
||||
//log.Print(resp)
|
||||
//log.Printf("Watcher().Node.Value(%v): %+v", key, resp.Node.Value)
|
||||
// FIXME: we should actually reset when the server comes back, not here on msg!
|
||||
//XXX: can we fix this with one of these patterns?: https://blog.golang.org/go-concurrency-patterns-timing-out-and
|
||||
t = tmin // reset timer
|
||||
|
||||
// don't trigger event if nothing changed
|
||||
if n, p := resp.Node, resp.PrevNode; resp.Action == "set" && p != nil {
|
||||
if n.Key == p.Key && n.Value == p.Value {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: we get events on key/type/value changes for
|
||||
// each type directory... ignore the non final ones...
|
||||
// IOW, ignore everything except for the value or some
|
||||
// field which gets set last... this could be the max count field thing...
|
||||
|
||||
log.Printf("Etcd: Value: %v", resp.Node.Value) // event
|
||||
ch <- etcdEvent // event
|
||||
}
|
||||
|
||||
} // end for loop
|
||||
//close(ch)
|
||||
}(ch) // call go routine
|
||||
return ch
|
||||
}
|
||||
|
||||
// helper function to store our data in etcd
|
||||
func (etcdO *EtcdWObject) EtcdPut(hostname, key, typ string, obj interface{}) bool {
|
||||
kapi := etcdO.GetKAPI()
|
||||
output, ok := ObjToB64(obj)
|
||||
if !ok {
|
||||
log.Printf("Etcd: Could not encode %v key.", key)
|
||||
return false
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/exported/%s/types/%s/type", hostname, key)
|
||||
_, err := kapi.Set(etcd_context.Background(), path, typ, nil)
|
||||
// XXX validate...
|
||||
|
||||
path = fmt.Sprintf("/exported/%s/types/%s/value", hostname, key)
|
||||
resp, err := kapi.Set(etcd_context.Background(), path, output, nil)
|
||||
if err != nil {
|
||||
if cerr, ok := err.(*etcd.ClusterError); ok {
|
||||
// not running or disconnected
|
||||
for _, e := range cerr.Errors {
|
||||
if strings.HasSuffix(e.Error(), "getsockopt: connection refused") {
|
||||
}
|
||||
//if e == etcd.ErrClusterUnavailable
|
||||
}
|
||||
}
|
||||
log.Printf("Etcd: Could not store %v key.", key)
|
||||
return false
|
||||
}
|
||||
log.Print("Etcd: ", resp) // w00t... bonus
|
||||
return true
|
||||
}
|
||||
|
||||
// lookup /exported/ node hierarchy
|
||||
func (etcdO *EtcdWObject) EtcdGet() (etcd.Nodes, bool) {
|
||||
kapi := etcdO.GetKAPI()
|
||||
// key structure is /exported/<hostname>/types/...
|
||||
resp, err := kapi.Get(etcd_context.Background(), "/exported/", &etcd.GetOptions{Recursive: true})
|
||||
if err != nil {
|
||||
return nil, false // not found
|
||||
}
|
||||
return resp.Node.Nodes, true
|
||||
}
|
||||
|
||||
func (etcdO *EtcdWObject) EtcdGetProcess(nodes etcd.Nodes, typ string) []string {
|
||||
//path := fmt.Sprintf("/exported/%s/types/", h)
|
||||
top := "/exported/"
|
||||
log.Printf("Etcd: Get: %+v", nodes) // Get().Nodes.Nodes
|
||||
var output []string
|
||||
|
||||
for _, x := range nodes { // loop through hosts
|
||||
if !strings.HasPrefix(x.Key, top) {
|
||||
log.Fatal("Error!")
|
||||
}
|
||||
host := x.Key[len(top):]
|
||||
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", -1, host, x.Nodes)
|
||||
//log.Printf("Get().Nodes[%v]: %+v ==> %+v", i, x.Key, x.Nodes)
|
||||
types, ok := EtcdGetChildNodeByKey(x, "types")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, y := range types.Nodes { // loop through types
|
||||
//key := y.Key # UUID?
|
||||
//log.Printf("Get(%v): TYPE[%v]", host, y.Key)
|
||||
t, ok := EtcdGetChildNodeByKey(y, "type")
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if typ != "" && typ != t.Value {
|
||||
continue
|
||||
} // filter based on type
|
||||
|
||||
v, ok := EtcdGetChildNodeByKey(y, "value") // B64ToObj this
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
log.Printf("Etcd: Hostname: %v; Get: %v", host, t.Value)
|
||||
|
||||
output = append(output, v.Value)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
// TODO: wrap this somehow so it's a method of *etcd.Node
|
||||
// helper function that returns the node for a particular key under a node
|
||||
func EtcdGetChildNodeByKey(node *etcd.Node, key string) (*etcd.Node, bool) {
|
||||
for _, x := range node.Nodes {
|
||||
if x.Key == fmt.Sprintf("%s/%s", node.Key, key) {
|
||||
return x, true
|
||||
}
|
||||
}
|
||||
return nil, false // not found
|
||||
}
|
||||
2281
etcd/etcd.go
Normal file
55
event.go
@@ -1,55 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
//go:generate stringer -type=eventName -output=eventname_stringer.go
|
||||
type eventName int
|
||||
|
||||
const (
|
||||
eventExit eventName = iota
|
||||
eventStart
|
||||
eventPause
|
||||
eventPoke
|
||||
eventBackPoke
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Name eventName
|
||||
Resp chan bool // channel to send an ack response on, nil to skip
|
||||
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
|
||||
Msg string // some words for fun
|
||||
Activity bool // did something interesting happen?
|
||||
}
|
||||
|
||||
// send a single acknowledgement on the channel if one was requested
|
||||
func (event *Event) ACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp <- true // send ACK
|
||||
}
|
||||
}
|
||||
|
||||
func (event *Event) NACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp <- false // send NACK
|
||||
}
|
||||
}
|
||||
|
||||
// get the activity value
|
||||
func (event *Event) GetActivity() bool {
|
||||
return event.Activity
|
||||
}
|
||||
120
event/event.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package event provides some primitives that are used for message passing.
|
||||
package event
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=EventName -output=eventname_stringer.go
|
||||
|
||||
// EventName represents the type of event being passed.
|
||||
type EventName int
|
||||
|
||||
// The different event names are used in different contexts.
|
||||
const (
|
||||
EventNil EventName = iota
|
||||
EventExit
|
||||
EventStart
|
||||
EventPause
|
||||
EventPoke
|
||||
EventBackPoke
|
||||
)
|
||||
|
||||
// Resp is a channel to be used for boolean responses. A nil represents an ACK,
|
||||
// and a non-nil represents a NACK (false). This also lets us use custom errors.
|
||||
type Resp chan error
|
||||
|
||||
// Event is the main struct that stores event information and responses.
|
||||
type Event struct {
|
||||
Name EventName
|
||||
Resp Resp // channel to send an ack response on, nil to skip
|
||||
//Wg *sync.WaitGroup // receiver barrier to Wait() for everyone else on
|
||||
Msg string // some words for fun
|
||||
Activity bool // did something interesting happen?
|
||||
}
|
||||
|
||||
// ACK sends a single acknowledgement on the channel if one was requested.
|
||||
func (event *Event) ACK() {
|
||||
if event.Resp != nil { // if they've requested an ACK
|
||||
event.Resp.ACK()
|
||||
}
|
||||
}
|
||||
|
||||
// NACK sends a negative acknowledgement message on the channel if one was requested.
|
||||
func (event *Event) NACK() {
|
||||
if event.Resp != nil { // if they've requested a NACK
|
||||
event.Resp.NACK()
|
||||
}
|
||||
}
|
||||
|
||||
// ACKNACK sends a custom ACK or NACK message on the channel if one was requested.
|
||||
func (event *Event) ACKNACK(err error) {
|
||||
if event.Resp != nil { // if they've requested a NACK
|
||||
event.Resp.ACKNACK(err)
|
||||
}
|
||||
}
|
||||
|
||||
// NewResp is just a helper to return the right type of response channel.
|
||||
func NewResp() Resp {
|
||||
resp := make(chan error)
|
||||
return resp
|
||||
}
|
||||
|
||||
// ACK sends a true value to resp.
|
||||
func (resp Resp) ACK() {
|
||||
if resp != nil {
|
||||
resp <- nil
|
||||
}
|
||||
}
|
||||
|
||||
// NACK sends a false value to resp.
|
||||
func (resp Resp) NACK() {
|
||||
if resp != nil {
|
||||
resp <- fmt.Errorf("NACK")
|
||||
}
|
||||
}
|
||||
|
||||
// ACKNACK sends a custom ACK or NACK. The ACK value is always nil, the NACK can
|
||||
// be any non-nil error value.
|
||||
func (resp Resp) ACKNACK(err error) {
|
||||
if resp != nil {
|
||||
resp <- err
|
||||
}
|
||||
}
|
||||
|
||||
// Wait waits for any response from a Resp channel and returns it.
|
||||
func (resp Resp) Wait() error {
|
||||
return <-resp
|
||||
}
|
||||
|
||||
// ACKWait waits for a +ive Ack from a Resp channel.
|
||||
func (resp Resp) ACKWait() {
|
||||
for {
|
||||
// wait until true value
|
||||
if resp.Wait() == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetActivity returns the activity value.
|
||||
func (event *Event) GetActivity() bool {
|
||||
return event.Activity
|
||||
}
|
||||
19
examples/autoedges1.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/tmp/foo/bar/f1"
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
- name: file2
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/tmp/foo/"
|
||||
content: |
|
||||
i am f2
|
||||
state: exists
|
||||
edges: []
|
||||
24
examples/autoedges2.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/etc/drbd.conf"
|
||||
content: |
|
||||
# this is an mgmt test
|
||||
state: exists
|
||||
- name: file2
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/tmp/foo/"
|
||||
content: |
|
||||
i am f2
|
||||
state: exists
|
||||
pkg:
|
||||
- name: drbd-utils
|
||||
meta:
|
||||
autoedge: true
|
||||
state: installed
|
||||
edges: []
|
||||
29
examples/autoedges3.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: drbd-utils
|
||||
meta:
|
||||
autoedge: true
|
||||
state: installed
|
||||
file:
|
||||
- name: file1
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/etc/drbd.conf"
|
||||
content: |
|
||||
# this is an mgmt test
|
||||
state: exists
|
||||
- name: file2
|
||||
meta:
|
||||
autoedge: true
|
||||
path: "/etc/drbd.d/"
|
||||
content: |
|
||||
i am a directory
|
||||
state: exists
|
||||
svc:
|
||||
- name: drbd
|
||||
meta:
|
||||
autoedge: true
|
||||
state: stopped
|
||||
edges: []
|
||||
21
examples/autogroup1.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: drbd-utils
|
||||
meta:
|
||||
autogroup: false
|
||||
state: installed
|
||||
- name: powertop
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
- name: sl
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
- name: cowsay
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
edges: []
|
||||
17
examples/autogroup2.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: powertop
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
- name: sl
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
- name: cowsay
|
||||
meta:
|
||||
autogroup: true
|
||||
state: installed
|
||||
edges: []
|
||||
18
examples/etcd1a.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1a
|
||||
path: "/tmp/mgmtA/f1a"
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
- name: "@@file2a"
|
||||
path: "/tmp/mgmtA/f2a"
|
||||
content: |
|
||||
i am f2, exported from host A
|
||||
state: exists
|
||||
collect:
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtA/"
|
||||
edges: []
|
||||
18
examples/etcd1b.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1b
|
||||
path: "/tmp/mgmtB/f1b"
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
- name: "@@file2b"
|
||||
path: "/tmp/mgmtB/f2b"
|
||||
content: |
|
||||
i am f2, exported from host B
|
||||
state: exists
|
||||
collect:
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtB/"
|
||||
edges: []
|
||||
18
examples/etcd1c.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1c
|
||||
path: "/tmp/mgmtC/f1c"
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
- name: "@@file2c"
|
||||
path: "/tmp/mgmtC/f2c"
|
||||
content: |
|
||||
i am f2, exported from host C
|
||||
state: exists
|
||||
collect:
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtC/"
|
||||
edges: []
|
||||
18
examples/etcd1d.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
file:
|
||||
- name: file1d
|
||||
path: "/tmp/mgmtD/f1d"
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
- name: "@@file2d"
|
||||
path: "/tmp/mgmtD/f2d"
|
||||
content: |
|
||||
i am f2, exported from host D
|
||||
state: exists
|
||||
collect:
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtD/"
|
||||
edges: []
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
@@ -45,15 +45,15 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
- name: e2
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec3
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
@@ -25,8 +25,8 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
@@ -25,8 +25,8 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: echo hello from exec1
|
||||
@@ -25,8 +25,8 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: echo hello from exec1
|
||||
83
examples/exec2.yaml
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec2
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec3
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec4
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec5
|
||||
cmd: sleep 15s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
kind: exec
|
||||
name: exec2
|
||||
- name: e2
|
||||
from:
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
kind: exec
|
||||
name: exec3
|
||||
- name: e3
|
||||
from:
|
||||
kind: exec
|
||||
name: exec2
|
||||
to:
|
||||
kind: exec
|
||||
name: exec4
|
||||
- name: e4
|
||||
from:
|
||||
kind: exec
|
||||
name: exec3
|
||||
to:
|
||||
kind: exec
|
||||
name: exec4
|
||||
59
examples/exec3.yaml
Normal file
@@ -0,0 +1,59 @@
|
||||
---
|
||||
graph: parallel
|
||||
resources:
|
||||
exec:
|
||||
- name: pkg10
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: svc10
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec10
|
||||
cmd: sleep 10s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: pkg15
|
||||
cmd: sleep 15s
|
||||
shell: ''
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: exec
|
||||
name: pkg10
|
||||
to:
|
||||
kind: exec
|
||||
name: svc10
|
||||
- name: e2
|
||||
from:
|
||||
kind: exec
|
||||
name: svc10
|
||||
to:
|
||||
kind: exec
|
||||
name: exec10
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
file:
|
||||
@@ -27,15 +27,15 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: file
|
||||
kind: file
|
||||
name: file1
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file2
|
||||
- name: e2
|
||||
from:
|
||||
type: file
|
||||
kind: file
|
||||
name: file2
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file3
|
||||
13
examples/file2.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt/hello/"
|
||||
source: "/var/lib/mgmt/files/some_dir/"
|
||||
recurse: true
|
||||
force: true
|
||||
state: exists
|
||||
edges: []
|
||||
14
examples/file3.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: You can test Watch and CheckApply failures with chmod ugo-r and chmod ugo-w.
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt/f1"
|
||||
meta:
|
||||
retry: 3
|
||||
delay: 5000
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
edges: []
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: hello world example
|
||||
types:
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
file:
|
||||
@@ -13,8 +13,8 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: noop
|
||||
kind: noop
|
||||
name: noop1
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: simple exec fan in to fan out example to demonstrate optimization
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
@@ -86,43 +86,43 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
- name: e2
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
- name: e3
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec3
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
- name: e4
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec5
|
||||
- name: e5
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec6
|
||||
- name: e6
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec4
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec7
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt/f1"
|
||||
@@ -15,8 +15,8 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: file
|
||||
kind: file
|
||||
name: file1
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file2
|
||||
path: "/tmp/mgmt/f2"
|
||||
@@ -15,8 +15,8 @@ types:
|
||||
edges:
|
||||
- name: e2
|
||||
from:
|
||||
type: file
|
||||
kind: file
|
||||
name: file2
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file3
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1a
|
||||
path: "/tmp/mgmtA/f1a"
|
||||
@@ -23,6 +23,6 @@ types:
|
||||
i am f4, exported from host A
|
||||
state: exists
|
||||
collect:
|
||||
- type: file
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtA/"
|
||||
edges: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1b
|
||||
path: "/tmp/mgmtB/f1b"
|
||||
@@ -23,6 +23,6 @@ types:
|
||||
i am f4, exported from host B
|
||||
state: exists
|
||||
collect:
|
||||
- type: file
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtB/"
|
||||
edges: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1c
|
||||
path: "/tmp/mgmtC/f1c"
|
||||
@@ -23,6 +23,6 @@ types:
|
||||
i am f4, exported from host C
|
||||
state: exists
|
||||
collect:
|
||||
- type: file
|
||||
- kind: file
|
||||
pattern: "/tmp/mgmtC/"
|
||||
edges: []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt/f1"
|
||||
@@ -13,6 +13,6 @@ types:
|
||||
i am f3, exported from host A
|
||||
state: exists
|
||||
collect:
|
||||
- type: file
|
||||
- kind: file
|
||||
pattern: ''
|
||||
edges:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt/f1"
|
||||
@@ -8,6 +8,6 @@ types:
|
||||
i am f1
|
||||
state: exists
|
||||
collect:
|
||||
- type: file
|
||||
- kind: file
|
||||
pattern: ''
|
||||
edges:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
edges:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
exec:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: simple exec fan in example to demonstrate optimization
|
||||
types:
|
||||
resources:
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: sleep 10s
|
||||
@@ -56,22 +56,22 @@ types:
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec1
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec5
|
||||
- name: e2
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec2
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec5
|
||||
- name: e3
|
||||
from:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec3
|
||||
to:
|
||||
type: exec
|
||||
kind: exec
|
||||
name: exec5
|
||||
|
||||
19
examples/msg1.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: timer example
|
||||
resources:
|
||||
timer:
|
||||
- name: timer1
|
||||
interval: 30
|
||||
msg:
|
||||
- name: msg1
|
||||
body: mgmt logged this message
|
||||
journal: true
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: timer
|
||||
name: timer1
|
||||
to:
|
||||
kind: msg
|
||||
name: msg1
|
||||
24
examples/noop1.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: noop example
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
meta:
|
||||
noop: true
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt-hello-noop"
|
||||
content: |
|
||||
hello world from @purpleidea
|
||||
state: exists
|
||||
meta:
|
||||
noop: true
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: noop
|
||||
name: noop1
|
||||
to:
|
||||
kind: file
|
||||
name: file1
|
||||
7
examples/pkg1.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: powertop
|
||||
state: installed
|
||||
edges: []
|
||||
7
examples/pkg2.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
graph: mygraph
|
||||
resources:
|
||||
pkg:
|
||||
- name: powertop
|
||||
state: uninstalled
|
||||
edges: []
|
||||
23
examples/remote1.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: remote noop example
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
meta:
|
||||
noop: true
|
||||
file:
|
||||
- name: file1
|
||||
path: "/tmp/mgmt-remote-hello"
|
||||
content: |
|
||||
hello world from @purpleidea
|
||||
state: exists
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: noop
|
||||
name: noop1
|
||||
to:
|
||||
kind: file
|
||||
name: file1
|
||||
remote: "ssh://root:password@hostname:22"
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
graph: mygraph
|
||||
types:
|
||||
resources:
|
||||
noop:
|
||||
- name: noop1
|
||||
file:
|
||||
@@ -9,22 +9,22 @@ types:
|
||||
content: |
|
||||
i am f1
|
||||
state: exists
|
||||
service:
|
||||
svc:
|
||||
- name: purpleidea
|
||||
state: running
|
||||
startup: enabled
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
type: noop
|
||||
kind: noop
|
||||
name: noop1
|
||||
to:
|
||||
type: file
|
||||
kind: file
|
||||
name: file1
|
||||
- name: e2
|
||||
from:
|
||||
type: file
|
||||
kind: file
|
||||
name: file1
|
||||
to:
|
||||
type: service
|
||||
kind: svc
|
||||
name: purpleidea
|
||||
25
examples/timer1.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: timer example
|
||||
resources:
|
||||
timer:
|
||||
- name: timer1
|
||||
interval: 30
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: echo hello world
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: timer
|
||||
name: timer1
|
||||
to:
|
||||
kind: exec
|
||||
name: exec1
|
||||
43
examples/timer2.yaml
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
graph: mygraph
|
||||
comment: example of multiple timers
|
||||
resources:
|
||||
timer:
|
||||
- name: timer1
|
||||
interval: 30
|
||||
- name: timer2
|
||||
interval: 60
|
||||
exec:
|
||||
- name: exec1
|
||||
cmd: echo hello world 30
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
- name: exec2
|
||||
cmd: echo hello world 60
|
||||
timeout: 0
|
||||
watchcmd: ''
|
||||
watchshell: ''
|
||||
ifcmd: ''
|
||||
ifshell: ''
|
||||
pollint: 0
|
||||
state: present
|
||||
edges:
|
||||
- name: e1
|
||||
from:
|
||||
kind: timer
|
||||
name: timer1
|
||||
to:
|
||||
kind: exec
|
||||
name: exec1
|
||||
- name: e2
|
||||
from:
|
||||
kind: timer
|
||||
name: timer2
|
||||
to:
|
||||
kind: exec
|
||||
name: exec2
|
||||
411
file.go
@@ -1,411 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"gopkg.in/fsnotify.v1"
|
||||
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type FileType struct {
|
||||
BaseType `yaml:",inline"`
|
||||
Path string `yaml:"path"` // path variable (should default to name)
|
||||
Dirname string `yaml:"dirname"`
|
||||
Basename string `yaml:"basename"`
|
||||
Content string `yaml:"content"`
|
||||
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
|
||||
sha256sum string
|
||||
}
|
||||
|
||||
func NewFileType(name, path, dirname, basename, content, state string) *FileType {
|
||||
// FIXME if path = nil, path = name ...
|
||||
return &FileType{
|
||||
BaseType: BaseType{
|
||||
Name: name,
|
||||
events: make(chan Event),
|
||||
vertex: nil,
|
||||
},
|
||||
Path: path,
|
||||
Dirname: dirname,
|
||||
Basename: basename,
|
||||
Content: content,
|
||||
State: state,
|
||||
sha256sum: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *FileType) GetType() string {
|
||||
return "File"
|
||||
}
|
||||
|
||||
// validate if the params passed in are valid data
|
||||
func (obj *FileType) Validate() bool {
|
||||
if obj.Dirname != "" {
|
||||
// must end with /
|
||||
if obj.Dirname[len(obj.Dirname)-1:] != "/" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if obj.Basename != "" {
|
||||
// must not start with /
|
||||
if obj.Basename[0:1] == "/" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (obj *FileType) GetPath() string {
|
||||
d := Dirname(obj.Path)
|
||||
b := Basename(obj.Path)
|
||||
if !obj.Validate() || (obj.Dirname == "" && obj.Basename == "") {
|
||||
return obj.Path
|
||||
} else if obj.Dirname == "" {
|
||||
return d + obj.Basename
|
||||
} else if obj.Basename == "" {
|
||||
return obj.Dirname + b
|
||||
} else { // if obj.dirname != "" && obj.basename != "" {
|
||||
return obj.Dirname + obj.Basename
|
||||
}
|
||||
}
|
||||
|
||||
// File watcher for files and directories
|
||||
// Modify with caution, probably important to write some test cases first!
|
||||
// obj.GetPath(): file or directory
|
||||
func (obj *FileType) Watch() {
|
||||
if obj.IsWatching() {
|
||||
return
|
||||
}
|
||||
obj.SetWatching(true)
|
||||
defer obj.SetWatching(false)
|
||||
|
||||
//var recursive bool = false
|
||||
//var isdir = (obj.GetPath()[len(obj.GetPath())-1:] == "/") // dirs have trailing slashes
|
||||
//log.Printf("IsDirectory: %v", isdir)
|
||||
//vertex := obj.GetVertex() // stored with SetVertex
|
||||
var safename = path.Clean(obj.GetPath()) // no trailing slash
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
patharray := PathSplit(safename) // tokenize the path
|
||||
var index = len(patharray) // starting index
|
||||
var current string // current "watcher" location
|
||||
var deltaDepth int // depth delta between watcher and event
|
||||
var send = false // send event?
|
||||
var exit = false
|
||||
var dirty = false
|
||||
|
||||
for {
|
||||
current = strings.Join(patharray[0:index], "/")
|
||||
if current == "" { // the empty string top is the root dir ("/")
|
||||
current = "/"
|
||||
}
|
||||
if DEBUG {
|
||||
log.Printf("File[%v]: Watching: %v", obj.GetName(), current) // attempting to watch...
|
||||
}
|
||||
// initialize in the loop so that we can reset on rm-ed handles
|
||||
err = watcher.Add(current)
|
||||
if err != nil {
|
||||
if DEBUG {
|
||||
log.Printf("File[%v]: watcher.Add(%v): Error: %v", obj.GetName(), current, err)
|
||||
}
|
||||
if err == syscall.ENOENT {
|
||||
index-- // usually not found, move up one dir
|
||||
} else if err == syscall.ENOSPC {
|
||||
// XXX: occasionally: no space left on device,
|
||||
// XXX: probably due to lack of inotify watches
|
||||
log.Printf("Lack of watches for file[%v] error: %+v", obj.Name, err.Error) // 0x408da0
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
log.Printf("Unknown file[%v] error:", obj.Name)
|
||||
log.Fatal(err)
|
||||
}
|
||||
index = int(math.Max(1, float64(index)))
|
||||
continue
|
||||
}
|
||||
|
||||
obj.SetState(typeWatching) // reset
|
||||
select {
|
||||
case event := <-watcher.Events:
|
||||
if DEBUG {
|
||||
log.Printf("File[%v]: Watch(%v), Event(%v): %v", obj.GetName(), current, event.Name, event.Op)
|
||||
}
|
||||
obj.SetConvergedState(typeConvergedNil) // XXX: technically i can detect if the event is erroneous or not first
|
||||
// the deeper you go, the bigger the deltaDepth is...
|
||||
// this is the difference between what we're watching,
|
||||
// and the event... doesn't mean we can't watch deeper
|
||||
if current == event.Name {
|
||||
deltaDepth = 0 // i was watching what i was looking for
|
||||
|
||||
} else if HasPathPrefix(event.Name, current) {
|
||||
deltaDepth = len(PathSplit(current)) - len(PathSplit(event.Name)) // -1 or less
|
||||
|
||||
} else if HasPathPrefix(current, event.Name) {
|
||||
deltaDepth = len(PathSplit(event.Name)) - len(PathSplit(current)) // +1 or more
|
||||
|
||||
} else {
|
||||
// TODO different watchers get each others events!
|
||||
// https://github.com/go-fsnotify/fsnotify/issues/95
|
||||
// this happened with two values such as:
|
||||
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
||||
continue
|
||||
}
|
||||
//log.Printf("The delta depth is: %v", deltaDepth)
|
||||
|
||||
// if we have what we wanted, awesome, send an event...
|
||||
if event.Name == safename {
|
||||
//log.Println("Event!")
|
||||
// FIXME: should all these below cases trigger?
|
||||
send = true
|
||||
dirty = true
|
||||
|
||||
// file removed, move the watch upwards
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
//log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
// we must be a parent watcher, so descend in
|
||||
if deltaDepth < 0 {
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if safename starts with event.Name, we're above, and no event should be sent
|
||||
} else if HasPathPrefix(safename, event.Name) {
|
||||
//log.Println("Above!")
|
||||
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
log.Println("Removal!")
|
||||
watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
if deltaDepth < 0 {
|
||||
log.Println("Parent!")
|
||||
if PathPrefixDelta(safename, event.Name) == 1 { // we're the parent dir
|
||||
send = true
|
||||
dirty = true
|
||||
}
|
||||
watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if event.Name startswith safename, send event, we're already deeper
|
||||
} else if HasPathPrefix(event.Name, safename) {
|
||||
//log.Println("Event2!")
|
||||
send = true
|
||||
dirty = true
|
||||
}
|
||||
|
||||
case err := <-watcher.Errors:
|
||||
obj.SetConvergedState(typeConvergedNil) // XXX ?
|
||||
log.Println("error:", err)
|
||||
log.Fatal(err)
|
||||
//obj.events <- fmt.Sprintf("file: %v", "error") // XXX: how should we handle errors?
|
||||
|
||||
case event := <-obj.events:
|
||||
obj.SetConvergedState(typeConvergedNil)
|
||||
if exit, send = obj.ReadEvent(&event); exit {
|
||||
return // exit
|
||||
}
|
||||
//dirty = false // these events don't invalidate state
|
||||
|
||||
case _ = <-TimeAfterOrBlock(obj.ctimeout):
|
||||
obj.SetConvergedState(typeConvergedTimeout)
|
||||
obj.converged <- true
|
||||
continue
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
// only invalid state on certain types of events
|
||||
if dirty {
|
||||
dirty = false
|
||||
obj.isStateOK = false // something made state dirty
|
||||
}
|
||||
Process(obj) // XXX: rename this function
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *FileType) HashSHA256fromContent() string {
|
||||
if obj.sha256sum != "" { // return if already computed
|
||||
return obj.sha256sum
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(obj.Content))
|
||||
obj.sha256sum = hex.EncodeToString(hash.Sum(nil))
|
||||
return obj.sha256sum
|
||||
}
|
||||
|
||||
// FIXME: add the obj.CleanState() calls all over the true returns!
|
||||
func (obj *FileType) StateOK() bool {
|
||||
if obj.isStateOK { // cache the state
|
||||
return true
|
||||
}
|
||||
|
||||
if _, err := os.Stat(obj.GetPath()); os.IsNotExist(err) {
|
||||
// no such file or directory
|
||||
if obj.State == "absent" {
|
||||
return obj.CleanState() // missing file should be missing, phew :)
|
||||
} else {
|
||||
// state invalid, skip expensive checksums
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: add file mode check here...
|
||||
|
||||
if PathIsDir(obj.GetPath()) {
|
||||
return obj.StateOKDir()
|
||||
} else {
|
||||
return obj.StateOKFile()
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *FileType) StateOKFile() bool {
|
||||
if PathIsDir(obj.GetPath()) {
|
||||
log.Fatal("This should only be called on a File type.")
|
||||
}
|
||||
|
||||
// run a diff, and return true if needs changing
|
||||
|
||||
hash := sha256.New()
|
||||
|
||||
f, err := os.Open(obj.GetPath())
|
||||
if err != nil {
|
||||
//log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if _, err := io.Copy(hash, f); err != nil {
|
||||
//log.Fatal(err)
|
||||
return false
|
||||
}
|
||||
|
||||
sha256sum := hex.EncodeToString(hash.Sum(nil))
|
||||
//log.Printf("sha256sum: %v", sha256sum)
|
||||
|
||||
if obj.HashSHA256fromContent() == sha256sum {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (obj *FileType) StateOKDir() bool {
|
||||
if !PathIsDir(obj.GetPath()) {
|
||||
log.Fatal("This should only be called on a Dir type.")
|
||||
}
|
||||
|
||||
// XXX: not implemented
|
||||
log.Fatal("Not implemented!")
|
||||
return false
|
||||
}
|
||||
|
||||
func (obj *FileType) Apply() bool {
|
||||
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
|
||||
|
||||
if PathIsDir(obj.GetPath()) {
|
||||
return obj.ApplyDir()
|
||||
} else {
|
||||
return obj.ApplyFile()
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *FileType) ApplyFile() bool {
|
||||
|
||||
if PathIsDir(obj.GetPath()) {
|
||||
log.Fatal("This should only be called on a File type.")
|
||||
}
|
||||
|
||||
if obj.State == "absent" {
|
||||
log.Printf("About to remove: %v", obj.GetPath())
|
||||
err := os.Remove(obj.GetPath())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
//log.Println("writing: " + filename)
|
||||
f, err := os.Create(obj.GetPath())
|
||||
if err != nil {
|
||||
log.Println("error:", err)
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
_, err = io.WriteString(f, obj.Content)
|
||||
if err != nil {
|
||||
log.Println("error:", err)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (obj *FileType) ApplyDir() bool {
|
||||
if !PathIsDir(obj.GetPath()) {
|
||||
log.Fatal("This should only be called on a Dir type.")
|
||||
}
|
||||
|
||||
// XXX: not implemented
|
||||
log.Fatal("Not implemented!")
|
||||
return true
|
||||
}
|
||||
|
||||
func (obj *FileType) Compare(typ Type) bool {
|
||||
switch typ.(type) {
|
||||
case *FileType:
|
||||
typ := typ.(*FileType)
|
||||
if obj.Name != typ.Name {
|
||||
return false
|
||||
}
|
||||
if obj.GetPath() != typ.Path {
|
||||
return false
|
||||
}
|
||||
if obj.Content != typ.Content {
|
||||
return false
|
||||
}
|
||||
if obj.State != typ.State {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
269
gconfig/gconfig.go
Normal file
@@ -0,0 +1,269 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package gconfig provides the facilities for loading a graph from a yaml file.
|
||||
package gconfig
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/etcd"
|
||||
"github.com/purpleidea/mgmt/event"
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type collectorResConfig struct {
|
||||
Kind string `yaml:"kind"`
|
||||
Pattern string `yaml:"pattern"` // XXX: Not Implemented
|
||||
}
|
||||
|
||||
type vertexConfig struct {
|
||||
Kind string `yaml:"kind"`
|
||||
Name string `yaml:"name"`
|
||||
}
|
||||
|
||||
type edgeConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
From vertexConfig `yaml:"from"`
|
||||
To vertexConfig `yaml:"to"`
|
||||
}
|
||||
|
||||
// GraphConfig is the data structure that describes a single graph to run.
|
||||
type GraphConfig struct {
|
||||
Graph string `yaml:"graph"`
|
||||
Resources struct {
|
||||
Noop []*resources.NoopRes `yaml:"noop"`
|
||||
Pkg []*resources.PkgRes `yaml:"pkg"`
|
||||
File []*resources.FileRes `yaml:"file"`
|
||||
Svc []*resources.SvcRes `yaml:"svc"`
|
||||
Exec []*resources.ExecRes `yaml:"exec"`
|
||||
Timer []*resources.TimerRes `yaml:"timer"`
|
||||
Msg []*resources.MsgRes `yaml:"msg"`
|
||||
} `yaml:"resources"`
|
||||
Collector []collectorResConfig `yaml:"collect"`
|
||||
Edges []edgeConfig `yaml:"edges"`
|
||||
Comment string `yaml:"comment"`
|
||||
Hostname string `yaml:"hostname"` // uuid for the host
|
||||
Remote string `yaml:"remote"`
|
||||
}
|
||||
|
||||
// Parse parses a data stream into the graph structure.
|
||||
func (c *GraphConfig) Parse(data []byte) error {
|
||||
if err := yaml.Unmarshal(data, c); err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Graph == "" {
|
||||
return errors.New("Graph config: invalid `graph`")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseConfigFromFile takes a filename and returns the graph config structure.
|
||||
func ParseConfigFromFile(filename string) *GraphConfig {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
log.Printf("Config: Error: ParseConfigFromFile: File: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
var config GraphConfig
|
||||
if err := config.Parse(data); err != nil {
|
||||
log.Printf("Config: Error: ParseConfigFromFile: Parse: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
// NewGraphFromConfig returns a new graph from existing input, such as from the
|
||||
// existing graph, and a GraphConfig struct.
|
||||
func (c *GraphConfig) NewGraphFromConfig(g *pgraph.Graph, embdEtcd *etcd.EmbdEtcd, noop bool) (*pgraph.Graph, error) {
|
||||
if c.Hostname == "" {
|
||||
return nil, fmt.Errorf("Config: Error: Hostname can't be empty!")
|
||||
}
|
||||
|
||||
var graph *pgraph.Graph // new graph to return
|
||||
if g == nil { // FIXME: how can we check for an empty graph?
|
||||
graph = pgraph.NewGraph("Graph") // give graph a default name
|
||||
} else {
|
||||
graph = g.Copy() // same vertices, since they're pointers!
|
||||
}
|
||||
|
||||
var lookup = make(map[string]map[string]*pgraph.Vertex)
|
||||
|
||||
//log.Printf("%+v", config) // debug
|
||||
|
||||
// TODO: if defined (somehow)...
|
||||
graph.SetName(c.Graph) // set graph name
|
||||
|
||||
var keep []*pgraph.Vertex // list of vertex which are the same in new graph
|
||||
var resourceList []resources.Res // list of resources to export
|
||||
// use reflection to avoid duplicating code... better options welcome!
|
||||
value := reflect.Indirect(reflect.ValueOf(c.Resources))
|
||||
vtype := value.Type()
|
||||
for i := 0; i < vtype.NumField(); i++ { // number of fields in struct
|
||||
name := vtype.Field(i).Name // string of field name
|
||||
field := value.FieldByName(name)
|
||||
iface := field.Interface() // interface type of value
|
||||
slice := reflect.ValueOf(iface)
|
||||
// XXX: should we just drop these everywhere and have the kind strings be all lowercase?
|
||||
kind := util.FirstToUpper(name)
|
||||
if global.DEBUG {
|
||||
log.Printf("Config: Processing: %v...", kind)
|
||||
}
|
||||
for j := 0; j < slice.Len(); j++ { // loop through resources of same kind
|
||||
x := slice.Index(j).Interface()
|
||||
res, ok := x.(resources.Res) // convert to Res type
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Config: Error: Can't convert: %v of type: %T to Res.", x, x)
|
||||
}
|
||||
if noop {
|
||||
res.Meta().Noop = noop
|
||||
}
|
||||
if _, exists := lookup[kind]; !exists {
|
||||
lookup[kind] = make(map[string]*pgraph.Vertex)
|
||||
}
|
||||
// XXX: should we export based on a @@ prefix, or a metaparam
|
||||
// like exported => true || exported => (host pattern)||(other pattern?)
|
||||
if !strings.HasPrefix(res.GetName(), "@@") { // not exported resource
|
||||
// XXX: we don't have a way of knowing if any of the
|
||||
// metaparams are undefined, and as a result to set the
|
||||
// defaults that we want! I hate the go yaml parser!!!
|
||||
v := graph.GetVertexMatch(res)
|
||||
if v == nil { // no match found
|
||||
res.Init()
|
||||
v = pgraph.NewVertex(res)
|
||||
graph.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
lookup[kind][res.GetName()] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
|
||||
} else if !noop { // do not export any resources if noop
|
||||
// store for addition to etcd storage...
|
||||
res.SetName(res.GetName()[2:]) //slice off @@
|
||||
res.SetKind(kind) // cheap init
|
||||
resourceList = append(resourceList, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
// store in etcd
|
||||
if err := etcd.EtcdSetResources(embdEtcd, c.Hostname, resourceList); err != nil {
|
||||
return nil, fmt.Errorf("Config: Could not export resources: %v", err)
|
||||
}
|
||||
|
||||
// lookup from etcd
|
||||
var hostnameFilter []string // empty to get from everyone
|
||||
kindFilter := []string{}
|
||||
for _, t := range c.Collector {
|
||||
// XXX: should we just drop these everywhere and have the kind strings be all lowercase?
|
||||
kind := util.FirstToUpper(t.Kind)
|
||||
kindFilter = append(kindFilter, kind)
|
||||
}
|
||||
// do all the graph look ups in one single step, so that if the etcd
|
||||
// database changes, we don't have a partial state of affairs...
|
||||
if len(kindFilter) > 0 { // if kindFilter is empty, don't need to do lookups!
|
||||
var err error
|
||||
resourceList, err = etcd.EtcdGetResources(embdEtcd, 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 {
|
||||
// XXX: should we just drop these everywhere and have the kind strings be all lowercase?
|
||||
kind := util.FirstToUpper(t.Kind)
|
||||
// use t.Kind and optionally t.Pattern to collect from etcd storage
|
||||
log.Printf("Collect: %v; Pattern: %v", kind, t.Pattern)
|
||||
|
||||
// XXX: expand to more complex pattern matching here...
|
||||
if res.Kind() != kind {
|
||||
continue
|
||||
}
|
||||
|
||||
if matched {
|
||||
// we've already matched this resource, should we match again?
|
||||
log.Printf("Config: Warning: Matching %v[%v] again!", kind, res.GetName())
|
||||
}
|
||||
matched = true
|
||||
|
||||
// collect resources but add the noop metaparam
|
||||
if noop {
|
||||
res.Meta().Noop = noop
|
||||
}
|
||||
|
||||
if t.Pattern != "" { // XXX: simplistic for now
|
||||
res.CollectPattern(t.Pattern) // res.Dirname = t.Pattern
|
||||
}
|
||||
|
||||
log.Printf("Collect: %v[%v]: collected!", kind, res.GetName())
|
||||
|
||||
// XXX: similar to other resource add code:
|
||||
if _, exists := lookup[kind]; !exists {
|
||||
lookup[kind] = make(map[string]*pgraph.Vertex)
|
||||
}
|
||||
v := graph.GetVertexMatch(res)
|
||||
if v == nil { // no match found
|
||||
res.Init() // initialize go channels or things won't work!!!
|
||||
v = pgraph.NewVertex(res)
|
||||
graph.AddVertex(v) // call standalone in case not part of an edge
|
||||
}
|
||||
lookup[kind][res.GetName()] = v // used for constructing edges
|
||||
keep = append(keep, v) // append
|
||||
|
||||
//break // let's see if another resource even matches
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of any vertices we shouldn't "keep" (that aren't in new graph)
|
||||
for _, v := range graph.GetVertices() {
|
||||
if !pgraph.VertexContains(v, keep) {
|
||||
// wait for exit before starting new graph!
|
||||
v.SendEvent(event.EventExit, true, false)
|
||||
graph.DeleteVertex(v)
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range c.Edges {
|
||||
if _, ok := lookup[util.FirstToUpper(e.From.Kind)]; !ok {
|
||||
return nil, fmt.Errorf("Can't find 'from' resource!")
|
||||
}
|
||||
if _, ok := lookup[util.FirstToUpper(e.To.Kind)]; !ok {
|
||||
return nil, fmt.Errorf("Can't find 'to' resource!")
|
||||
}
|
||||
if _, ok := lookup[util.FirstToUpper(e.From.Kind)][e.From.Name]; !ok {
|
||||
return nil, fmt.Errorf("Can't find 'from' name!")
|
||||
}
|
||||
if _, ok := lookup[util.FirstToUpper(e.To.Kind)][e.To.Name]; !ok {
|
||||
return nil, fmt.Errorf("Can't find 'to' name!")
|
||||
}
|
||||
graph.AddEdge(lookup[util.FirstToUpper(e.From.Kind)][e.From.Name], lookup[util.FirstToUpper(e.To.Kind)][e.To.Name], pgraph.NewEdge(e.Name))
|
||||
}
|
||||
|
||||
return graph, nil
|
||||
}
|
||||
26
global/global.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package global holds some global variables that are used throughout the code.
|
||||
package global
|
||||
|
||||
// These constants are used throughout the program.
|
||||
const (
|
||||
DEBUG = false // add additional log messages
|
||||
TRACE = false // add execution flow log messages
|
||||
VERBOSE = false // add extra log message output
|
||||
)
|
||||
2
gopath/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
pkg/
|
||||
1
gopath/src
Symbolic link
@@ -0,0 +1 @@
|
||||
../vendor
|
||||
500
main.go
@@ -18,134 +18,313 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/codegangsta/cli"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
//etcd_context "github.com/coreos/etcd/Godeps/_workspace/src/golang.org/x/net/context"
|
||||
|
||||
"github.com/purpleidea/mgmt/converger"
|
||||
"github.com/purpleidea/mgmt/etcd"
|
||||
"github.com/purpleidea/mgmt/gconfig"
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
"github.com/purpleidea/mgmt/pgraph"
|
||||
"github.com/purpleidea/mgmt/puppet"
|
||||
"github.com/purpleidea/mgmt/recwatch"
|
||||
"github.com/purpleidea/mgmt/remote"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
etcdtypes "github.com/coreos/etcd/pkg/types"
|
||||
"github.com/coreos/pkg/capnslog"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
// set at compile time
|
||||
var (
|
||||
program string
|
||||
version string
|
||||
)
|
||||
|
||||
const (
|
||||
DEBUG = false
|
||||
prefix = fmt.Sprintf("/var/lib/%s/", program)
|
||||
)
|
||||
|
||||
// signal handler
|
||||
func waitForSignal(exit chan bool) {
|
||||
func waitForSignal(exit chan error) error {
|
||||
signals := make(chan os.Signal, 1)
|
||||
signal.Notify(signals, os.Interrupt) // catch ^C
|
||||
//signal.Notify(signals, os.Kill) // catch signals
|
||||
signal.Notify(signals, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case e := <-signals: // any signal will do
|
||||
if e == os.Interrupt {
|
||||
case sig := <-signals: // any signal will do
|
||||
if sig == os.Interrupt {
|
||||
log.Println("Interrupted by ^C")
|
||||
return nil
|
||||
} else {
|
||||
log.Println("Interrupted by signal")
|
||||
return fmt.Errorf("Killed by %v", sig)
|
||||
}
|
||||
case <-exit: // or a manual signal
|
||||
case err := <-exit: // or a manual signal
|
||||
log.Println("Interrupted by exit signal")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func run(c *cli.Context) {
|
||||
// run is the main run target.
|
||||
func run(c *cli.Context) error {
|
||||
var start = time.Now().UnixNano()
|
||||
var wg sync.WaitGroup
|
||||
exit := make(chan bool) // exit signal
|
||||
converged := make(chan bool) // converged signal
|
||||
log.Printf("This is: %v, version: %v", program, version)
|
||||
log.Printf("Main: Start: %v", start)
|
||||
G := NewGraph("Graph") // give graph a default name
|
||||
|
||||
hostname, _ := os.Hostname()
|
||||
// allow passing in the hostname, instead of using --hostname
|
||||
if c.IsSet("file") {
|
||||
if config := gconfig.ParseConfigFromFile(c.String("file")); config != nil {
|
||||
if h := config.Hostname; h != "" {
|
||||
hostname = h
|
||||
}
|
||||
}
|
||||
}
|
||||
if c.IsSet("hostname") { // override by cli
|
||||
if h := c.String("hostname"); h != "" {
|
||||
hostname = h
|
||||
}
|
||||
}
|
||||
noop := c.Bool("noop")
|
||||
|
||||
seeds, err := etcdtypes.NewURLs(
|
||||
util.FlattenListWithSplit(c.StringSlice("seeds"), []string{",", ";", " "}),
|
||||
)
|
||||
if err != nil && len(c.StringSlice("seeds")) > 0 {
|
||||
log.Printf("Main: Error: seeds didn't parse correctly!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
clientURLs, err := etcdtypes.NewURLs(
|
||||
util.FlattenListWithSplit(c.StringSlice("client-urls"), []string{",", ";", " "}),
|
||||
)
|
||||
if err != nil && len(c.StringSlice("client-urls")) > 0 {
|
||||
log.Printf("Main: Error: clientURLs didn't parse correctly!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
serverURLs, err := etcdtypes.NewURLs(
|
||||
util.FlattenListWithSplit(c.StringSlice("server-urls"), []string{",", ";", " "}),
|
||||
)
|
||||
if err != nil && len(c.StringSlice("server-urls")) > 0 {
|
||||
log.Printf("Main: Error: serverURLs didn't parse correctly!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
idealClusterSize := uint16(c.Int("ideal-cluster-size"))
|
||||
if idealClusterSize < 1 {
|
||||
log.Printf("Main: Error: idealClusterSize should be at least one!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
if c.IsSet("file") && c.IsSet("puppet") {
|
||||
log.Println("Main: Error: the --file and --puppet parameters cannot be used together!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
if c.Bool("no-server") && len(c.StringSlice("remote")) > 0 {
|
||||
// TODO: in this case, we won't be able to tunnel stuff back to
|
||||
// here, so if we're okay with every remote graph running in an
|
||||
// isolated mode, then this is okay. Improve on this if there's
|
||||
// someone who really wants to be able to do this.
|
||||
log.Println("Main: Error: the --no-server and --remote parameters cannot be used together!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
cConns := uint16(c.Int("cconns"))
|
||||
if cConns < 0 {
|
||||
log.Printf("Main: Error: --cconns should be at least zero!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
if c.IsSet("converged-timeout") && cConns > 0 && len(c.StringSlice("remote")) > c.Int("cconns") {
|
||||
log.Printf("Main: Error: combining --converged-timeout with more remotes than available connections will never converge!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
depth := uint16(c.Int("depth"))
|
||||
if depth < 0 { // user should not be using this argument manually
|
||||
log.Printf("Main: Error: negative values for --depth are not permitted!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
|
||||
if c.IsSet("prefix") && c.Bool("tmp-prefix") {
|
||||
log.Println("Main: Error: combining --prefix and the request for a tmp prefix is illogical!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
if s := c.String("prefix"); c.IsSet("prefix") && s != "" {
|
||||
prefix = s
|
||||
}
|
||||
|
||||
// make sure the working directory prefix exists
|
||||
if c.Bool("tmp-prefix") || os.MkdirAll(prefix, 0770) != nil {
|
||||
if c.Bool("tmp-prefix") || c.Bool("allow-tmp-prefix") {
|
||||
if prefix, err = ioutil.TempDir("", program+"-"); err != nil {
|
||||
log.Printf("Main: Error: Can't create temporary prefix!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
log.Println("Main: Warning: Working prefix directory is temporary!")
|
||||
|
||||
} else {
|
||||
log.Printf("Main: Error: Can't create prefix!")
|
||||
return cli.NewExitError("", 1)
|
||||
}
|
||||
}
|
||||
log.Printf("Main: Working prefix is: %s", prefix)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
exit := make(chan error) // exit signal
|
||||
var G, fullGraph *pgraph.Graph
|
||||
|
||||
// exit after `max-runtime` seconds for no reason at all...
|
||||
if i := c.Int("max-runtime"); i > 0 {
|
||||
go func() {
|
||||
time.Sleep(time.Duration(i) * time.Second)
|
||||
exit <- true
|
||||
exit <- nil
|
||||
}()
|
||||
}
|
||||
|
||||
// initial etcd peer endpoint
|
||||
seed := c.String("seed")
|
||||
if seed == "" {
|
||||
// XXX: start up etcd server, others will join me!
|
||||
seed = "http://127.0.0.1:2379" // thus we use the local server!
|
||||
// setup converger
|
||||
converger := converger.NewConverger(
|
||||
c.Int("converged-timeout"),
|
||||
nil, // stateFn gets added in by EmbdEtcd
|
||||
)
|
||||
go converger.Loop(true) // main loop for converger, true to start paused
|
||||
|
||||
// embedded etcd
|
||||
if len(seeds) == 0 {
|
||||
log.Printf("Main: Seeds: No seeds specified!")
|
||||
} else {
|
||||
log.Printf("Main: Seeds(%v): %v", len(seeds), seeds)
|
||||
}
|
||||
// then, connect to `seed` as a client
|
||||
|
||||
// FIXME: validate seed, or wait for it to fail in etcd init?
|
||||
|
||||
// etcd
|
||||
etcdO := &EtcdWObject{
|
||||
seed: seed,
|
||||
ctimeout: c.Int("converged-timeout"),
|
||||
converged: converged,
|
||||
EmbdEtcd := etcd.NewEmbdEtcd(
|
||||
hostname,
|
||||
seeds,
|
||||
clientURLs,
|
||||
serverURLs,
|
||||
c.Bool("no-server"),
|
||||
idealClusterSize,
|
||||
prefix,
|
||||
converger,
|
||||
)
|
||||
if EmbdEtcd == nil {
|
||||
// TODO: verify EmbdEtcd is not nil below...
|
||||
exit <- fmt.Errorf("Main: Etcd: Creation failed!")
|
||||
} else if err := EmbdEtcd.Startup(); err != nil { // startup (returns when etcd main loop is running)
|
||||
exit <- fmt.Errorf("Main: Etcd: Startup failed: %v", err)
|
||||
}
|
||||
convergerStateFn := func(b bool) error {
|
||||
// exit if we are using the converged-timeout and we are the
|
||||
// root node. otherwise, if we are a child node in a remote
|
||||
// execution hierarchy, we should only notify our converged
|
||||
// state and wait for the parent to trigger the exit.
|
||||
if depth == 0 && c.Int("converged-timeout") >= 0 {
|
||||
if b {
|
||||
log.Printf("Converged for %d seconds, exiting!", c.Int("converged-timeout"))
|
||||
exit <- nil // trigger an exit!
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// send our individual state into etcd for others to see
|
||||
return etcd.EtcdSetHostnameConverged(EmbdEtcd, hostname, b) // TODO: what should happen on error?
|
||||
}
|
||||
if EmbdEtcd != nil {
|
||||
converger.SetStateFn(convergerStateFn)
|
||||
}
|
||||
|
||||
hostname := c.String("hostname")
|
||||
if hostname == "" {
|
||||
hostname, _ = os.Hostname() // etcd watch key // XXX: this is not the correct key name this is the set key name... WOOPS
|
||||
}
|
||||
exitchan := make(chan struct{}) // exit on close
|
||||
go func() {
|
||||
startchan := make(chan struct{}) // start signal
|
||||
go func() { startchan <- struct{}{} }()
|
||||
file := c.String("file")
|
||||
configchan := make(chan bool)
|
||||
if !c.Bool("no-watch") {
|
||||
configchan = ConfigWatch(file)
|
||||
var configchan chan error
|
||||
var puppetchan <-chan time.Time
|
||||
if !c.Bool("no-watch") && c.IsSet("file") {
|
||||
configchan = recwatch.ConfigWatch(file)
|
||||
} else if c.IsSet("puppet") {
|
||||
interval := puppet.PuppetInterval(c.String("puppet-conf"))
|
||||
puppetchan = time.Tick(time.Duration(interval) * time.Second)
|
||||
}
|
||||
log.Printf("Etcd: Starting...")
|
||||
etcdchan := etcdO.EtcdWatch()
|
||||
log.Println("Etcd: Starting...")
|
||||
etcdchan := etcd.EtcdWatch(EmbdEtcd)
|
||||
first := true // first loop or not
|
||||
for {
|
||||
log.Println("Main: Waiting...")
|
||||
select {
|
||||
case _ = <-startchan: // kick the loop once at start
|
||||
case <-startchan: // kick the loop once at start
|
||||
// pass
|
||||
case msg := <-etcdchan:
|
||||
switch msg {
|
||||
// some types of messages we ignore...
|
||||
case etcdFoo, etcdBar:
|
||||
|
||||
case b := <-etcdchan:
|
||||
if !b { // ignore the message
|
||||
continue
|
||||
// while others passthrough and cause a compile!
|
||||
case etcdStart, etcdEvent:
|
||||
// pass
|
||||
default:
|
||||
log.Fatal("Etcd: Unhandled message: ", msg)
|
||||
}
|
||||
case msg := <-configchan:
|
||||
if c.Bool("no-watch") || !msg {
|
||||
// everything else passes through to cause a compile!
|
||||
|
||||
case <-puppetchan:
|
||||
// nothing, just go on
|
||||
|
||||
case e := <-configchan:
|
||||
if c.Bool("no-watch") {
|
||||
continue // not ready to read config
|
||||
}
|
||||
//case compile_event: XXX
|
||||
if e != nil {
|
||||
exit <- e // trigger exit
|
||||
continue
|
||||
//return // TODO: return or wait for exitchan?
|
||||
}
|
||||
// XXX: case compile_event: ...
|
||||
// ...
|
||||
case <-exitchan:
|
||||
return
|
||||
}
|
||||
|
||||
config := ParseConfigFromFile(file)
|
||||
var config *gconfig.GraphConfig
|
||||
if c.IsSet("file") {
|
||||
config = gconfig.ParseConfigFromFile(file)
|
||||
} else if c.IsSet("puppet") {
|
||||
config = puppet.ParseConfigFromPuppet(c.String("puppet"), c.String("puppet-conf"))
|
||||
}
|
||||
if config == nil {
|
||||
log.Printf("Config parse failure")
|
||||
log.Printf("Config: Parse failure")
|
||||
continue
|
||||
}
|
||||
|
||||
if config.Hostname != "" && config.Hostname != hostname {
|
||||
log.Printf("Config: Hostname changed, ignoring config!")
|
||||
continue
|
||||
}
|
||||
config.Hostname = hostname // set it in case it was ""
|
||||
|
||||
// run graph vertex LOCK...
|
||||
if !first { // XXX: we can flatten this check out I think
|
||||
log.Printf("State: %v -> %v", G.SetState(graphPausing), G.GetState())
|
||||
if !first { // TODO: we can flatten this check out I think
|
||||
converger.Pause() // FIXME: add sync wait?
|
||||
G.Pause() // sync
|
||||
log.Printf("State: %v -> %v", G.SetState(graphPaused), G.GetState())
|
||||
}
|
||||
|
||||
// build the graph from a config file
|
||||
// build the graph on events (eg: from etcd)
|
||||
if !UpdateGraphFromConfig(config, hostname, G, etcdO) {
|
||||
log.Fatal("Config: We borked the graph.") // XXX
|
||||
// build graph from yaml file on events (eg: from etcd)
|
||||
// we need the vertices to be paused to work on them
|
||||
if newFullgraph, err := config.NewGraphFromConfig(fullGraph, EmbdEtcd, noop); err == nil { // keep references to all original elements
|
||||
fullGraph = newFullgraph
|
||||
} else {
|
||||
log.Printf("Config: Error making new graph from config: %v", err)
|
||||
// unpause!
|
||||
if !first {
|
||||
G.Start(&wg, first) // sync
|
||||
converger.Start() // after G.Start()
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
G = fullGraph.Copy() // copy to active graph
|
||||
// XXX: do etcd transaction out here...
|
||||
G.AutoEdges() // add autoedges; modifies the graph
|
||||
G.AutoGroup() // run autogroup; modifies the graph
|
||||
// TODO: do we want to do a transitive reduction?
|
||||
|
||||
log.Printf("Graph: %v", G) // show graph
|
||||
err := G.ExecGraphviz(c.String("graphviz-filter"), c.String("graphviz"))
|
||||
if err != nil {
|
||||
@@ -153,53 +332,85 @@ func run(c *cli.Context) {
|
||||
} else {
|
||||
log.Printf("Graphviz: Successfully generated graph!")
|
||||
}
|
||||
G.SetVertex()
|
||||
G.SetConvergedCallback(c.Int("converged-timeout"), converged)
|
||||
G.AssociateData(converger)
|
||||
// G.Start(...) needs to be synchronous or wait,
|
||||
// because if half of the nodes are started and
|
||||
// some are not ready yet and the EtcdWatch
|
||||
// loops, we'll cause G.Pause(...) before we
|
||||
// even got going, thus causing nil pointer errors
|
||||
log.Printf("State: %v -> %v", G.SetState(graphStarting), G.GetState())
|
||||
G.Start(&wg, first) // sync
|
||||
log.Printf("State: %v -> %v", G.SetState(graphStarted), G.GetState())
|
||||
converger.Start() // after G.Start()
|
||||
first = false
|
||||
}
|
||||
}()
|
||||
|
||||
if i := c.Int("converged-timeout"); i >= 0 {
|
||||
configWatcher := recwatch.NewConfigWatcher()
|
||||
events := configWatcher.Events()
|
||||
if !c.Bool("no-watch") {
|
||||
configWatcher.Add(c.StringSlice("remote")...) // add all the files...
|
||||
} else {
|
||||
events = nil // signal that no-watch is true
|
||||
}
|
||||
go func() {
|
||||
ConvergedLoop:
|
||||
for {
|
||||
<-converged // when anyone says they have converged
|
||||
select {
|
||||
case err := <-configWatcher.Error():
|
||||
exit <- err // trigger an exit!
|
||||
|
||||
if etcdO.GetConvergedState() != etcdConvergedTimeout {
|
||||
continue
|
||||
}
|
||||
for v := range G.GetVerticesChan() {
|
||||
if v.Type.GetConvergedState() != typeConvergedTimeout {
|
||||
continue ConvergedLoop
|
||||
}
|
||||
}
|
||||
|
||||
// if all have converged, exit
|
||||
log.Printf("Converged for %d seconds, exiting!", i)
|
||||
exit <- true
|
||||
for {
|
||||
<-converged
|
||||
} // unblock/drain
|
||||
//return
|
||||
case <-exitchan:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
// initialize the add watcher, which calls the f callback on map changes
|
||||
convergerCb := func(f func(map[string]bool) error) (func(), error) {
|
||||
return etcd.EtcdAddHostnameConvergedWatcher(EmbdEtcd, f)
|
||||
}
|
||||
|
||||
// build remotes struct for remote ssh
|
||||
remotes := remote.NewRemotes(
|
||||
EmbdEtcd.LocalhostClientURLs().StringSlice(),
|
||||
[]string{etcd.DefaultClientURL},
|
||||
noop,
|
||||
c.StringSlice("remote"), // list of files
|
||||
events, // watch for file changes
|
||||
cConns,
|
||||
c.Bool("allow-interactive"),
|
||||
c.String("ssh-priv-id-rsa"),
|
||||
!c.Bool("no-caching"),
|
||||
depth,
|
||||
prefix,
|
||||
converger,
|
||||
convergerCb,
|
||||
program,
|
||||
)
|
||||
|
||||
// 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()
|
||||
|
||||
if !c.IsSet("file") && !c.IsSet("puppet") {
|
||||
converger.Start() // better start this for empty graphs
|
||||
}
|
||||
log.Println("Main: Running...")
|
||||
|
||||
waitForSignal(exit) // pass in exit channel to watch
|
||||
err = waitForSignal(exit) // pass in exit channel to watch
|
||||
|
||||
log.Println("Destroy...")
|
||||
|
||||
configWatcher.Close() // stop sending file changes to remotes
|
||||
remotes.Exit() // tell all the remote connections to shutdown; waits!
|
||||
|
||||
G.Exit() // tell all the children to exit
|
||||
|
||||
if DEBUG {
|
||||
// tell inner main loop to exit
|
||||
close(exitchan)
|
||||
|
||||
// cleanup etcd main loop last so it can process everything first
|
||||
if err := EmbdEtcd.Destroy(); err != nil { // shutdown and cleanup etcd
|
||||
log.Printf("Etcd exited poorly with: %v", err)
|
||||
}
|
||||
|
||||
if global.DEBUG {
|
||||
log.Printf("Graph: %v", G)
|
||||
}
|
||||
|
||||
@@ -207,13 +418,26 @@ func run(c *cli.Context) {
|
||||
|
||||
// TODO: wait for each vertex to exit...
|
||||
log.Println("Goodbye!")
|
||||
return err
|
||||
}
|
||||
|
||||
func main() {
|
||||
//if DEBUG {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
//}
|
||||
log.SetFlags(log.Flags() - log.Ldate) // remove the date for now
|
||||
var flags int
|
||||
if global.DEBUG || true { // TODO: remove || true
|
||||
flags = log.LstdFlags | log.Lshortfile
|
||||
}
|
||||
flags = (flags - log.Ldate) // remove the date for now
|
||||
log.SetFlags(flags)
|
||||
|
||||
// un-hijack from capnslog...
|
||||
log.SetOutput(os.Stderr)
|
||||
if global.VERBOSE {
|
||||
capnslog.SetFormatter(capnslog.NewLogFormatter(os.Stderr, "(etcd) ", flags))
|
||||
} else {
|
||||
capnslog.SetFormatter(capnslog.NewNilFormatter())
|
||||
}
|
||||
|
||||
// test for sanity
|
||||
if program == "" || version == "" {
|
||||
log.Fatal("Program was not compiled correctly. Please see Makefile.")
|
||||
}
|
||||
@@ -234,6 +458,7 @@ func main() {
|
||||
Name: "file, f",
|
||||
Value: "",
|
||||
Usage: "graph definition to run",
|
||||
EnvVar: "MGMT_FILE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-watch",
|
||||
@@ -261,20 +486,105 @@ func main() {
|
||||
Usage: "hostname to use",
|
||||
},
|
||||
// if empty, it will startup a new server
|
||||
cli.StringFlag{
|
||||
Name: "seed, s",
|
||||
Value: "",
|
||||
Usage: "default etc peer endpoint",
|
||||
cli.StringSliceFlag{
|
||||
Name: "seeds, s",
|
||||
Value: &cli.StringSlice{}, // empty slice
|
||||
Usage: "default etc client endpoint",
|
||||
EnvVar: "MGMT_SEEDS",
|
||||
},
|
||||
// port 2379 and 4001 are common
|
||||
cli.StringSliceFlag{
|
||||
Name: "client-urls",
|
||||
Value: &cli.StringSlice{},
|
||||
Usage: "list of URLs to listen on for client traffic",
|
||||
EnvVar: "MGMT_CLIENT_URLS",
|
||||
},
|
||||
// port 2380 and 7001 are common
|
||||
cli.StringSliceFlag{
|
||||
Name: "server-urls, peer-urls",
|
||||
Value: &cli.StringSlice{},
|
||||
Usage: "list of URLs to listen on for server (peer) traffic",
|
||||
EnvVar: "MGMT_SERVER_URLS",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-server",
|
||||
Usage: "do not let other servers peer with me",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ideal-cluster-size",
|
||||
Value: etcd.DefaultIdealClusterSize,
|
||||
Usage: "ideal number of server peers in cluster, only read by initial server",
|
||||
EnvVar: "MGMT_IDEAL_CLUSTER_SIZE",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "converged-timeout, t",
|
||||
Value: -1,
|
||||
Usage: "exit after approximately this many seconds in a converged state",
|
||||
EnvVar: "MGMT_CONVERGED_TIMEOUT",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "max-runtime",
|
||||
Value: 0,
|
||||
Usage: "exit after a maximum of approximately this many seconds",
|
||||
EnvVar: "MGMT_MAX_RUNTIME",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "noop",
|
||||
Usage: "globally force all resources into no-op mode",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "puppet, p",
|
||||
Value: "",
|
||||
Usage: "load graph from puppet, optionally takes a manifest or path to manifest file",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "puppet-conf",
|
||||
Value: "",
|
||||
Usage: "supply the path to an alternate puppet.conf file to use",
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "remote",
|
||||
Value: &cli.StringSlice{},
|
||||
Usage: "list of remote graph definitions to run",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow-interactive",
|
||||
Usage: "allow interactive prompting, such as for remote passwords",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "ssh-priv-id-rsa",
|
||||
Value: "~/.ssh/id_rsa",
|
||||
Usage: "default path to ssh key file, set empty to never touch",
|
||||
EnvVar: "MGMT_SSH_PRIV_ID_RSA",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "cconns",
|
||||
Value: 0,
|
||||
Usage: "number of maximum concurrent remote ssh connections to run, 0 for unlimited",
|
||||
EnvVar: "MGMT_CCONNS",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-caching",
|
||||
Usage: "don't allow remote caching of remote execution binary",
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "depth",
|
||||
Hidden: true, // internal use only
|
||||
Value: 0,
|
||||
Usage: "specify depth in remote hierarchy",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "prefix",
|
||||
Usage: "specify a path to the working prefix directory",
|
||||
EnvVar: "MGMT_PREFIX",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "tmp-prefix",
|
||||
Usage: "request a pseudo-random, temporary prefix to be used",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "allow-tmp-prefix",
|
||||
Usage: "allow creation of a new temporary prefix if main prefix is unavailable",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
122
misc.go
@@ -1,122 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/gob"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Similar to the GNU dirname command
|
||||
func Dirname(p string) string {
|
||||
if p == "/" {
|
||||
return ""
|
||||
}
|
||||
d, _ := path.Split(path.Clean(p))
|
||||
return d
|
||||
}
|
||||
|
||||
func Basename(p string) string {
|
||||
_, b := path.Split(path.Clean(p))
|
||||
if p[len(p)-1:] == "/" { // don't loose the tail slash
|
||||
b += "/"
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Split a path into an array of tokens excluding any trailing empty tokens
|
||||
func PathSplit(p string) []string {
|
||||
return strings.Split(path.Clean(p), "/")
|
||||
}
|
||||
|
||||
// Does path string contain the given path prefix in it?
|
||||
func HasPathPrefix(p, prefix string) bool {
|
||||
|
||||
patharray := PathSplit(p)
|
||||
prefixarray := PathSplit(prefix)
|
||||
|
||||
if len(prefixarray) > len(patharray) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(prefixarray); i++ {
|
||||
if prefixarray[i] != patharray[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Delta of path prefix, tells you how many path tokens different the prefix is
|
||||
func PathPrefixDelta(p, prefix string) int {
|
||||
|
||||
if !HasPathPrefix(p, prefix) {
|
||||
return -1
|
||||
}
|
||||
patharray := PathSplit(p)
|
||||
prefixarray := PathSplit(prefix)
|
||||
return len(patharray) - len(prefixarray)
|
||||
}
|
||||
|
||||
func PathIsDir(p string) bool {
|
||||
return p[len(p)-1:] == "/" // a dir has a trailing slash in this context
|
||||
}
|
||||
|
||||
// encode an object as base 64, serialize and then base64 encode
|
||||
func ObjToB64(obj interface{}) (string, bool) {
|
||||
b := bytes.Buffer{}
|
||||
e := gob.NewEncoder(&b)
|
||||
err := e.Encode(obj)
|
||||
if err != nil {
|
||||
//log.Println("Gob failed to Encode: ", err)
|
||||
return "", false
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(b.Bytes()), true
|
||||
}
|
||||
|
||||
// TODO: is it possible to somehow generically just return the obj?
|
||||
// decode an object into the waiting obj which you pass a reference to
|
||||
func B64ToObj(str string, obj interface{}) bool {
|
||||
bb, err := base64.StdEncoding.DecodeString(str)
|
||||
if err != nil {
|
||||
//log.Println("Base64 failed to Decode: ", err)
|
||||
return false
|
||||
}
|
||||
b := bytes.NewBuffer(bb)
|
||||
d := gob.NewDecoder(b)
|
||||
err = d.Decode(obj)
|
||||
if err != nil {
|
||||
//log.Println("Gob failed to Decode: ", err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// special version of time.After that blocks when given a negative integer
|
||||
// when used in a case statement, the timer restarts on each select call to it
|
||||
func TimeAfterOrBlock(t int) <-chan time.Time {
|
||||
if t < 0 {
|
||||
return make(chan time.Time) // blocks forever
|
||||
}
|
||||
return time.After(time.Duration(t) * time.Second)
|
||||
}
|
||||
1
misc/example.conf
Normal file
@@ -0,0 +1 @@
|
||||
# example mgmt configuration file, currently has no options at the moment!
|
||||
15
misc/go
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# hack around stupid $GOPATH semantics, with ~/bin/go helper
|
||||
# thanks to Nilium in #go-nuts for 1/3 of the idea
|
||||
[ -z "$GOPATH" ] && echo '$GOPATH is not set!' && exit 1
|
||||
GO="$(which -a go | sed -e '2q;d')" # TODO: pick /usr/bin/go in a better way
|
||||
if [ "$1" = "generate" ]; then
|
||||
exec $GO "$@" # go generate is stupid and gets confused by $GOPATH
|
||||
fi
|
||||
# the idea is to have $project/gopath/src/ be a symlink to ../vendor but you put
|
||||
# all of your vendored things in vendor/ but with this gopath can be per project
|
||||
if [ -d "$PWD/vendor/" ] && [ -d "$PWD/gopath/" ] && [ "`readlink $PWD/gopath/src`" = "../vendor" ] ; then
|
||||
GOPATH="$PWD/gopath/:$GOPATH" $GO "$@"
|
||||
else
|
||||
$GO "$@"
|
||||
fi
|
||||
@@ -1,11 +1,16 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# setup a simple go environment
|
||||
XPWD=`pwd`
|
||||
ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )" # dir!
|
||||
cd "${ROOT}" >/dev/null
|
||||
|
||||
travis=0
|
||||
if env | grep -q '^TRAVIS=true$'; then
|
||||
travis=1
|
||||
fi
|
||||
|
||||
sudo_command=$(which sudo)
|
||||
|
||||
if [ $travis -eq 0 ]; then
|
||||
YUM=`which yum 2>/dev/null`
|
||||
APT=`which apt-get 2>/dev/null`
|
||||
@@ -15,25 +20,33 @@ if [ $travis -eq 0 ]; then
|
||||
fi
|
||||
if [ ! -z "$YUM" ]; then
|
||||
# some go dependencies are stored in mercurial
|
||||
sudo $YUM install -y golang golang-googlecode-tools-stringer hg
|
||||
$sudo_command $YUM install -y golang golang-googlecode-tools-stringer hg
|
||||
|
||||
fi
|
||||
if [ ! -z "$APT" ]; then
|
||||
sudo $APT install -y golang mercurial
|
||||
|
||||
$sudo_command $APT update
|
||||
$sudo_command $APT install -y golang make gcc packagekit mercurial
|
||||
# one of these two golang tools packages should work on debian
|
||||
$sudo_command $APT install -y golang-golang-x-tools || true
|
||||
$sudo_command $APT install -y golang-go.tools || true
|
||||
$sudo_command $APT install -y libpcap0.8-dev || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# build etcd
|
||||
git clone --recursive https://github.com/coreos/etcd/ && cd etcd
|
||||
git checkout v2.2.4 # TODO: update to newer versions as needed
|
||||
[ -x build ] && ./build
|
||||
mkdir -p ~/bin/
|
||||
cp bin/etcd ~/bin/
|
||||
cd -
|
||||
rm -rf etcd # clean up to avoid failing on upstream gofmt errors
|
||||
# if golang is too old, we don't want to fail with an obscure error later
|
||||
if go version | grep 'go1\.[0123]\.'; then
|
||||
echo "mgmt requires go1.4 or higher."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
go get ./... # get all the go dependencies
|
||||
[ -e "$GOBIN/mgmt" ] && rm -f "$GOBIN/mgmt" # the `go get` version has no -X
|
||||
# vet is built-in in go 1.6 - we check for go vet command
|
||||
go vet 1> /dev/null 2>&1
|
||||
ret=$?
|
||||
if [[ $ret != 0 ]]; then
|
||||
go get golang.org/x/tools/cmd/vet # add in `go vet` for travis
|
||||
fi
|
||||
go get golang.org/x/tools/cmd/stringer # for automatic stringer-ing
|
||||
go get github.com/golang/lint/golint # for `golint`-ing
|
||||
cd "$XPWD" >/dev/null
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# example mgmt configuration file, currently has not options at the moment!
|
||||
13
misc/mgmt.service
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Run mgmt configuration management
|
||||
Documentation=https://github.com/purpleidea/mgmt/
|
||||
After=systemd-networkd.service
|
||||
Requires=systemd-networkd.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/mgmt run ${OPTS}
|
||||
RestartSec=5s
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
200
misc_test.go
@@ -1,200 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMiscT1(t *testing.T) {
|
||||
|
||||
if Dirname("/foo/bar/baz") != "/foo/bar/" {
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
if Dirname("/foo/bar/baz/") != "/foo/bar/" {
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
if Dirname("/") != "" { // TODO: should this equal "/" or "" ?
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
if Basename("/foo/bar/baz") != "baz" {
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
if Basename("/foo/bar/baz/") != "baz/" {
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
if Basename("/") != "/" { // TODO: should this equal "" or "/" ?
|
||||
t.Errorf("Result is incorrect.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMiscT2(t *testing.T) {
|
||||
|
||||
// TODO: compare the output with the actual list
|
||||
p1 := "/foo/bar/baz"
|
||||
r1 := []string{"", "foo", "bar", "baz"}
|
||||
if len(PathSplit(p1)) != len(r1) {
|
||||
//t.Errorf("Result should be: %q.", r1)
|
||||
t.Errorf("Result should have a length of: %v.", len(r1))
|
||||
}
|
||||
|
||||
p2 := "/foo/bar/baz/"
|
||||
r2 := []string{"", "foo", "bar", "baz"}
|
||||
if len(PathSplit(p2)) != len(r2) {
|
||||
t.Errorf("Result should have a length of: %v.", len(r2))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscT3(t *testing.T) {
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz", "/foo/ba") != false {
|
||||
t.Errorf("Result should be false.")
|
||||
}
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz", "/foo/bar") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz", "/foo/bar/") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz/", "/foo/bar") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz/", "/foo/bar/") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
|
||||
if HasPathPrefix("/foo/bar/baz/", "/foo/bar/baz/dude") != false {
|
||||
t.Errorf("Result should be false.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscT4(t *testing.T) {
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz", "/foo/ba") != -1 {
|
||||
t.Errorf("Result should be -1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz", "/foo/bar") != 1 {
|
||||
t.Errorf("Result should be 1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz", "/foo/bar/") != 1 {
|
||||
t.Errorf("Result should be 1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar") != 1 {
|
||||
t.Errorf("Result should be 1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/") != 1 {
|
||||
t.Errorf("Result should be 1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/baz/dude") != -1 {
|
||||
t.Errorf("Result should be -1.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz/a/b/c/", "/foo/bar/baz") != 3 {
|
||||
t.Errorf("Result should be 3.")
|
||||
}
|
||||
|
||||
if PathPrefixDelta("/foo/bar/baz/", "/foo/bar/baz") != 0 {
|
||||
t.Errorf("Result should be 0.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscT5(t *testing.T) {
|
||||
|
||||
if PathIsDir("/foo/bar/baz/") != true {
|
||||
t.Errorf("Result should be false.")
|
||||
}
|
||||
|
||||
if PathIsDir("/foo/bar/baz") != false {
|
||||
t.Errorf("Result should be false.")
|
||||
}
|
||||
|
||||
if PathIsDir("/foo/") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
|
||||
if PathIsDir("/") != true {
|
||||
t.Errorf("Result should be true.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscT6(t *testing.T) {
|
||||
|
||||
type foo struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Value int `yaml:"value"`
|
||||
}
|
||||
|
||||
obj := foo{"dude", "sweet", 42}
|
||||
output, ok := ObjToB64(obj)
|
||||
if ok != true {
|
||||
t.Errorf("First result should be true.")
|
||||
}
|
||||
var data foo
|
||||
if B64ToObj(output, &data) != true {
|
||||
t.Errorf("Second result should be true.")
|
||||
}
|
||||
// TODO: there is probably a better way to compare these two...
|
||||
if fmt.Sprintf("%+v\n", obj) != fmt.Sprintf("%+v\n", data) {
|
||||
t.Errorf("Strings should match.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiscT7(t *testing.T) {
|
||||
|
||||
type Foo struct {
|
||||
Name string `yaml:"name"`
|
||||
Type string `yaml:"type"`
|
||||
Value int `yaml:"value"`
|
||||
}
|
||||
|
||||
type bar struct {
|
||||
Foo `yaml:",inline"` // anonymous struct must be public!
|
||||
Comment string `yaml:"comment"`
|
||||
}
|
||||
|
||||
obj := bar{Foo{"dude", "sweet", 42}, "hello world"}
|
||||
output, ok := ObjToB64(obj)
|
||||
if ok != true {
|
||||
t.Errorf("First result should be true.")
|
||||
}
|
||||
var data bar
|
||||
if B64ToObj(output, &data) != true {
|
||||
t.Errorf("Second result should be true.")
|
||||
}
|
||||
// TODO: there is probably a better way to compare these two...
|
||||
if fmt.Sprintf("%+v\n", obj) != fmt.Sprintf("%+v\n", data) {
|
||||
t.Errorf("Strings should match.")
|
||||
}
|
||||
}
|
||||
634
pgraph.go
@@ -1,634 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Pgraph (Pointer Graph)
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=graphState -output=graphstate_stringer.go
|
||||
type graphState int
|
||||
|
||||
const (
|
||||
graphNil graphState = iota
|
||||
graphStarting
|
||||
graphStarted
|
||||
graphPausing
|
||||
graphPaused
|
||||
)
|
||||
|
||||
// The graph abstract data type (ADT) is defined as follows:
|
||||
// * the directed graph arrows point from left to right ( -> )
|
||||
// * the arrows point away from their dependencies (eg: arrows mean "before")
|
||||
// * IOW, you might see package -> file -> service (where package runs first)
|
||||
// * This is also the direction that the notify should happen in...
|
||||
type Graph struct {
|
||||
Name string
|
||||
Adjacency map[*Vertex]map[*Vertex]*Edge // *Vertex -> *Vertex (edge)
|
||||
state graphState
|
||||
mutex sync.Mutex // used when modifying graph State variable
|
||||
//Directed bool
|
||||
}
|
||||
|
||||
type Vertex struct {
|
||||
graph *Graph // store a pointer to the graph it's on
|
||||
Type // anonymous field
|
||||
data map[string]string // XXX: currently unused i think, remove?
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func NewGraph(name string) *Graph {
|
||||
return &Graph{
|
||||
Name: name,
|
||||
Adjacency: make(map[*Vertex]map[*Vertex]*Edge),
|
||||
state: graphNil,
|
||||
}
|
||||
}
|
||||
|
||||
func NewVertex(t Type) *Vertex {
|
||||
return &Vertex{
|
||||
Type: t,
|
||||
data: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
func NewEdge(name string) *Edge {
|
||||
return &Edge{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// returns the name of the graph
|
||||
func (g *Graph) GetName() string {
|
||||
return g.Name
|
||||
}
|
||||
|
||||
// set name of the graph
|
||||
func (g *Graph) SetName(name string) {
|
||||
g.Name = name
|
||||
}
|
||||
|
||||
func (g *Graph) GetState() graphState {
|
||||
//g.mutex.Lock()
|
||||
//defer g.mutex.Unlock()
|
||||
return g.state
|
||||
}
|
||||
|
||||
// set graph state and return previous state
|
||||
func (g *Graph) SetState(state graphState) graphState {
|
||||
g.mutex.Lock()
|
||||
defer g.mutex.Unlock()
|
||||
prev := g.GetState()
|
||||
g.state = state
|
||||
return prev
|
||||
}
|
||||
|
||||
// store a pointer in the type to it's parent vertex
|
||||
func (g *Graph) SetVertex() {
|
||||
for v := range g.GetVerticesChan() {
|
||||
v.Type.SetVertex(v)
|
||||
}
|
||||
}
|
||||
|
||||
// add a new vertex to the graph
|
||||
func (g *Graph) AddVertex(v *Vertex) {
|
||||
if _, exists := g.Adjacency[v]; !exists {
|
||||
g.Adjacency[v] = make(map[*Vertex]*Edge)
|
||||
|
||||
// store a pointer to the graph it's on for convenience and readability
|
||||
v.graph = g
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) DeleteVertex(v *Vertex) {
|
||||
delete(g.Adjacency, v)
|
||||
for k := range g.Adjacency {
|
||||
delete(g.Adjacency[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
// adds a directed edge to the graph from v1 to v2
|
||||
func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
|
||||
// NOTE: this doesn't allow more than one edge between two vertexes...
|
||||
// TODO: is this a problem?
|
||||
g.AddVertex(v1)
|
||||
g.AddVertex(v2)
|
||||
g.Adjacency[v1][v2] = e
|
||||
}
|
||||
|
||||
// XXX: does it make sense to return a channel here?
|
||||
// GetVertex finds the vertex in the graph with a particular search name
|
||||
func (g *Graph) GetVertex(name string) chan *Vertex {
|
||||
ch := make(chan *Vertex, 1)
|
||||
go func(name string) {
|
||||
for k := range g.Adjacency {
|
||||
if k.GetName() == name {
|
||||
ch <- k
|
||||
break
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}(name)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (g *Graph) GetVertexMatch(obj Type) *Vertex {
|
||||
for k := range g.Adjacency {
|
||||
if k.Compare(obj) { // XXX test
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *Graph) HasVertex(v *Vertex) bool {
|
||||
if _, exists := g.Adjacency[v]; exists {
|
||||
return true
|
||||
}
|
||||
//for k := range g.Adjacency {
|
||||
// if k == v {
|
||||
// return true
|
||||
// }
|
||||
//}
|
||||
return false
|
||||
}
|
||||
|
||||
// number of vertices in the graph
|
||||
func (g *Graph) NumVertices() int {
|
||||
return len(g.Adjacency)
|
||||
}
|
||||
|
||||
// number of edges in the graph
|
||||
func (g *Graph) NumEdges() int {
|
||||
count := 0
|
||||
for k := range g.Adjacency {
|
||||
count += len(g.Adjacency[k])
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// get an array (slice) of all vertices in the graph
|
||||
func (g *Graph) GetVertices() []*Vertex {
|
||||
var vertices []*Vertex
|
||||
for k := range g.Adjacency {
|
||||
vertices = append(vertices, k)
|
||||
}
|
||||
return vertices
|
||||
}
|
||||
|
||||
// returns a channel of all vertices in the graph
|
||||
func (g *Graph) GetVerticesChan() chan *Vertex {
|
||||
ch := make(chan *Vertex)
|
||||
go func(ch chan *Vertex) {
|
||||
for k := range g.Adjacency {
|
||||
ch <- k
|
||||
}
|
||||
close(ch)
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// make the graph pretty print
|
||||
func (g *Graph) String() string {
|
||||
return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges())
|
||||
}
|
||||
|
||||
// output the graph in graphviz format
|
||||
// https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29
|
||||
func (g *Graph) Graphviz() (out string) {
|
||||
//digraph g {
|
||||
// label="hello world";
|
||||
// node [shape=box];
|
||||
// A [label="A"];
|
||||
// B [label="B"];
|
||||
// C [label="C"];
|
||||
// D [label="D"];
|
||||
// E [label="E"];
|
||||
// A -> B [label=f];
|
||||
// B -> C [label=g];
|
||||
// D -> E [label=h];
|
||||
//}
|
||||
out += fmt.Sprintf("digraph %v {\n", g.GetName())
|
||||
out += fmt.Sprintf("\tlabel=\"%v\";\n", g.GetName())
|
||||
//out += "\tnode [shape=box];\n"
|
||||
str := ""
|
||||
for i := range g.Adjacency { // reverse paths
|
||||
out += fmt.Sprintf("\t%v [label=\"%v[%v]\"];\n", i.GetName(), i.GetType(), i.GetName())
|
||||
for j := range g.Adjacency[i] {
|
||||
k := g.Adjacency[i][j]
|
||||
// use str for clearer output ordering
|
||||
str += fmt.Sprintf("\t%v -> %v [label=%v];\n", i.GetName(), j.GetName(), k.Name)
|
||||
}
|
||||
}
|
||||
out += str
|
||||
out += "}\n"
|
||||
return
|
||||
}
|
||||
|
||||
// write out the graphviz data and run the correct graphviz filter command
|
||||
func (g *Graph) ExecGraphviz(program, filename string) error {
|
||||
|
||||
switch program {
|
||||
case "dot", "neato", "twopi", "circo", "fdp":
|
||||
default:
|
||||
return errors.New("Invalid graphviz program selected!")
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
return errors.New("No filename given!")
|
||||
}
|
||||
|
||||
// run as a normal user if possible when run with sudo
|
||||
uid, err1 := strconv.Atoi(os.Getenv("SUDO_UID"))
|
||||
gid, err2 := strconv.Atoi(os.Getenv("SUDO_GID"))
|
||||
|
||||
err := ioutil.WriteFile(filename, []byte(g.Graphviz()), 0644)
|
||||
if err != nil {
|
||||
return errors.New("Error writing to filename!")
|
||||
}
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
if err := os.Chown(filename, uid, gid); err != nil {
|
||||
return errors.New("Error changing file owner!")
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(program)
|
||||
if err != nil {
|
||||
return errors.New("Graphviz is missing!")
|
||||
}
|
||||
|
||||
out := fmt.Sprintf("%v.png", filename)
|
||||
cmd := exec.Command(path, "-Tpng", fmt.Sprintf("-o%v", out), filename)
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{
|
||||
Uid: uint32(uid),
|
||||
Gid: uint32(gid),
|
||||
}
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return errors.New("Error writing to image!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// google/golang hackers apparently do not think contains should be a built-in!
|
||||
func Contains(s []*Vertex, element *Vertex) bool {
|
||||
for _, v := range s {
|
||||
if element == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// return an array (slice) of all directed vertices to vertex v (??? -> v)
|
||||
// ostimestamp should use this
|
||||
func (g *Graph) IncomingGraphEdges(v *Vertex) []*Vertex {
|
||||
// TODO: we might be able to implement this differently by reversing
|
||||
// the Adjacency graph and then looping through it again...
|
||||
var s []*Vertex
|
||||
for k := range g.Adjacency { // reverse paths
|
||||
for w := range g.Adjacency[k] {
|
||||
if w == v {
|
||||
s = append(s, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// return an array (slice) of all vertices that vertex v points to (v -> ???)
|
||||
// poke should use this
|
||||
func (g *Graph) OutgoingGraphEdges(v *Vertex) []*Vertex {
|
||||
var s []*Vertex
|
||||
for k := range g.Adjacency[v] { // forward paths
|
||||
s = append(s, k)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// return an array (slice) of all vertices that connect to vertex v
|
||||
func (g *Graph) GraphEdges(v *Vertex) []*Vertex {
|
||||
var s []*Vertex
|
||||
s = append(s, g.IncomingGraphEdges(v)...)
|
||||
s = append(s, g.OutgoingGraphEdges(v)...)
|
||||
return s
|
||||
}
|
||||
|
||||
func (g *Graph) DFS(start *Vertex) []*Vertex {
|
||||
var d []*Vertex // discovered
|
||||
var s []*Vertex // stack
|
||||
if _, exists := g.Adjacency[start]; !exists {
|
||||
return nil // TODO: error
|
||||
}
|
||||
v := start
|
||||
s = append(s, v)
|
||||
for len(s) > 0 {
|
||||
|
||||
v, s = s[len(s)-1], s[:len(s)-1] // s.pop()
|
||||
|
||||
if !Contains(d, v) { // if not discovered
|
||||
d = append(d, v) // label as discovered
|
||||
|
||||
for _, w := range g.GraphEdges(v) {
|
||||
s = append(s, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// build a new graph containing only vertices from the list...
|
||||
func (g *Graph) FilterGraph(name string, vertices []*Vertex) *Graph {
|
||||
newgraph := NewGraph(name)
|
||||
|
||||
for k1, x := range g.Adjacency {
|
||||
for k2, e := range x {
|
||||
|
||||
//log.Printf("Filter: %v -> %v # %v", k1.Name, k2.Name, e.Name)
|
||||
if Contains(vertices, k1) || Contains(vertices, k2) {
|
||||
newgraph.AddEdge(k1, k2, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newgraph
|
||||
}
|
||||
|
||||
// return a channel containing the N disconnected graphs in our main graph
|
||||
// we can then process each of these in parallel
|
||||
func (g *Graph) GetDisconnectedGraphs() chan *Graph {
|
||||
ch := make(chan *Graph)
|
||||
go func() {
|
||||
var start *Vertex
|
||||
var d []*Vertex // discovered
|
||||
c := g.NumVertices()
|
||||
for len(d) < c {
|
||||
|
||||
// get an undiscovered vertex to start from
|
||||
for _, s := range g.GetVertices() {
|
||||
if !Contains(d, s) {
|
||||
start = s
|
||||
}
|
||||
}
|
||||
|
||||
// dfs through the graph
|
||||
dfs := g.DFS(start)
|
||||
// filter all the collected elements into a new graph
|
||||
newgraph := g.FilterGraph(g.Name, dfs)
|
||||
|
||||
// add number of elements found to found variable
|
||||
d = append(d, dfs...) // extend
|
||||
|
||||
// return this new graph to the channel
|
||||
ch <- newgraph
|
||||
|
||||
// if we've found all the elements, then we're done
|
||||
// otherwise loop through to continue...
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// return the indegree for the graph, IOW the count of vertices that point to me
|
||||
// NOTE: this returns the values for all vertices in one big lookup table
|
||||
func (g *Graph) InDegree() map[*Vertex]int {
|
||||
result := make(map[*Vertex]int)
|
||||
for k := range g.Adjacency {
|
||||
result[k] = 0 // initialize
|
||||
}
|
||||
|
||||
for k := range g.Adjacency {
|
||||
for z := range g.Adjacency[k] {
|
||||
result[z]++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// return the outdegree for the graph, IOW the count of vertices that point away
|
||||
// NOTE: this returns the values for all vertices in one big lookup table
|
||||
func (g *Graph) OutDegree() map[*Vertex]int {
|
||||
result := make(map[*Vertex]int)
|
||||
|
||||
for k := range g.Adjacency {
|
||||
result[k] = 0 // initialize
|
||||
for _ = range g.Adjacency[k] {
|
||||
result[k]++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// returns a topological sort for the graph
|
||||
// based on descriptions and code from wikipedia and rosetta code
|
||||
// TODO: add memoization, and cache invalidation to speed this up :)
|
||||
func (g *Graph) TopologicalSort() (result []*Vertex, ok bool) { // kahn's algorithm
|
||||
|
||||
var L []*Vertex // empty list that will contain the sorted elements
|
||||
var S []*Vertex // set of all nodes with no incoming edges
|
||||
remaining := make(map[*Vertex]int) // amount of edges remaining
|
||||
|
||||
for v, d := range g.InDegree() {
|
||||
if d == 0 {
|
||||
// accumulate set of all nodes with no incoming edges
|
||||
S = append(S, v)
|
||||
} else {
|
||||
// initialize remaining edge count from indegree
|
||||
remaining[v] = d
|
||||
}
|
||||
}
|
||||
|
||||
for len(S) > 0 {
|
||||
last := len(S) - 1 // remove a node v from S
|
||||
v := S[last]
|
||||
S = S[:last]
|
||||
L = append(L, v) // add v to tail of L
|
||||
for n := range g.Adjacency[v] {
|
||||
// for each node n remaining in the graph, consume from
|
||||
// remaining, so for remaining[n] > 0
|
||||
if remaining[n] > 0 {
|
||||
remaining[n]-- // remove edge from the graph
|
||||
if remaining[n] == 0 { // if n has no other incoming edges
|
||||
S = append(S, n) // insert n into S
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if graph has edges, eg if any value in rem is > 0
|
||||
for c, in := range remaining {
|
||||
if in > 0 {
|
||||
for n := range g.Adjacency[c] {
|
||||
if remaining[n] > 0 {
|
||||
return nil, false // not a dag!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return L, true
|
||||
}
|
||||
|
||||
func (v *Vertex) Value(key string) (string, bool) {
|
||||
if value, exists := v.data[key]; exists {
|
||||
return value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (v *Vertex) SetValue(key, value string) bool {
|
||||
v.data[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *Graph) GetVerticesKeyValue(key, value string) chan *Vertex {
|
||||
ch := make(chan *Vertex)
|
||||
go func() {
|
||||
for vertex := range g.GetVerticesChan() {
|
||||
if v, exists := vertex.Value(key); exists && v == value {
|
||||
ch <- vertex
|
||||
}
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// return a pointer to the graph a vertex is on
|
||||
func (v *Vertex) GetGraph() *Graph {
|
||||
return v.graph
|
||||
}
|
||||
|
||||
func HeisenbergCount(ch chan *Vertex) int {
|
||||
c := 0
|
||||
for x := range ch {
|
||||
_ = x
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// main kick to start the graph
|
||||
func (g *Graph) Start(wg *sync.WaitGroup, first bool) { // start or continue
|
||||
t, _ := g.TopologicalSort()
|
||||
// TODO: only calculate indegree if `first` is true to save resources
|
||||
indegree := g.InDegree() // compute all of the indegree's
|
||||
for _, v := range Reverse(t) {
|
||||
|
||||
if !v.Type.IsWatching() { // if Watch() is not running...
|
||||
wg.Add(1)
|
||||
// must pass in value to avoid races...
|
||||
// see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/
|
||||
go func(vv *Vertex) {
|
||||
defer wg.Done()
|
||||
vv.Type.Watch()
|
||||
log.Printf("%v[%v]: Exited", vv.GetType(), vv.GetName())
|
||||
}(v)
|
||||
}
|
||||
|
||||
// selective poke: here we reduce the number of initial pokes
|
||||
// to the minimum required to activate every vertex in the
|
||||
// graph, either by direct action, or by getting poked by a
|
||||
// vertex that was previously activated. if we poke each vertex
|
||||
// that has no incoming edges, then we can be sure to reach the
|
||||
// whole graph. Please note: this may mask certain optimization
|
||||
// failures, such as any poke limiting code in Poke() or
|
||||
// BackPoke(). You might want to disable this selective start
|
||||
// when experimenting with and testing those elements.
|
||||
// if we are unpausing (since it's not the first run of this
|
||||
// function) we need to poke to *unpause* every graph vertex,
|
||||
// and not just selectively the subset with no indegree.
|
||||
if (!first) || indegree[v] == 0 {
|
||||
// ensure state is started before continuing on to next vertex
|
||||
for !v.Type.SendEvent(eventStart, true, false) {
|
||||
if DEBUG {
|
||||
// if SendEvent fails, we aren't up yet
|
||||
log.Printf("%v[%v]: Retrying SendEvent(Start)", v.GetType(), v.GetName())
|
||||
// sleep here briefly or otherwise cause
|
||||
// a different goroutine to be scheduled
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) Pause() {
|
||||
t, _ := g.TopologicalSort()
|
||||
for _, v := range t { // squeeze out the events...
|
||||
v.Type.SendEvent(eventPause, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) Exit() {
|
||||
t, _ := g.TopologicalSort()
|
||||
for _, v := range t { // squeeze out the events...
|
||||
// turn off the taps...
|
||||
// XXX: do this by sending an exit signal, and then returning
|
||||
// when we hit the 'default' in the select statement!
|
||||
// XXX: we can do this to quiesce, but it's not necessary now
|
||||
|
||||
v.Type.SendEvent(eventExit, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Graph) SetConvergedCallback(ctimeout int, converged chan bool) {
|
||||
for v := range g.GetVerticesChan() {
|
||||
v.Type.SetConvegedCallback(ctimeout, converged)
|
||||
}
|
||||
}
|
||||
|
||||
// in array function to test *vertices in a slice of *vertices
|
||||
func HasVertex(v *Vertex, haystack []*Vertex) bool {
|
||||
for _, r := range haystack {
|
||||
if v == r {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// reverse a list of vertices
|
||||
func Reverse(vs []*Vertex) []*Vertex {
|
||||
//var out []*Vertex // XXX: golint suggests, but it fails testing
|
||||
out := make([]*Vertex, 0) // empty list
|
||||
l := len(vs)
|
||||
for i := range vs {
|
||||
out = append(out, vs[l-i-1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
104
pgraph/autoedge.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package pgraph represents the internal "pointer graph" that we use.
|
||||
package pgraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
)
|
||||
|
||||
// add edges to the vertex in a graph based on if it matches a uuid list
|
||||
func (g *Graph) addEdgesByMatchingUUIDS(v *Vertex, uuids []resources.ResUUID) []bool {
|
||||
// search for edges and see what matches!
|
||||
var result []bool
|
||||
|
||||
// loop through each uuid, and see if it matches any vertex
|
||||
for _, uuid := range uuids {
|
||||
var found = false
|
||||
// uuid is a ResUUID object
|
||||
for _, vv := range g.GetVertices() { // search
|
||||
if v == vv { // skip self
|
||||
continue
|
||||
}
|
||||
if global.DEBUG {
|
||||
log.Printf("Compile: AutoEdge: Match: %v[%v] with UUID: %v[%v]", vv.Kind(), vv.GetName(), uuid.Kind(), uuid.GetName())
|
||||
}
|
||||
// we must match to an effective UUID for the resource,
|
||||
// that is to say, the name value of a res is a helpful
|
||||
// handle, but it is not necessarily a unique identity!
|
||||
// remember, resources can return multiple UUID's each!
|
||||
if resources.UUIDExistsInUUIDs(uuid, vv.GetUUIDs()) {
|
||||
// add edge from: vv -> v
|
||||
if uuid.Reversed() {
|
||||
txt := fmt.Sprintf("AutoEdge: %v[%v] -> %v[%v]", vv.Kind(), vv.GetName(), v.Kind(), v.GetName())
|
||||
log.Printf("Compile: Adding %v", txt)
|
||||
g.AddEdge(vv, v, NewEdge(txt))
|
||||
} else { // edges go the "normal" way, eg: pkg resource
|
||||
txt := fmt.Sprintf("AutoEdge: %v[%v] -> %v[%v]", v.Kind(), v.GetName(), vv.Kind(), vv.GetName())
|
||||
log.Printf("Compile: Adding %v", txt)
|
||||
g.AddEdge(v, vv, NewEdge(txt))
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, found)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// AutoEdges adds the automatic edges to the graph.
|
||||
func (g *Graph) AutoEdges() {
|
||||
log.Println("Compile: Adding AutoEdges...")
|
||||
for _, v := range g.GetVertices() { // for each vertexes autoedges
|
||||
if !v.Meta().AutoEdge { // is the metaparam true?
|
||||
continue
|
||||
}
|
||||
autoEdgeObj := v.AutoEdges()
|
||||
if autoEdgeObj == nil {
|
||||
log.Printf("%v[%v]: Config: No auto edges were found!", v.Kind(), v.GetName())
|
||||
continue // next vertex
|
||||
}
|
||||
|
||||
for { // while the autoEdgeObj has more uuids to add...
|
||||
uuids := autoEdgeObj.Next() // get some!
|
||||
if uuids == nil {
|
||||
log.Printf("%v[%v]: Config: The auto edge list is empty!", v.Kind(), v.GetName())
|
||||
break // inner loop
|
||||
}
|
||||
if global.DEBUG {
|
||||
log.Println("Compile: AutoEdge: UUIDS:")
|
||||
for i, u := range uuids {
|
||||
log.Printf("Compile: AutoEdge: UUID%d: %v", i, u)
|
||||
}
|
||||
}
|
||||
|
||||
// match and add edges
|
||||
result := g.addEdgesByMatchingUUIDS(v, uuids)
|
||||
|
||||
// report back, and find out if we should continue
|
||||
if !autoEdgeObj.Test(result) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
348
pgraph/autogroup.go
Normal file
@@ -0,0 +1,348 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package pgraph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
)
|
||||
|
||||
// AutoGrouper is the required interface to implement for an autogroup algorithm
|
||||
type AutoGrouper interface {
|
||||
// listed in the order these are typically called in...
|
||||
name() string // friendly identifier
|
||||
init(*Graph) error // only call once
|
||||
vertexNext() (*Vertex, *Vertex, error) // mostly algorithmic
|
||||
vertexCmp(*Vertex, *Vertex) error // can we merge these ?
|
||||
vertexMerge(*Vertex, *Vertex) (*Vertex, error) // vertex merge fn to use
|
||||
edgeMerge(*Edge, *Edge) *Edge // edge merge fn to use
|
||||
vertexTest(bool) (bool, error) // call until false
|
||||
}
|
||||
|
||||
// baseGrouper is the base type for implementing the AutoGrouper interface
|
||||
type baseGrouper struct {
|
||||
graph *Graph // store a pointer to the graph
|
||||
vertices []*Vertex // cached list of vertices
|
||||
i int
|
||||
j int
|
||||
done bool
|
||||
}
|
||||
|
||||
// name provides a friendly name for the logs to see
|
||||
func (ag *baseGrouper) name() string {
|
||||
return "baseGrouper"
|
||||
}
|
||||
|
||||
// init is called only once and before using other AutoGrouper interface methods
|
||||
// the name method is the only exception: call it any time without side effects!
|
||||
func (ag *baseGrouper) init(g *Graph) error {
|
||||
if ag.graph != nil {
|
||||
return fmt.Errorf("The init method has already been called!")
|
||||
}
|
||||
ag.graph = g // pointer
|
||||
ag.vertices = ag.graph.GetVerticesSorted() // cache in deterministic order!
|
||||
ag.i = 0
|
||||
ag.j = 0
|
||||
if len(ag.vertices) == 0 { // empty graph
|
||||
ag.done = true
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// vertexNext is a simple iterator that loops through vertex (pair) combinations
|
||||
// an intelligent algorithm would selectively offer only valid pairs of vertices
|
||||
// these should satisfy logical grouping requirements for the autogroup designs!
|
||||
// the desired algorithms can override, but keep this method as a base iterator!
|
||||
func (ag *baseGrouper) vertexNext() (v1, v2 *Vertex, err error) {
|
||||
// this does a for v... { for w... { return v, w }} but stepwise!
|
||||
l := len(ag.vertices)
|
||||
if ag.i < l {
|
||||
v1 = ag.vertices[ag.i]
|
||||
}
|
||||
if ag.j < l {
|
||||
v2 = ag.vertices[ag.j]
|
||||
}
|
||||
|
||||
// in case the vertex was deleted
|
||||
if !ag.graph.HasVertex(v1) {
|
||||
v1 = nil
|
||||
}
|
||||
if !ag.graph.HasVertex(v2) {
|
||||
v2 = nil
|
||||
}
|
||||
|
||||
// two nested loops...
|
||||
if ag.j < l {
|
||||
ag.j++
|
||||
}
|
||||
if ag.j == l {
|
||||
ag.j = 0
|
||||
if ag.i < l {
|
||||
ag.i++
|
||||
}
|
||||
if ag.i == l {
|
||||
ag.done = true
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (ag *baseGrouper) vertexCmp(v1, v2 *Vertex) error {
|
||||
if v1 == nil || v2 == nil {
|
||||
return fmt.Errorf("Vertex is nil!")
|
||||
}
|
||||
if v1 == v2 { // skip yourself
|
||||
return fmt.Errorf("Vertices are the same!")
|
||||
}
|
||||
if v1.Kind() != v2.Kind() { // we must group similar kinds
|
||||
// TODO: maybe future resources won't need this limitation?
|
||||
return fmt.Errorf("The two resources aren't the same kind!")
|
||||
}
|
||||
// someone doesn't want to group!
|
||||
if !v1.Meta().AutoGroup || !v2.Meta().AutoGroup {
|
||||
return fmt.Errorf("One of the autogroup flags is false!")
|
||||
}
|
||||
if v1.Res.IsGrouped() { // already grouped!
|
||||
return fmt.Errorf("Already grouped!")
|
||||
}
|
||||
if len(v2.Res.GetGroup()) > 0 { // already has children grouped!
|
||||
return fmt.Errorf("Already has groups!")
|
||||
}
|
||||
if !v1.Res.GroupCmp(v2.Res) { // resource groupcmp failed!
|
||||
return fmt.Errorf("The GroupCmp failed!")
|
||||
}
|
||||
return nil // success
|
||||
}
|
||||
|
||||
func (ag *baseGrouper) vertexMerge(v1, v2 *Vertex) (v *Vertex, err error) {
|
||||
// NOTE: it's important to use w.Res instead of w, b/c
|
||||
// the w by itself is the *Vertex obj, not the *Res obj
|
||||
// which is contained within it! They both satisfy the
|
||||
// Res interface, which is why both will compile! :(
|
||||
err = v1.Res.GroupRes(v2.Res) // GroupRes skips stupid groupings
|
||||
return // success or fail, and no need to merge the actual vertices!
|
||||
}
|
||||
|
||||
func (ag *baseGrouper) edgeMerge(e1, e2 *Edge) *Edge {
|
||||
return e1 // noop
|
||||
}
|
||||
|
||||
// vertexTest processes the results of the grouping for the algorithm to know
|
||||
// return an error if something went horribly wrong, and bool false to stop
|
||||
func (ag *baseGrouper) vertexTest(b bool) (bool, error) {
|
||||
// NOTE: this particular baseGrouper version doesn't track what happens
|
||||
// because since we iterate over every pair, we don't care which merge!
|
||||
if ag.done {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// TODO: this algorithm may not be correct in all cases. replace if needed!
|
||||
type nonReachabilityGrouper struct {
|
||||
baseGrouper // "inherit" what we want, and reimplement the rest
|
||||
}
|
||||
|
||||
func (ag *nonReachabilityGrouper) name() string {
|
||||
return "nonReachabilityGrouper"
|
||||
}
|
||||
|
||||
// this algorithm relies on the observation that if there's a path from a to b,
|
||||
// then they *can't* be merged (b/c of the existing dependency) so therefore we
|
||||
// merge anything that *doesn't* satisfy this condition or that of the reverse!
|
||||
func (ag *nonReachabilityGrouper) vertexNext() (v1, v2 *Vertex, err error) {
|
||||
for {
|
||||
v1, v2, err = ag.baseGrouper.vertexNext() // get all iterable pairs
|
||||
if err != nil {
|
||||
log.Fatalf("Error running autoGroup(vertexNext): %v", err)
|
||||
}
|
||||
|
||||
if v1 != v2 { // ignore self cmp early (perf optimization)
|
||||
// if NOT reachable, they're viable...
|
||||
out1 := ag.graph.Reachability(v1, v2)
|
||||
out2 := ag.graph.Reachability(v2, v1)
|
||||
if len(out1) == 0 && len(out2) == 0 {
|
||||
return // return v1 and v2, they're viable
|
||||
}
|
||||
}
|
||||
|
||||
// if we got here, it means we're skipping over this candidate!
|
||||
if ok, err := ag.baseGrouper.vertexTest(false); err != nil {
|
||||
log.Fatalf("Error running autoGroup(vertexTest): %v", err)
|
||||
} else if !ok {
|
||||
return nil, nil, nil // done!
|
||||
}
|
||||
|
||||
// the vertexTest passed, so loop and try with a new pair...
|
||||
}
|
||||
}
|
||||
|
||||
// VertexMerge merges v2 into v1 by reattaching the edges where appropriate,
|
||||
// and then by deleting v2 from the graph. Since more than one edge between two
|
||||
// vertices is not allowed, duplicate edges are merged as well. an edge merge
|
||||
// function can be provided if you'd like to control how you merge the edges!
|
||||
func (g *Graph) VertexMerge(v1, v2 *Vertex, vertexMergeFn func(*Vertex, *Vertex) (*Vertex, error), edgeMergeFn func(*Edge, *Edge) *Edge) error {
|
||||
// methodology
|
||||
// 1) edges between v1 and v2 are removed
|
||||
//Loop:
|
||||
for k1 := range g.Adjacency {
|
||||
for k2 := range g.Adjacency[k1] {
|
||||
// v1 -> v2 || v2 -> v1
|
||||
if (k1 == v1 && k2 == v2) || (k1 == v2 && k2 == v1) {
|
||||
delete(g.Adjacency[k1], k2) // delete map & edge
|
||||
// NOTE: if we assume this is a DAG, then we can
|
||||
// assume only v1 -> v2 OR v2 -> v1 exists, and
|
||||
// we can break out of these loops immediately!
|
||||
//break Loop
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) edges that point towards v2 from X now point to v1 from X (no dupes)
|
||||
for _, x := range g.IncomingGraphEdges(v2) { // all to vertex v (??? -> v)
|
||||
e := g.Adjacency[x][v2] // previous edge
|
||||
r := g.Reachability(x, v1)
|
||||
// merge e with ex := g.Adjacency[x][v1] if it exists!
|
||||
if ex, exists := g.Adjacency[x][v1]; exists && edgeMergeFn != nil && len(r) == 0 {
|
||||
e = edgeMergeFn(e, ex)
|
||||
}
|
||||
if len(r) == 0 { // if not reachable, add it
|
||||
g.AddEdge(x, v1, e) // overwrite edge
|
||||
} else if edgeMergeFn != nil { // reachable, merge e through...
|
||||
prev := x // initial condition
|
||||
for i, next := range r {
|
||||
if i == 0 {
|
||||
// next == prev, therefore skip
|
||||
continue
|
||||
}
|
||||
// this edge is from: prev, to: next
|
||||
ex, _ := g.Adjacency[prev][next] // get
|
||||
ex = edgeMergeFn(ex, e)
|
||||
g.Adjacency[prev][next] = ex // set
|
||||
prev = next
|
||||
}
|
||||
}
|
||||
delete(g.Adjacency[x], v2) // delete old edge
|
||||
}
|
||||
|
||||
// 3) edges that point from v2 to X now point from v1 to X (no dupes)
|
||||
for _, x := range g.OutgoingGraphEdges(v2) { // all from vertex v (v -> ???)
|
||||
e := g.Adjacency[v2][x] // previous edge
|
||||
r := g.Reachability(v1, x)
|
||||
// merge e with ex := g.Adjacency[v1][x] if it exists!
|
||||
if ex, exists := g.Adjacency[v1][x]; exists && edgeMergeFn != nil && len(r) == 0 {
|
||||
e = edgeMergeFn(e, ex)
|
||||
}
|
||||
if len(r) == 0 {
|
||||
g.AddEdge(v1, x, e) // overwrite edge
|
||||
} else if edgeMergeFn != nil { // reachable, merge e through...
|
||||
prev := v1 // initial condition
|
||||
for i, next := range r {
|
||||
if i == 0 {
|
||||
// next == prev, therefore skip
|
||||
continue
|
||||
}
|
||||
// this edge is from: prev, to: next
|
||||
ex, _ := g.Adjacency[prev][next]
|
||||
ex = edgeMergeFn(ex, e)
|
||||
g.Adjacency[prev][next] = ex
|
||||
prev = next
|
||||
}
|
||||
}
|
||||
delete(g.Adjacency[v2], x)
|
||||
}
|
||||
|
||||
// 4) merge and then remove the (now merged/grouped) vertex
|
||||
if vertexMergeFn != nil { // run vertex merge function
|
||||
if v, err := vertexMergeFn(v1, v2); err != nil {
|
||||
return err
|
||||
} else if v != nil { // replace v1 with the "merged" version...
|
||||
v1 = v // XXX: will this replace v1 the way we want?
|
||||
}
|
||||
}
|
||||
g.DeleteVertex(v2) // remove grouped vertex
|
||||
|
||||
// 5) creation of a cyclic graph should throw an error
|
||||
if _, dag := g.TopologicalSort(); !dag { // am i a dag or not?
|
||||
return fmt.Errorf("Graph is not a dag!")
|
||||
}
|
||||
return nil // success
|
||||
}
|
||||
|
||||
// autoGroup is the mechanical auto group "runner" that runs the interface spec
|
||||
func (g *Graph) autoGroup(ag AutoGrouper) chan string {
|
||||
strch := make(chan string) // output log messages here
|
||||
go func(strch chan string) {
|
||||
strch <- fmt.Sprintf("Compile: Grouping: Algorithm: %v...", ag.name())
|
||||
if err := ag.init(g); err != nil {
|
||||
log.Fatalf("Error running autoGroup(init): %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
var v, w *Vertex
|
||||
v, w, err := ag.vertexNext() // get pair to compare
|
||||
if err != nil {
|
||||
log.Fatalf("Error running autoGroup(vertexNext): %v", err)
|
||||
}
|
||||
merged := false
|
||||
// save names since they change during the runs
|
||||
vStr := fmt.Sprintf("%s", v) // valid even if it is nil
|
||||
wStr := fmt.Sprintf("%s", w)
|
||||
|
||||
if err := ag.vertexCmp(v, w); err != nil { // cmp ?
|
||||
if global.DEBUG {
|
||||
strch <- fmt.Sprintf("Compile: Grouping: !GroupCmp for: %s into %s", wStr, vStr)
|
||||
}
|
||||
|
||||
// remove grouped vertex and merge edges (res is safe)
|
||||
} else if err := g.VertexMerge(v, w, ag.vertexMerge, ag.edgeMerge); err != nil { // merge...
|
||||
strch <- fmt.Sprintf("Compile: Grouping: !VertexMerge for: %s into %s", wStr, vStr)
|
||||
|
||||
} else { // success!
|
||||
strch <- fmt.Sprintf("Compile: Grouping: Success for: %s into %s", wStr, vStr)
|
||||
merged = true // woo
|
||||
}
|
||||
|
||||
// did these get used?
|
||||
if ok, err := ag.vertexTest(merged); err != nil {
|
||||
log.Fatalf("Error running autoGroup(vertexTest): %v", err)
|
||||
} else if !ok {
|
||||
break // done!
|
||||
}
|
||||
}
|
||||
|
||||
close(strch)
|
||||
return
|
||||
}(strch) // call function
|
||||
return strch
|
||||
}
|
||||
|
||||
// AutoGroup runs the auto grouping on the graph and prints out log messages
|
||||
func (g *Graph) AutoGroup() {
|
||||
// receive log messages from channel...
|
||||
// this allows test cases to avoid printing them when they're unwanted!
|
||||
// TODO: this algorithm may not be correct in all cases. replace if needed!
|
||||
for str := range g.autoGroup(&nonReachabilityGrouper{}) {
|
||||
log.Println(str)
|
||||
}
|
||||
}
|
||||
968
pgraph/pgraph.go
Normal file
@@ -0,0 +1,968 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package pgraph represents the internal "pointer graph" that we use.
|
||||
package pgraph
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/converger"
|
||||
"github.com/purpleidea/mgmt/event"
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
"github.com/purpleidea/mgmt/resources"
|
||||
)
|
||||
|
||||
//go:generate stringer -type=graphState -output=graphstate_stringer.go
|
||||
type graphState int
|
||||
|
||||
const (
|
||||
graphStateNil graphState = iota
|
||||
graphStateStarting
|
||||
graphStateStarted
|
||||
graphStatePausing
|
||||
graphStatePaused
|
||||
)
|
||||
|
||||
// Graph is the graph structure in this library.
|
||||
// The graph abstract data type (ADT) is defined as follows:
|
||||
// * the directed graph arrows point from left to right ( -> )
|
||||
// * the arrows point away from their dependencies (eg: arrows mean "before")
|
||||
// * IOW, you might see package -> file -> service (where package runs first)
|
||||
// * This is also the direction that the notify should happen in...
|
||||
type Graph struct {
|
||||
Name string
|
||||
Adjacency map[*Vertex]map[*Vertex]*Edge // *Vertex -> *Vertex (edge)
|
||||
state graphState
|
||||
mutex sync.Mutex // used when modifying graph State variable
|
||||
}
|
||||
|
||||
// Vertex is the primary vertex struct in this library.
|
||||
type Vertex struct {
|
||||
resources.Res // anonymous field
|
||||
timestamp int64 // last updated timestamp ?
|
||||
}
|
||||
|
||||
// Edge is the primary edge struct in this library.
|
||||
type Edge struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
// NewGraph builds a new graph.
|
||||
func NewGraph(name string) *Graph {
|
||||
return &Graph{
|
||||
Name: name,
|
||||
Adjacency: make(map[*Vertex]map[*Vertex]*Edge),
|
||||
state: graphStateNil,
|
||||
}
|
||||
}
|
||||
|
||||
// NewVertex returns a new graph vertex struct with a contained resource.
|
||||
func NewVertex(r resources.Res) *Vertex {
|
||||
return &Vertex{
|
||||
Res: r,
|
||||
}
|
||||
}
|
||||
|
||||
// NewEdge returns a new graph edge struct.
|
||||
func NewEdge(name string) *Edge {
|
||||
return &Edge{
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
|
||||
// Copy makes a copy of the graph struct
|
||||
func (g *Graph) Copy() *Graph {
|
||||
newGraph := &Graph{
|
||||
Name: g.Name,
|
||||
Adjacency: make(map[*Vertex]map[*Vertex]*Edge, len(g.Adjacency)),
|
||||
state: g.state,
|
||||
}
|
||||
for k, v := range g.Adjacency {
|
||||
newGraph.Adjacency[k] = v // copy
|
||||
}
|
||||
return newGraph
|
||||
}
|
||||
|
||||
// GetName returns the name of the graph.
|
||||
func (g *Graph) GetName() string {
|
||||
return g.Name
|
||||
}
|
||||
|
||||
// SetName sets the name of the graph.
|
||||
func (g *Graph) SetName(name string) {
|
||||
g.Name = name
|
||||
}
|
||||
|
||||
// getState returns the state of the graph. This state is used for optimizing
|
||||
// certain algorithms by knowing what part of processing the graph is currently
|
||||
// undergoing.
|
||||
func (g *Graph) getState() graphState {
|
||||
//g.mutex.Lock()
|
||||
//defer g.mutex.Unlock()
|
||||
return g.state
|
||||
}
|
||||
|
||||
// setState sets the graph state and returns the previous state.
|
||||
func (g *Graph) setState(state graphState) graphState {
|
||||
g.mutex.Lock()
|
||||
defer g.mutex.Unlock()
|
||||
prev := g.getState()
|
||||
g.state = state
|
||||
return prev
|
||||
}
|
||||
|
||||
// AddVertex uses variadic input to add all listed vertices to the graph
|
||||
func (g *Graph) AddVertex(xv ...*Vertex) {
|
||||
for _, v := range xv {
|
||||
if _, exists := g.Adjacency[v]; !exists {
|
||||
g.Adjacency[v] = make(map[*Vertex]*Edge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteVertex deletes a particular vertex from the graph.
|
||||
func (g *Graph) DeleteVertex(v *Vertex) {
|
||||
delete(g.Adjacency, v)
|
||||
for k := range g.Adjacency {
|
||||
delete(g.Adjacency[k], v)
|
||||
}
|
||||
}
|
||||
|
||||
// AddEdge adds a directed edge to the graph from v1 to v2.
|
||||
func (g *Graph) AddEdge(v1, v2 *Vertex, e *Edge) {
|
||||
// NOTE: this doesn't allow more than one edge between two vertexes...
|
||||
g.AddVertex(v1, v2) // supports adding N vertices now
|
||||
// TODO: check if an edge exists to avoid overwriting it!
|
||||
// NOTE: VertexMerge() depends on overwriting it at the moment...
|
||||
g.Adjacency[v1][v2] = e
|
||||
}
|
||||
|
||||
// GetVertexMatch searches for an equivalent resource in the graph and returns
|
||||
// the vertex it is found in, or nil if not found.
|
||||
func (g *Graph) GetVertexMatch(obj resources.Res) *Vertex {
|
||||
for k := range g.Adjacency {
|
||||
if k.Res.Compare(obj) {
|
||||
return k
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasVertex returns if the input vertex exists in the graph.
|
||||
func (g *Graph) HasVertex(v *Vertex) bool {
|
||||
if _, exists := g.Adjacency[v]; exists {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NumVertices returns the number of vertices in the graph.
|
||||
func (g *Graph) NumVertices() int {
|
||||
return len(g.Adjacency)
|
||||
}
|
||||
|
||||
// NumEdges returns the number of edges in the graph.
|
||||
func (g *Graph) NumEdges() int {
|
||||
count := 0
|
||||
for k := range g.Adjacency {
|
||||
count += len(g.Adjacency[k])
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// GetVertices returns a randomly sorted slice of all vertices in the graph
|
||||
// The order is random, because the map implementation is intentionally so!
|
||||
func (g *Graph) GetVertices() []*Vertex {
|
||||
var vertices []*Vertex
|
||||
for k := range g.Adjacency {
|
||||
vertices = append(vertices, k)
|
||||
}
|
||||
return vertices
|
||||
}
|
||||
|
||||
// GetVerticesChan returns a channel of all vertices in the graph.
|
||||
func (g *Graph) GetVerticesChan() chan *Vertex {
|
||||
ch := make(chan *Vertex)
|
||||
go func(ch chan *Vertex) {
|
||||
for k := range g.Adjacency {
|
||||
ch <- k
|
||||
}
|
||||
close(ch)
|
||||
}(ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
// VertexSlice is a linear list of vertices. It can be sorted.
|
||||
type VertexSlice []*Vertex
|
||||
|
||||
func (vs VertexSlice) Len() int { return len(vs) }
|
||||
func (vs VertexSlice) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
||||
func (vs VertexSlice) Less(i, j int) bool { return vs[i].String() < vs[j].String() }
|
||||
|
||||
// GetVerticesSorted returns a sorted slice of all vertices in the graph
|
||||
// The order is sorted by String() to avoid the non-determinism in the map type
|
||||
func (g *Graph) GetVerticesSorted() []*Vertex {
|
||||
var vertices []*Vertex
|
||||
for k := range g.Adjacency {
|
||||
vertices = append(vertices, k)
|
||||
}
|
||||
sort.Sort(VertexSlice(vertices)) // add determinism
|
||||
return vertices
|
||||
}
|
||||
|
||||
// String makes the graph pretty print.
|
||||
func (g *Graph) String() string {
|
||||
return fmt.Sprintf("Vertices(%d), Edges(%d)", g.NumVertices(), g.NumEdges())
|
||||
}
|
||||
|
||||
// String returns the canonical form for a vertex
|
||||
func (v *Vertex) String() string {
|
||||
return fmt.Sprintf("%s[%s]", v.Res.Kind(), v.Res.GetName())
|
||||
}
|
||||
|
||||
// Graphviz outputs the graph in graphviz format.
|
||||
// https://en.wikipedia.org/wiki/DOT_%28graph_description_language%29
|
||||
func (g *Graph) Graphviz() (out string) {
|
||||
//digraph g {
|
||||
// label="hello world";
|
||||
// node [shape=box];
|
||||
// A [label="A"];
|
||||
// B [label="B"];
|
||||
// C [label="C"];
|
||||
// D [label="D"];
|
||||
// E [label="E"];
|
||||
// A -> B [label=f];
|
||||
// B -> C [label=g];
|
||||
// D -> E [label=h];
|
||||
//}
|
||||
out += fmt.Sprintf("digraph %v {\n", g.GetName())
|
||||
out += fmt.Sprintf("\tlabel=\"%v\";\n", g.GetName())
|
||||
//out += "\tnode [shape=box];\n"
|
||||
str := ""
|
||||
for i := range g.Adjacency { // reverse paths
|
||||
out += fmt.Sprintf("\t%v [label=\"%v[%v]\"];\n", i.GetName(), i.Kind(), i.GetName())
|
||||
for j := range g.Adjacency[i] {
|
||||
k := g.Adjacency[i][j]
|
||||
// use str for clearer output ordering
|
||||
str += fmt.Sprintf("\t%v -> %v [label=%v];\n", i.GetName(), j.GetName(), k.Name)
|
||||
}
|
||||
}
|
||||
out += str
|
||||
out += "}\n"
|
||||
return
|
||||
}
|
||||
|
||||
// ExecGraphviz writes out the graphviz data and runs the correct graphviz
|
||||
// filter command.
|
||||
func (g *Graph) ExecGraphviz(program, filename string) error {
|
||||
|
||||
switch program {
|
||||
case "dot", "neato", "twopi", "circo", "fdp":
|
||||
default:
|
||||
return errors.New("Invalid graphviz program selected!")
|
||||
}
|
||||
|
||||
if filename == "" {
|
||||
return errors.New("No filename given!")
|
||||
}
|
||||
|
||||
// run as a normal user if possible when run with sudo
|
||||
uid, err1 := strconv.Atoi(os.Getenv("SUDO_UID"))
|
||||
gid, err2 := strconv.Atoi(os.Getenv("SUDO_GID"))
|
||||
|
||||
err := ioutil.WriteFile(filename, []byte(g.Graphviz()), 0644)
|
||||
if err != nil {
|
||||
return errors.New("Error writing to filename!")
|
||||
}
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
if err := os.Chown(filename, uid, gid); err != nil {
|
||||
return errors.New("Error changing file owner!")
|
||||
}
|
||||
}
|
||||
|
||||
path, err := exec.LookPath(program)
|
||||
if err != nil {
|
||||
return errors.New("Graphviz is missing!")
|
||||
}
|
||||
|
||||
out := fmt.Sprintf("%v.png", filename)
|
||||
cmd := exec.Command(path, "-Tpng", fmt.Sprintf("-o%v", out), filename)
|
||||
|
||||
if err1 == nil && err2 == nil {
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{}
|
||||
cmd.SysProcAttr.Credential = &syscall.Credential{
|
||||
Uid: uint32(uid),
|
||||
Gid: uint32(gid),
|
||||
}
|
||||
}
|
||||
_, err = cmd.Output()
|
||||
if err != nil {
|
||||
return errors.New("Error writing to image!")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncomingGraphEdges returns an array (slice) of all directed vertices to
|
||||
// vertex v (??? -> v). OKTimestamp should probably use this.
|
||||
func (g *Graph) IncomingGraphEdges(v *Vertex) []*Vertex {
|
||||
// TODO: we might be able to implement this differently by reversing
|
||||
// the Adjacency graph and then looping through it again...
|
||||
var s []*Vertex
|
||||
for k := range g.Adjacency { // reverse paths
|
||||
for w := range g.Adjacency[k] {
|
||||
if w == v {
|
||||
s = append(s, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// OutgoingGraphEdges returns an array (slice) of all vertices that vertex v
|
||||
// points to (v -> ???). Poke should probably use this.
|
||||
func (g *Graph) OutgoingGraphEdges(v *Vertex) []*Vertex {
|
||||
var s []*Vertex
|
||||
for k := range g.Adjacency[v] { // forward paths
|
||||
s = append(s, k)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GraphEdges returns an array (slice) of all vertices that connect to vertex v.
|
||||
// This is the union of IncomingGraphEdges and OutgoingGraphEdges.
|
||||
func (g *Graph) GraphEdges(v *Vertex) []*Vertex {
|
||||
var s []*Vertex
|
||||
s = append(s, g.IncomingGraphEdges(v)...)
|
||||
s = append(s, g.OutgoingGraphEdges(v)...)
|
||||
return s
|
||||
}
|
||||
|
||||
// DFS returns a depth first search for the graph, starting at the input vertex.
|
||||
func (g *Graph) DFS(start *Vertex) []*Vertex {
|
||||
var d []*Vertex // discovered
|
||||
var s []*Vertex // stack
|
||||
if _, exists := g.Adjacency[start]; !exists {
|
||||
return nil // TODO: error
|
||||
}
|
||||
v := start
|
||||
s = append(s, v)
|
||||
for len(s) > 0 {
|
||||
v, s = s[len(s)-1], s[:len(s)-1] // s.pop()
|
||||
|
||||
if !VertexContains(v, d) { // if not discovered
|
||||
d = append(d, v) // label as discovered
|
||||
|
||||
for _, w := range g.GraphEdges(v) {
|
||||
s = append(s, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// FilterGraph builds a new graph containing only vertices from the list.
|
||||
func (g *Graph) FilterGraph(name string, vertices []*Vertex) *Graph {
|
||||
newgraph := NewGraph(name)
|
||||
for k1, x := range g.Adjacency {
|
||||
for k2, e := range x {
|
||||
//log.Printf("Filter: %v -> %v # %v", k1.Name, k2.Name, e.Name)
|
||||
if VertexContains(k1, vertices) || VertexContains(k2, vertices) {
|
||||
newgraph.AddEdge(k1, k2, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newgraph
|
||||
}
|
||||
|
||||
// GetDisconnectedGraphs returns a channel containing the N disconnected graphs
|
||||
// in our main graph. We can then process each of these in parallel.
|
||||
func (g *Graph) GetDisconnectedGraphs() chan *Graph {
|
||||
ch := make(chan *Graph)
|
||||
go func() {
|
||||
var start *Vertex
|
||||
var d []*Vertex // discovered
|
||||
c := g.NumVertices()
|
||||
for len(d) < c {
|
||||
|
||||
// get an undiscovered vertex to start from
|
||||
for _, s := range g.GetVertices() {
|
||||
if !VertexContains(s, d) {
|
||||
start = s
|
||||
}
|
||||
}
|
||||
|
||||
// dfs through the graph
|
||||
dfs := g.DFS(start)
|
||||
// filter all the collected elements into a new graph
|
||||
newgraph := g.FilterGraph(g.Name, dfs)
|
||||
|
||||
// add number of elements found to found variable
|
||||
d = append(d, dfs...) // extend
|
||||
|
||||
// return this new graph to the channel
|
||||
ch <- newgraph
|
||||
|
||||
// if we've found all the elements, then we're done
|
||||
// otherwise loop through to continue...
|
||||
}
|
||||
close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// InDegree returns the count of vertices that point to me in one big lookup map.
|
||||
func (g *Graph) InDegree() map[*Vertex]int {
|
||||
result := make(map[*Vertex]int)
|
||||
for k := range g.Adjacency {
|
||||
result[k] = 0 // initialize
|
||||
}
|
||||
|
||||
for k := range g.Adjacency {
|
||||
for z := range g.Adjacency[k] {
|
||||
result[z]++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// OutDegree returns the count of vertices that point away in one big lookup map.
|
||||
func (g *Graph) OutDegree() map[*Vertex]int {
|
||||
result := make(map[*Vertex]int)
|
||||
|
||||
for k := range g.Adjacency {
|
||||
result[k] = 0 // initialize
|
||||
for range g.Adjacency[k] {
|
||||
result[k]++
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TopologicalSort returns the sort of graph vertices in that order.
|
||||
// based on descriptions and code from wikipedia and rosetta code
|
||||
// TODO: add memoization, and cache invalidation to speed this up :)
|
||||
func (g *Graph) TopologicalSort() (result []*Vertex, ok bool) { // kahn's algorithm
|
||||
var L []*Vertex // empty list that will contain the sorted elements
|
||||
var S []*Vertex // set of all nodes with no incoming edges
|
||||
remaining := make(map[*Vertex]int) // amount of edges remaining
|
||||
|
||||
for v, d := range g.InDegree() {
|
||||
if d == 0 {
|
||||
// accumulate set of all nodes with no incoming edges
|
||||
S = append(S, v)
|
||||
} else {
|
||||
// initialize remaining edge count from indegree
|
||||
remaining[v] = d
|
||||
}
|
||||
}
|
||||
|
||||
for len(S) > 0 {
|
||||
last := len(S) - 1 // remove a node v from S
|
||||
v := S[last]
|
||||
S = S[:last]
|
||||
L = append(L, v) // add v to tail of L
|
||||
for n := range g.Adjacency[v] {
|
||||
// for each node n remaining in the graph, consume from
|
||||
// remaining, so for remaining[n] > 0
|
||||
if remaining[n] > 0 {
|
||||
remaining[n]-- // remove edge from the graph
|
||||
if remaining[n] == 0 { // if n has no other incoming edges
|
||||
S = append(S, n) // insert n into S
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if graph has edges, eg if any value in rem is > 0
|
||||
for c, in := range remaining {
|
||||
if in > 0 {
|
||||
for n := range g.Adjacency[c] {
|
||||
if remaining[n] > 0 {
|
||||
return nil, false // not a dag!
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return L, true
|
||||
}
|
||||
|
||||
// Reachability finds the shortest path in a DAG from a to b, and returns the
|
||||
// slice of vertices that matched this particular path including both a and b.
|
||||
// It returns nil if a or b is nil, and returns empty list if no path is found.
|
||||
// Since there could be more than one possible result for this operation, we
|
||||
// arbitrarily choose one of the shortest possible. As a result, this should
|
||||
// actually return a tree if we cared about correctness.
|
||||
// This operates by a recursive algorithm; a more efficient version is likely.
|
||||
// If you don't give this function a DAG, you might cause infinite recursion!
|
||||
func (g *Graph) Reachability(a, b *Vertex) []*Vertex {
|
||||
if a == nil || b == nil {
|
||||
return nil
|
||||
}
|
||||
vertices := g.OutgoingGraphEdges(a) // what points away from a ?
|
||||
if len(vertices) == 0 {
|
||||
return []*Vertex{} // nope
|
||||
}
|
||||
if VertexContains(b, vertices) {
|
||||
return []*Vertex{a, b} // found
|
||||
}
|
||||
// TODO: parallelize this with go routines?
|
||||
var collected = make([][]*Vertex, len(vertices))
|
||||
pick := -1
|
||||
for i, v := range vertices {
|
||||
collected[i] = g.Reachability(v, b) // find b by recursion
|
||||
if l := len(collected[i]); l > 0 {
|
||||
// pick shortest path
|
||||
// TODO: technically i should return a tree
|
||||
if pick < 0 || l < len(collected[pick]) {
|
||||
pick = i
|
||||
}
|
||||
}
|
||||
}
|
||||
if pick < 0 {
|
||||
return []*Vertex{} // nope
|
||||
}
|
||||
result := []*Vertex{a} // tack on a
|
||||
result = append(result, collected[pick]...)
|
||||
return result
|
||||
}
|
||||
|
||||
// GetTimestamp returns the timestamp of a vertex
|
||||
func (v *Vertex) GetTimestamp() int64 {
|
||||
return v.timestamp
|
||||
}
|
||||
|
||||
// UpdateTimestamp updates the timestamp on a vertex and returns the new value
|
||||
func (v *Vertex) UpdateTimestamp() int64 {
|
||||
v.timestamp = time.Now().UnixNano() // update
|
||||
return v.timestamp
|
||||
}
|
||||
|
||||
// OKTimestamp returns true if this element can run right now?
|
||||
func (g *Graph) OKTimestamp(v *Vertex) bool {
|
||||
// these are all the vertices pointing TO v, eg: ??? -> v
|
||||
for _, n := range g.IncomingGraphEdges(v) {
|
||||
// if the vertex has a greater timestamp than any pre-req (n)
|
||||
// then we can't run right now...
|
||||
// if they're equal (eg: on init of 0) then we also can't run
|
||||
// b/c we should let our pre-req's go first...
|
||||
x, y := v.GetTimestamp(), n.GetTimestamp()
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: OKTimestamp: (%v) >= %v[%v](%v): !%v", v.Kind(), v.GetName(), x, n.Kind(), n.GetName(), y, x >= y)
|
||||
}
|
||||
if x >= y {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Poke notifies nodes after me in the dependency graph that they need refreshing...
|
||||
// NOTE: this assumes that this can never fail or need to be rescheduled
|
||||
func (g *Graph) Poke(v *Vertex, activity bool) {
|
||||
// these are all the vertices pointing AWAY FROM v, eg: v -> ???
|
||||
for _, n := range g.OutgoingGraphEdges(v) {
|
||||
// XXX: if we're in state event and haven't been cancelled by
|
||||
// apply, then we can cancel a poke to a child, right? XXX
|
||||
// XXX: if n.Res.getState() != resources.ResStateEvent { // is this correct?
|
||||
if true { // XXX
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: Poke: %v[%v]", v.Kind(), v.GetName(), n.Kind(), n.GetName())
|
||||
}
|
||||
n.SendEvent(event.EventPoke, false, activity) // XXX: can this be switched to sync?
|
||||
} else {
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: Poke: %v[%v]: Skipped!", v.Kind(), v.GetName(), n.Kind(), n.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BackPoke pokes the pre-requisites that are stale and need to run before I can run.
|
||||
func (g *Graph) BackPoke(v *Vertex) {
|
||||
// these are all the vertices pointing TO v, eg: ??? -> v
|
||||
for _, n := range g.IncomingGraphEdges(v) {
|
||||
x, y, s := v.GetTimestamp(), n.GetTimestamp(), n.Res.GetState()
|
||||
// if the parent timestamp needs poking AND it's not in state
|
||||
// ResStateEvent, then poke it. If the parent is in ResStateEvent it
|
||||
// means that an event is pending, so we'll be expecting a poke
|
||||
// back soon, so we can safely discard the extra parent poke...
|
||||
// TODO: implement a stateLT (less than) to tell if something
|
||||
// happens earlier in the state cycle and that doesn't wrap nil
|
||||
if x >= y && (s != resources.ResStateEvent && s != resources.ResStateCheckApply) {
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: BackPoke: %v[%v]", v.Kind(), v.GetName(), n.Kind(), n.GetName())
|
||||
}
|
||||
n.SendEvent(event.EventBackPoke, false, false) // XXX: can this be switched to sync?
|
||||
} else {
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: BackPoke: %v[%v]: Skipped!", v.Kind(), v.GetName(), n.Kind(), n.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process is the primary function to execute for a particular vertex in the graph.
|
||||
func (g *Graph) Process(v *Vertex) error {
|
||||
obj := v.Res
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: Process()", obj.Kind(), obj.GetName())
|
||||
}
|
||||
obj.SetState(resources.ResStateEvent)
|
||||
var ok = true
|
||||
var apply = false // did we run an apply?
|
||||
// is it okay to run dependency wise right now?
|
||||
// if not, that's okay because when the dependency runs, it will poke
|
||||
// us back and we will run if needed then!
|
||||
if g.OKTimestamp(v) {
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: OKTimestamp(%v)", obj.Kind(), obj.GetName(), v.GetTimestamp())
|
||||
}
|
||||
|
||||
obj.SetState(resources.ResStateCheckApply)
|
||||
// if this fails, don't UpdateTimestamp()
|
||||
checkok, err := obj.CheckApply(!obj.Meta().Noop)
|
||||
if checkok && err != nil { // should never return this way
|
||||
log.Fatalf("%v[%v]: CheckApply(): %t, %+v", obj.Kind(), obj.GetName(), checkok, err)
|
||||
}
|
||||
if global.DEBUG {
|
||||
log.Printf("%v[%v]: CheckApply(): %t, %v", obj.Kind(), obj.GetName(), checkok, err)
|
||||
}
|
||||
|
||||
if !checkok { // if state *was* not ok, we had to have apply'ed
|
||||
if err != nil { // error during check or apply
|
||||
ok = false
|
||||
} else {
|
||||
apply = true
|
||||
}
|
||||
}
|
||||
|
||||
// when noop is true we always want to update timestamp
|
||||
if obj.Meta().Noop && err == nil {
|
||||
ok = true
|
||||
}
|
||||
|
||||
if ok {
|
||||
// update this timestamp *before* we poke or the poked
|
||||
// nodes might fail due to having a too old timestamp!
|
||||
v.UpdateTimestamp() // this was touched...
|
||||
obj.SetState(resources.ResStatePoking) // can't cancel parent poke
|
||||
g.Poke(v, apply)
|
||||
}
|
||||
// poke at our pre-req's instead since they need to refresh/run...
|
||||
return err
|
||||
}
|
||||
// else... only poke at the pre-req's that need to run
|
||||
go g.BackPoke(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SentinelErr is a sentinal as an error type that wraps an arbitrary error.
|
||||
type SentinelErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// Error is the required method to fulfill the error type.
|
||||
func (obj *SentinelErr) Error() string {
|
||||
return obj.err.Error()
|
||||
}
|
||||
|
||||
// Worker is the common run frontend of the vertex. It handles all of the retry
|
||||
// and retry delay common code, and ultimately returns the final status of this
|
||||
// vertex execution.
|
||||
func (g *Graph) Worker(v *Vertex) error {
|
||||
// listen for chan events from Watch() and run
|
||||
// the Process() function when they're received
|
||||
// this avoids us having to pass the data into
|
||||
// the Watch() function about which graph it is
|
||||
// running on, which isolates things nicely...
|
||||
obj := v.Res
|
||||
chanProcess := make(chan event.Event)
|
||||
go func() {
|
||||
running := false
|
||||
var timer = time.NewTimer(time.Duration(math.MaxInt64)) // longest duration
|
||||
if !timer.Stop() {
|
||||
<-timer.C // unnecessary, shouldn't happen
|
||||
}
|
||||
var delay = time.Duration(v.Meta().Delay) * time.Millisecond
|
||||
var retry = v.Meta().Retry // number of tries left, -1 for infinite
|
||||
var saved event.Event
|
||||
Loop:
|
||||
for {
|
||||
// this has to be synchronous, because otherwise the Res
|
||||
// event loop will keep running and change state,
|
||||
// causing the converged timeout to fire!
|
||||
select {
|
||||
case event, ok := <-chanProcess: // must use like this
|
||||
if running && ok {
|
||||
// we got an event that wasn't a close,
|
||||
// while we were waiting for the timer!
|
||||
// if this happens, it might be a bug:(
|
||||
log.Fatalf("%v[%v]: Worker: Unexpected event: %+v", v.Kind(), v.GetName(), event)
|
||||
}
|
||||
if !ok { // chanProcess closed, let's exit
|
||||
break Loop // no event, so no ack!
|
||||
}
|
||||
|
||||
// the above mentioned synchronous part, is the
|
||||
// running of this function, paired with an ack.
|
||||
if e := g.Process(v); e != nil {
|
||||
saved = event
|
||||
log.Printf("%v[%v]: CheckApply errored: %v", v.Kind(), v.GetName(), e)
|
||||
if retry == 0 {
|
||||
// wrap the error in the sentinel
|
||||
event.ACKNACK(&SentinelErr{e}) // fail the Watch()
|
||||
break Loop
|
||||
}
|
||||
if retry > 0 { // don't decrement the -1
|
||||
retry--
|
||||
}
|
||||
log.Printf("%v[%v]: CheckApply: Retrying after %.4f seconds (%d left)", v.Kind(), v.GetName(), delay.Seconds(), retry)
|
||||
// start the timer...
|
||||
timer.Reset(delay)
|
||||
running = true
|
||||
continue
|
||||
}
|
||||
retry = v.Meta().Retry // reset on success
|
||||
event.ACK() // sync
|
||||
|
||||
case <-timer.C:
|
||||
if !timer.Stop() {
|
||||
//<-timer.C // blocks, docs are wrong!
|
||||
}
|
||||
running = false
|
||||
log.Printf("%s[%s]: CheckApply delay expired!", v.Kind(), v.GetName())
|
||||
// re-send this failed event, to trigger a CheckApply()
|
||||
go func() { chanProcess <- saved }()
|
||||
// TODO: should we send a fake event instead?
|
||||
//saved = nil
|
||||
}
|
||||
}
|
||||
}()
|
||||
var err error // propagate the error up (this is a permanent BAD error!)
|
||||
// the watch delay runs inside of the Watch resource loop, so that it
|
||||
// can still process signals and exit if needed. It shouldn't run any
|
||||
// resource specific code since this is supposed to be a retry delay.
|
||||
// NOTE: we're using the same retry and delay metaparams that CheckApply
|
||||
// uses. This is for practicality. We can separate them later if needed!
|
||||
var watchDelay time.Duration
|
||||
var watchRetry = v.Meta().Retry // number of tries left, -1 for infinite
|
||||
// watch blocks until it ends, & errors to retry
|
||||
for {
|
||||
// TODO: do we have to stop the converged-timeout when in this block (perhaps we're in the delay block!)
|
||||
// TODO: should we setup/manage some of the converged timeout stuff in here anyways?
|
||||
|
||||
// if a retry-delay was requested, wait, but don't block our events!
|
||||
if watchDelay > 0 {
|
||||
//var pendingSendEvent bool
|
||||
timer := time.NewTimer(watchDelay)
|
||||
Loop:
|
||||
for {
|
||||
select {
|
||||
case <-timer.C: // the wait is over
|
||||
break Loop // critical
|
||||
|
||||
// TODO: resources could have a separate exit channel to avoid this complexity!?
|
||||
case event := <-obj.Events():
|
||||
// NOTE: this code should match the similar Res code!
|
||||
//cuuid.SetConverged(false) // TODO ?
|
||||
if exit, send := obj.ReadEvent(&event); exit {
|
||||
return nil // exit
|
||||
} else if send {
|
||||
// if we dive down this rabbit hole, our
|
||||
// timer.C won't get seen until we get out!
|
||||
// in this situation, the Watch() is blocked
|
||||
// from performing until CheckApply returns
|
||||
// successfully, or errors out. This isn't
|
||||
// so bad, but we should document it. Is it
|
||||
// possible that some resource *needs* Watch
|
||||
// to run to be able to execute a CheckApply?
|
||||
// That situation shouldn't be common, and
|
||||
// should probably not be allowed. Can we
|
||||
// avoid it though?
|
||||
//if exit, err := doSend(); exit || err != nil {
|
||||
// return err // we exit or bubble up a NACK...
|
||||
//}
|
||||
// Instead of doing the above, we can
|
||||
// add events to a pending list, and
|
||||
// when we finish the delay, we can run
|
||||
// them.
|
||||
//pendingSendEvent = true // all events are identical for now...
|
||||
}
|
||||
}
|
||||
}
|
||||
timer.Stop() // it's nice to cleanup
|
||||
log.Printf("%s[%s]: Watch delay expired!", v.Kind(), v.GetName())
|
||||
// NOTE: we can avoid the send if running Watch guarantees
|
||||
// one CheckApply event on startup!
|
||||
//if pendingSendEvent { // TODO: should this become a list in the future?
|
||||
// if exit, err := obj.DoSend(chanProcess, ""); exit || err != nil {
|
||||
// return err // we exit or bubble up a NACK...
|
||||
// }
|
||||
//}
|
||||
}
|
||||
|
||||
// TODO: reset the watch retry count after some amount of success
|
||||
e := v.Res.Watch(chanProcess)
|
||||
if e == nil { // exit signal
|
||||
err = nil // clean exit
|
||||
break
|
||||
}
|
||||
if sentinelErr, ok := e.(*SentinelErr); ok { // unwrap the sentinel
|
||||
err = sentinelErr.err
|
||||
break // sentinel means, perma-exit
|
||||
}
|
||||
log.Printf("%v[%v]: Watch errored: %v", v.Kind(), v.GetName(), e)
|
||||
if watchRetry == 0 {
|
||||
err = fmt.Errorf("Permanent watch error: %v", e)
|
||||
break
|
||||
}
|
||||
if watchRetry > 0 { // don't decrement the -1
|
||||
watchRetry--
|
||||
}
|
||||
watchDelay = time.Duration(v.Meta().Delay) * time.Millisecond
|
||||
log.Printf("%v[%v]: Watch: Retrying after %.4f seconds (%d left)", v.Kind(), v.GetName(), watchDelay.Seconds(), watchRetry)
|
||||
// We need to trigger a CheckApply after Watch restarts, so that
|
||||
// we catch any lost events that happened while down. We do this
|
||||
// by getting the Watch resource to send one event once it's up!
|
||||
//v.SendEvent(eventPoke, false, false)
|
||||
}
|
||||
close(chanProcess)
|
||||
return err
|
||||
}
|
||||
|
||||
// Start is a main kick to start the graph. It goes through in reverse topological
|
||||
// sort order so that events can't hit un-started vertices.
|
||||
func (g *Graph) Start(wg *sync.WaitGroup, first bool) { // start or continue
|
||||
log.Printf("State: %v -> %v", g.setState(graphStateStarting), g.getState())
|
||||
defer log.Printf("State: %v -> %v", g.setState(graphStateStarted), g.getState())
|
||||
t, _ := g.TopologicalSort()
|
||||
// TODO: only calculate indegree if `first` is true to save resources
|
||||
indegree := g.InDegree() // compute all of the indegree's
|
||||
for _, v := range Reverse(t) {
|
||||
|
||||
if !v.Res.IsWatching() { // if Watch() is not running...
|
||||
wg.Add(1)
|
||||
// must pass in value to avoid races...
|
||||
// see: https://ttboj.wordpress.com/2015/07/27/golang-parallelism-issues-causing-too-many-open-files-error/
|
||||
go func(vv *Vertex) {
|
||||
defer wg.Done()
|
||||
// TODO: if a sufficient number of workers error,
|
||||
// should something be done? Will these restart
|
||||
// after perma-failure if we have a graph change?
|
||||
if err := g.Worker(vv); err != nil { // contains the Watch and CheckApply loops
|
||||
log.Printf("%s[%s]: Exited with failure: %v", vv.Kind(), vv.GetName(), err)
|
||||
return
|
||||
}
|
||||
log.Printf("%v[%v]: Exited", vv.Kind(), vv.GetName())
|
||||
}(v)
|
||||
}
|
||||
|
||||
// selective poke: here we reduce the number of initial pokes
|
||||
// to the minimum required to activate every vertex in the
|
||||
// graph, either by direct action, or by getting poked by a
|
||||
// vertex that was previously activated. if we poke each vertex
|
||||
// that has no incoming edges, then we can be sure to reach the
|
||||
// whole graph. Please note: this may mask certain optimization
|
||||
// failures, such as any poke limiting code in Poke() or
|
||||
// BackPoke(). You might want to disable this selective start
|
||||
// when experimenting with and testing those elements.
|
||||
// if we are unpausing (since it's not the first run of this
|
||||
// function) we need to poke to *unpause* every graph vertex,
|
||||
// and not just selectively the subset with no indegree.
|
||||
if (!first) || indegree[v] == 0 {
|
||||
// ensure state is started before continuing on to next vertex
|
||||
for !v.SendEvent(event.EventStart, true, false) {
|
||||
if global.DEBUG {
|
||||
// if SendEvent fails, we aren't up yet
|
||||
log.Printf("%v[%v]: Retrying SendEvent(Start)", v.Kind(), v.GetName())
|
||||
// sleep here briefly or otherwise cause
|
||||
// a different goroutine to be scheduled
|
||||
time.Sleep(1 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pause sends pause events to the graph in a topological sort order.
|
||||
func (g *Graph) Pause() {
|
||||
log.Printf("State: %v -> %v", g.setState(graphStatePausing), g.getState())
|
||||
defer log.Printf("State: %v -> %v", g.setState(graphStatePaused), g.getState())
|
||||
t, _ := g.TopologicalSort()
|
||||
for _, v := range t { // squeeze out the events...
|
||||
v.SendEvent(event.EventPause, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
// Exit sends exit events to the graph in a topological sort order.
|
||||
func (g *Graph) Exit() {
|
||||
if g == nil {
|
||||
return
|
||||
} // empty graph that wasn't populated yet
|
||||
t, _ := g.TopologicalSort()
|
||||
for _, v := range t { // squeeze out the events...
|
||||
// turn off the taps...
|
||||
// XXX: consider instead doing this by closing the Res.events channel instead?
|
||||
// XXX: do this by sending an exit signal, and then returning
|
||||
// when we hit the 'default' in the select statement!
|
||||
// XXX: we can do this to quiesce, but it's not necessary now
|
||||
|
||||
v.SendEvent(event.EventExit, true, false)
|
||||
}
|
||||
}
|
||||
|
||||
// AssociateData associates some data with the object in the graph in question
|
||||
func (g *Graph) AssociateData(converger converger.Converger) {
|
||||
for v := range g.GetVerticesChan() {
|
||||
v.Res.AssociateData(converger)
|
||||
}
|
||||
}
|
||||
|
||||
// VertexContains is an "in array" function to test for a vertex in a slice of vertices.
|
||||
func VertexContains(needle *Vertex, haystack []*Vertex) bool {
|
||||
for _, v := range haystack {
|
||||
if needle == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Reverse reverses a list of vertices.
|
||||
func Reverse(vs []*Vertex) []*Vertex {
|
||||
//var out []*Vertex // XXX: golint suggests, but it fails testing
|
||||
out := make([]*Vertex, 0) // empty list
|
||||
l := len(vs)
|
||||
for i := range vs {
|
||||
out = append(out, vs[l-i-1])
|
||||
}
|
||||
return out
|
||||
}
|
||||
1293
pgraph/pgraph_test.go
Normal file
408
pgraph_test.go
@@ -1,408 +0,0 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// NOTE: this is pgraph, a pointer graph
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPgraphT1(t *testing.T) {
|
||||
|
||||
G := NewGraph("g1")
|
||||
|
||||
if i := G.NumVertices(); i != 0 {
|
||||
t.Errorf("Should have 0 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
if i := G.NumEdges(); i != 0 {
|
||||
t.Errorf("Should have 0 edges instead of: %d.", i)
|
||||
}
|
||||
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
e1 := NewEdge("e1")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
|
||||
if i := G.NumVertices(); i != 2 {
|
||||
t.Errorf("Should have 2 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
if i := G.NumEdges(); i != 1 {
|
||||
t.Errorf("Should have 1 edges instead of: %d.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT2(t *testing.T) {
|
||||
|
||||
G := NewGraph("g2")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
//e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
G.AddEdge(v4, v5, e4)
|
||||
G.AddEdge(v5, v6, e5)
|
||||
|
||||
if i := G.NumVertices(); i != 6 {
|
||||
t.Errorf("Should have 6 vertices instead of: %d.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT3(t *testing.T) {
|
||||
|
||||
G := NewGraph("g3")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
//e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
G.AddEdge(v4, v5, e4)
|
||||
G.AddEdge(v5, v6, e5)
|
||||
//G.AddEdge(v6, v4, e6)
|
||||
out1 := G.DFS(v1)
|
||||
if i := len(out1); i != 3 {
|
||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||
t.Errorf("Found: %v", out1)
|
||||
for _, v := range out1 {
|
||||
t.Errorf("Value: %v", v.GetName())
|
||||
}
|
||||
}
|
||||
|
||||
out2 := G.DFS(v4)
|
||||
if i := len(out2); i != 3 {
|
||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||
t.Errorf("Found: %v", out1)
|
||||
for _, v := range out1 {
|
||||
t.Errorf("Value: %v", v.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT4(t *testing.T) {
|
||||
|
||||
G := NewGraph("g4")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
out := G.DFS(v1)
|
||||
if i := len(out); i != 3 {
|
||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||
t.Errorf("Found: %v", out)
|
||||
for _, v := range out {
|
||||
t.Errorf("Value: %v", v.GetName())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT5(t *testing.T) {
|
||||
G := NewGraph("g5")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
//e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
G.AddEdge(v4, v5, e4)
|
||||
G.AddEdge(v5, v6, e5)
|
||||
//G.AddEdge(v6, v4, e6)
|
||||
|
||||
save := []*Vertex{v1, v2, v3}
|
||||
out := G.FilterGraph("new g5", save)
|
||||
if i := out.NumVertices(); i != 3 {
|
||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT6(t *testing.T) {
|
||||
G := NewGraph("g6")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
//e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
G.AddEdge(v4, v5, e4)
|
||||
G.AddEdge(v5, v6, e5)
|
||||
//G.AddEdge(v6, v4, e6)
|
||||
|
||||
graphs := G.GetDisconnectedGraphs()
|
||||
HeisenbergGraphCount := func(ch chan *Graph) int {
|
||||
c := 0
|
||||
for x := range ch {
|
||||
_ = x
|
||||
c++
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
if i := HeisenbergGraphCount(graphs); i != 2 {
|
||||
t.Errorf("Should have 2 graphs instead of: %d.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT7(t *testing.T) {
|
||||
|
||||
G := NewGraph("g7")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v1, e3)
|
||||
|
||||
if i := G.NumVertices(); i != 3 {
|
||||
t.Errorf("Should have 3 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
G.DeleteVertex(v2)
|
||||
|
||||
if i := G.NumVertices(); i != 2 {
|
||||
t.Errorf("Should have 2 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
G.DeleteVertex(v1)
|
||||
|
||||
if i := G.NumVertices(); i != 1 {
|
||||
t.Errorf("Should have 1 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
G.DeleteVertex(v3)
|
||||
|
||||
if i := G.NumVertices(); i != 0 {
|
||||
t.Errorf("Should have 0 vertices instead of: %d.", i)
|
||||
}
|
||||
|
||||
G.DeleteVertex(v2) // duplicate deletes don't error...
|
||||
|
||||
if i := G.NumVertices(); i != 0 {
|
||||
t.Errorf("Should have 0 vertices instead of: %d.", i)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT8(t *testing.T) {
|
||||
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
if HasVertex(v1, []*Vertex{v1, v2, v3}) != true {
|
||||
t.Errorf("Should be true instead of false.")
|
||||
}
|
||||
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
if HasVertex(v4, []*Vertex{v5, v6}) != false {
|
||||
t.Errorf("Should be false instead of true.")
|
||||
}
|
||||
|
||||
v7 := NewVertex(NewNoopType("v7"))
|
||||
v8 := NewVertex(NewNoopType("v8"))
|
||||
v9 := NewVertex(NewNoopType("v9"))
|
||||
if HasVertex(v8, []*Vertex{v7, v8, v9}) != true {
|
||||
t.Errorf("Should be true instead of false.")
|
||||
}
|
||||
|
||||
v1b := NewVertex(NewNoopType("v1")) // same value, different objects
|
||||
if HasVertex(v1b, []*Vertex{v1, v2, v3}) != false {
|
||||
t.Errorf("Should be false instead of true.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT9(t *testing.T) {
|
||||
|
||||
G := NewGraph("g9")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v1, v3, e2)
|
||||
G.AddEdge(v2, v4, e3)
|
||||
G.AddEdge(v3, v4, e4)
|
||||
|
||||
G.AddEdge(v4, v5, e5)
|
||||
G.AddEdge(v5, v6, e6)
|
||||
|
||||
indegree := G.InDegree() // map[*Vertex]int
|
||||
if i := indegree[v1]; i != 0 {
|
||||
t.Errorf("Indegree of v1 should be 0 instead of: %d.", i)
|
||||
}
|
||||
if i := indegree[v2]; i != 1 {
|
||||
t.Errorf("Indegree of v2 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := indegree[v3]; i != 1 {
|
||||
t.Errorf("Indegree of v3 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := indegree[v4]; i != 2 {
|
||||
t.Errorf("Indegree of v4 should be 2 instead of: %d.", i)
|
||||
}
|
||||
if i := indegree[v5]; i != 1 {
|
||||
t.Errorf("Indegree of v5 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := indegree[v6]; i != 1 {
|
||||
t.Errorf("Indegree of v6 should be 1 instead of: %d.", i)
|
||||
}
|
||||
|
||||
outdegree := G.OutDegree() // map[*Vertex]int
|
||||
if i := outdegree[v1]; i != 2 {
|
||||
t.Errorf("Outdegree of v1 should be 2 instead of: %d.", i)
|
||||
}
|
||||
if i := outdegree[v2]; i != 1 {
|
||||
t.Errorf("Outdegree of v2 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := outdegree[v3]; i != 1 {
|
||||
t.Errorf("Outdegree of v3 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := outdegree[v4]; i != 1 {
|
||||
t.Errorf("Outdegree of v4 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := outdegree[v5]; i != 1 {
|
||||
t.Errorf("Outdegree of v5 should be 1 instead of: %d.", i)
|
||||
}
|
||||
if i := outdegree[v6]; i != 0 {
|
||||
t.Errorf("Outdegree of v6 should be 0 instead of: %d.", i)
|
||||
}
|
||||
|
||||
s, ok := G.TopologicalSort()
|
||||
// either possibility is a valid toposort
|
||||
match := reflect.DeepEqual(s, []*Vertex{v1, v2, v3, v4, v5, v6}) || reflect.DeepEqual(s, []*Vertex{v1, v3, v2, v4, v5, v6})
|
||||
if !ok || !match {
|
||||
t.Errorf("Topological sort failed, status: %v.", ok)
|
||||
str := "Found:"
|
||||
for _, v := range s {
|
||||
str += " " + v.Type.GetName()
|
||||
}
|
||||
t.Errorf(str)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT10(t *testing.T) {
|
||||
|
||||
G := NewGraph("g10")
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
e1 := NewEdge("e1")
|
||||
e2 := NewEdge("e2")
|
||||
e3 := NewEdge("e3")
|
||||
e4 := NewEdge("e4")
|
||||
e5 := NewEdge("e5")
|
||||
e6 := NewEdge("e6")
|
||||
G.AddEdge(v1, v2, e1)
|
||||
G.AddEdge(v2, v3, e2)
|
||||
G.AddEdge(v3, v4, e3)
|
||||
G.AddEdge(v4, v5, e4)
|
||||
G.AddEdge(v5, v6, e5)
|
||||
G.AddEdge(v4, v2, e6) // cycle
|
||||
|
||||
if _, ok := G.TopologicalSort(); ok {
|
||||
t.Errorf("Topological sort passed, but graph is cyclic.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPgraphT11(t *testing.T) {
|
||||
v1 := NewVertex(NewNoopType("v1"))
|
||||
v2 := NewVertex(NewNoopType("v2"))
|
||||
v3 := NewVertex(NewNoopType("v3"))
|
||||
v4 := NewVertex(NewNoopType("v4"))
|
||||
v5 := NewVertex(NewNoopType("v5"))
|
||||
v6 := NewVertex(NewNoopType("v6"))
|
||||
|
||||
if rev := Reverse([]*Vertex{}); !reflect.DeepEqual(rev, []*Vertex{}) {
|
||||
t.Errorf("Reverse of vertex slice failed.")
|
||||
}
|
||||
|
||||
if rev := Reverse([]*Vertex{v1}); !reflect.DeepEqual(rev, []*Vertex{v1}) {
|
||||
t.Errorf("Reverse of vertex slice failed.")
|
||||
}
|
||||
|
||||
if rev := Reverse([]*Vertex{v1, v2, v3, v4, v5, v6}); !reflect.DeepEqual(rev, []*Vertex{v6, v5, v4, v3, v2, v1}) {
|
||||
t.Errorf("Reverse of vertex slice failed.")
|
||||
}
|
||||
|
||||
if rev := Reverse([]*Vertex{v6, v5, v4, v3, v2, v1}); !reflect.DeepEqual(rev, []*Vertex{v1, v2, v3, v4, v5, v6}) {
|
||||
t.Errorf("Reverse of vertex slice failed.")
|
||||
}
|
||||
|
||||
}
|
||||
145
puppet/puppet.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package puppet provides the integration entrypoint for the puppet language.
|
||||
package puppet
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/purpleidea/mgmt/gconfig"
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
)
|
||||
|
||||
const (
|
||||
// PuppetYAMLBufferSize is the maximum buffer size for the yaml input data
|
||||
PuppetYAMLBufferSize = 65535
|
||||
)
|
||||
|
||||
func runPuppetCommand(cmd *exec.Cmd) ([]byte, error) {
|
||||
if global.DEBUG {
|
||||
log.Printf("Puppet: running command: %v", cmd)
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("Puppet: Error opening pipe to puppet command: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("Puppet: Error opening error pipe to puppet command: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("Puppet: Error starting puppet command: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// XXX: the current implementation is likely prone to fail
|
||||
// as soon as the YAML data overflows the buffer.
|
||||
data := make([]byte, PuppetYAMLBufferSize)
|
||||
var result []byte
|
||||
for err == nil {
|
||||
var count int
|
||||
count, err = stdout.Read(data)
|
||||
if err != nil && err != io.EOF {
|
||||
log.Printf("Puppet: Error reading YAML data from puppet: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
// Slicing down to the number of actual bytes is important, the YAML parser
|
||||
// will choke on an oversized slice. http://stackoverflow.com/a/33726617/3356612
|
||||
result = append(result, data[0:count]...)
|
||||
}
|
||||
if global.DEBUG {
|
||||
log.Printf("Puppet: read %v bytes of data from puppet", len(result))
|
||||
}
|
||||
for scanner := bufio.NewScanner(stderr); scanner.Scan(); {
|
||||
log.Printf("Puppet: (output) %v", scanner.Text())
|
||||
}
|
||||
if err := cmd.Wait(); err != nil {
|
||||
log.Printf("Puppet: Error: puppet command did not complete: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ParseConfigFromPuppet takes a special puppet param string and config and
|
||||
// returns the graph configuration structure.
|
||||
func ParseConfigFromPuppet(puppetParam, puppetConf string) *gconfig.GraphConfig {
|
||||
var puppetConfArg string
|
||||
if puppetConf != "" {
|
||||
puppetConfArg = "--config=" + puppetConf
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if puppetParam == "agent" {
|
||||
cmd = exec.Command("puppet", "mgmtgraph", "print", puppetConfArg)
|
||||
} else if strings.HasSuffix(puppetParam, ".pp") {
|
||||
cmd = exec.Command("puppet", "mgmtgraph", "print", puppetConfArg, "--manifest", puppetParam)
|
||||
} else {
|
||||
cmd = exec.Command("puppet", "mgmtgraph", "print", puppetConfArg, "--code", puppetParam)
|
||||
}
|
||||
|
||||
log.Println("Puppet: launching translator")
|
||||
|
||||
var config gconfig.GraphConfig
|
||||
if data, err := runPuppetCommand(cmd); err != nil {
|
||||
return nil
|
||||
} else if err := config.Parse(data); err != nil {
|
||||
log.Printf("Puppet: Error: Could not parse YAML output with Parse: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return &config
|
||||
}
|
||||
|
||||
// PuppetInterval returns the graph refresh interval from the puppet configuration.
|
||||
func PuppetInterval(puppetConf string) int {
|
||||
if global.DEBUG {
|
||||
log.Printf("Puppet: determining graph refresh interval")
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
if puppetConf != "" {
|
||||
cmd = exec.Command("puppet", "config", "print", "runinterval", "--config", puppetConf)
|
||||
} else {
|
||||
cmd = exec.Command("puppet", "config", "print", "runinterval")
|
||||
}
|
||||
|
||||
log.Println("Puppet: inspecting runinterval configuration")
|
||||
|
||||
interval := 1800
|
||||
data, err := runPuppetCommand(cmd)
|
||||
if err != nil {
|
||||
log.Printf("Puppet: could not determine configured run interval (%v), using default of %v", err, interval)
|
||||
return interval
|
||||
}
|
||||
result, err := strconv.ParseInt(strings.TrimSpace(string(data)), 10, 0)
|
||||
if err != nil {
|
||||
log.Printf("Puppet: error reading numeric runinterval value (%v), using default of %v", err, interval)
|
||||
return interval
|
||||
}
|
||||
|
||||
return int(result)
|
||||
}
|
||||
134
recwatch/configwatch.go
Normal file
@@ -0,0 +1,134 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package recwatch
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/purpleidea/mgmt/global"
|
||||
)
|
||||
|
||||
// ConfigWatcher returns events on a channel anytime one of its files events.
|
||||
type ConfigWatcher struct {
|
||||
ch chan string
|
||||
wg sync.WaitGroup
|
||||
closechan chan struct{}
|
||||
errorchan chan error
|
||||
}
|
||||
|
||||
// NewConfigWatcher creates a new ConfigWatcher struct.
|
||||
func NewConfigWatcher() *ConfigWatcher {
|
||||
return &ConfigWatcher{
|
||||
ch: make(chan string),
|
||||
closechan: make(chan struct{}),
|
||||
errorchan: make(chan error),
|
||||
}
|
||||
}
|
||||
|
||||
// Add new file paths to watch for events on.
|
||||
func (obj *ConfigWatcher) Add(file ...string) {
|
||||
if len(file) == 0 {
|
||||
return
|
||||
}
|
||||
if len(file) > 1 {
|
||||
for _, f := range file { // add all the files...
|
||||
obj.Add(f) // recurse
|
||||
}
|
||||
return
|
||||
}
|
||||
// otherwise, add the one file passed in...
|
||||
obj.wg.Add(1)
|
||||
go func() {
|
||||
defer obj.wg.Done()
|
||||
ch := ConfigWatch(file[0])
|
||||
for {
|
||||
select {
|
||||
case e := <-ch:
|
||||
if e != nil {
|
||||
obj.errorchan <- e
|
||||
return
|
||||
}
|
||||
obj.ch <- file[0]
|
||||
continue
|
||||
case <-obj.closechan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Error returns a channel of errors that notifies us of permanent issues.
|
||||
func (obj *ConfigWatcher) Error() <-chan error {
|
||||
return obj.errorchan
|
||||
}
|
||||
|
||||
// Events returns a channel to listen on for file events. It closes when it is
|
||||
// emptied after the Close() method is called. You can test for closure with the
|
||||
// f, more := <-obj.Events() pattern.
|
||||
func (obj *ConfigWatcher) Events() chan string {
|
||||
return obj.ch
|
||||
}
|
||||
|
||||
// Close shuts down the ConfigWatcher object. It closes the Events channel after
|
||||
// all the currently pending events have been emptied.
|
||||
func (obj *ConfigWatcher) Close() {
|
||||
if obj.ch == nil {
|
||||
return
|
||||
}
|
||||
close(obj.closechan)
|
||||
obj.wg.Wait() // wait until everyone is done sending on obj.ch
|
||||
//obj.ch <- "" // send finished message
|
||||
close(obj.ch)
|
||||
obj.ch = nil
|
||||
close(obj.errorchan)
|
||||
}
|
||||
|
||||
// ConfigWatch writes on the channel every time an event is seen for the path.
|
||||
func ConfigWatch(file string) chan error {
|
||||
ch := make(chan error)
|
||||
go func() {
|
||||
recWatcher, err := NewRecWatcher(file, false)
|
||||
if err != nil {
|
||||
ch <- err
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
defer recWatcher.Close()
|
||||
for {
|
||||
if global.DEBUG {
|
||||
log.Printf("Watching: %v", file)
|
||||
}
|
||||
select {
|
||||
case event, ok := <-recWatcher.Events():
|
||||
if !ok { // channel is closed
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
if err := event.Error; err != nil {
|
||||
ch <- err
|
||||
close(ch)
|
||||
return
|
||||
}
|
||||
ch <- nil // send event!
|
||||
}
|
||||
}
|
||||
//close(ch)
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
319
recwatch/recwatch.go
Normal file
@@ -0,0 +1,319 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2016+ James Shubin and the project contributors
|
||||
// Written by James Shubin <james@shubin.ca> and the project contributors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
// Package recwatch provides recursive file watching events via fsnotify.
|
||||
package recwatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/purpleidea/mgmt/global" // XXX: package mgmtmain instead?
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
|
||||
"gopkg.in/fsnotify.v1"
|
||||
//"github.com/go-fsnotify/fsnotify" // git master of "gopkg.in/fsnotify.v1"
|
||||
)
|
||||
|
||||
// Event represents a watcher event. These can include errors.
|
||||
type Event struct {
|
||||
Error error
|
||||
Body *fsnotify.Event
|
||||
}
|
||||
|
||||
// RecWatcher is the struct for the recursive watcher. Run Init() on it.
|
||||
type RecWatcher struct {
|
||||
Path string // computed path
|
||||
Recurse bool // should we watch recursively?
|
||||
isDir bool // computed isDir
|
||||
safename string // safe path
|
||||
watcher *fsnotify.Watcher
|
||||
watches map[string]struct{}
|
||||
events chan Event // one channel for events and err...
|
||||
once sync.Once
|
||||
wg sync.WaitGroup
|
||||
exit chan struct{}
|
||||
closeErr error
|
||||
}
|
||||
|
||||
// NewRecWatcher creates an initializes a new recursive watcher.
|
||||
func NewRecWatcher(path string, recurse bool) (*RecWatcher, error) {
|
||||
obj := &RecWatcher{
|
||||
Path: path,
|
||||
Recurse: recurse,
|
||||
}
|
||||
return obj, obj.Init()
|
||||
}
|
||||
|
||||
// Init starts the recursive file watcher.
|
||||
func (obj *RecWatcher) Init() error {
|
||||
obj.watcher = nil
|
||||
obj.watches = make(map[string]struct{})
|
||||
obj.events = make(chan Event)
|
||||
obj.exit = make(chan struct{})
|
||||
obj.isDir = strings.HasSuffix(obj.Path, "/") // dirs have trailing slashes
|
||||
obj.safename = path.Clean(obj.Path) // no trailing slash
|
||||
|
||||
var err error
|
||||
obj.watcher, err = fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if obj.isDir {
|
||||
if err := obj.addSubFolders(obj.safename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := obj.Watch(); err != nil {
|
||||
obj.events <- Event{Error: err}
|
||||
}
|
||||
obj.Close()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (obj *RecWatcher) Add(path string) error { // XXX implement me or not?
|
||||
//
|
||||
//}
|
||||
//
|
||||
//func (obj *RecWatcher) Remove(path string) error { // XXX implement me or not?
|
||||
//
|
||||
//}
|
||||
|
||||
// Close shuts down the watcher.
|
||||
func (obj *RecWatcher) Close() error {
|
||||
obj.once.Do(obj.close) // don't cause the channel to close twice
|
||||
return obj.closeErr
|
||||
}
|
||||
|
||||
// This close function is the function that actually does the close work. Don't
|
||||
// call it more than once!
|
||||
func (obj *RecWatcher) close() {
|
||||
var err error
|
||||
close(obj.exit) // send exit signal
|
||||
obj.wg.Wait()
|
||||
if obj.watcher != nil {
|
||||
err = obj.watcher.Close()
|
||||
obj.watcher = nil
|
||||
// TODO: should we send the close error?
|
||||
//if err != nil {
|
||||
// obj.events <- Event{Error: err}
|
||||
//}
|
||||
}
|
||||
close(obj.events)
|
||||
obj.closeErr = err // set the error
|
||||
}
|
||||
|
||||
// Events returns a channel of events. These include events for errors.
|
||||
func (obj *RecWatcher) Events() chan Event { return obj.events }
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *RecWatcher) Watch() error {
|
||||
if obj.watcher == nil {
|
||||
return fmt.Errorf("Watcher is not initialized!")
|
||||
}
|
||||
obj.wg.Add(1)
|
||||
defer obj.wg.Done()
|
||||
|
||||
patharray := util.PathSplit(obj.safename) // tokenize the path
|
||||
var index = len(patharray) // starting index
|
||||
var current string // current "watcher" location
|
||||
var deltaDepth int // depth delta between watcher and event
|
||||
var send = false // send event?
|
||||
|
||||
for {
|
||||
current = strings.Join(patharray[0:index], "/")
|
||||
if current == "" { // the empty string top is the root dir ("/")
|
||||
current = "/"
|
||||
}
|
||||
if global.DEBUG {
|
||||
log.Printf("Watching: %s", current) // attempting to watch...
|
||||
}
|
||||
// initialize in the loop so that we can reset on rm-ed handles
|
||||
if err := obj.watcher.Add(current); err != nil {
|
||||
if global.DEBUG {
|
||||
log.Printf("watcher.Add(%s): Error: %v", current, err)
|
||||
}
|
||||
|
||||
if err == syscall.ENOENT {
|
||||
index-- // usually not found, move up one dir
|
||||
index = int(math.Max(1, float64(index)))
|
||||
continue
|
||||
}
|
||||
|
||||
if err == syscall.ENOSPC {
|
||||
// no space left on device, out of inotify watches
|
||||
// TODO: consider letting the user fall back to
|
||||
// polling if they hit this error very often...
|
||||
return fmt.Errorf("Out of inotify watches: %v", err)
|
||||
} else if os.IsPermission(err) {
|
||||
return fmt.Errorf("Permission denied adding a watch: %v", err)
|
||||
}
|
||||
return fmt.Errorf("Unknown error: %v", err)
|
||||
}
|
||||
|
||||
select {
|
||||
case event := <-obj.watcher.Events:
|
||||
if global.DEBUG {
|
||||
log.Printf("Watch(%s), Event(%s): %v", current, event.Name, event.Op)
|
||||
}
|
||||
// the deeper you go, the bigger the deltaDepth is...
|
||||
// this is the difference between what we're watching,
|
||||
// and the event... doesn't mean we can't watch deeper
|
||||
if current == event.Name {
|
||||
deltaDepth = 0 // i was watching what i was looking for
|
||||
|
||||
} else if util.HasPathPrefix(event.Name, current) {
|
||||
deltaDepth = len(util.PathSplit(current)) - len(util.PathSplit(event.Name)) // -1 or less
|
||||
|
||||
} else if util.HasPathPrefix(current, event.Name) {
|
||||
deltaDepth = len(util.PathSplit(event.Name)) - len(util.PathSplit(current)) // +1 or more
|
||||
// if below me...
|
||||
if _, exists := obj.watches[event.Name]; exists {
|
||||
send = true
|
||||
if event.Op&fsnotify.Remove == fsnotify.Remove {
|
||||
obj.watcher.Remove(event.Name)
|
||||
delete(obj.watches, event.Name)
|
||||
}
|
||||
if (event.Op&fsnotify.Create == fsnotify.Create) && isDir(event.Name) {
|
||||
obj.watcher.Add(event.Name)
|
||||
obj.watches[event.Name] = struct{}{}
|
||||
if err := obj.addSubFolders(event.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// TODO different watchers get each others events!
|
||||
// https://github.com/go-fsnotify/fsnotify/issues/95
|
||||
// this happened with two values such as:
|
||||
// event.Name: /tmp/mgmt/f3 and current: /tmp/mgmt/f2
|
||||
continue
|
||||
}
|
||||
//log.Printf("The delta depth is: %v", deltaDepth)
|
||||
|
||||
// if we have what we wanted, awesome, send an event...
|
||||
if event.Name == obj.safename {
|
||||
//log.Println("Event!")
|
||||
// FIXME: should all these below cases trigger?
|
||||
send = true
|
||||
|
||||
if obj.isDir {
|
||||
if err := obj.addSubFolders(obj.safename); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// file removed, move the watch upwards
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
//log.Println("Removal!")
|
||||
obj.watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
// we must be a parent watcher, so descend in
|
||||
if deltaDepth < 0 {
|
||||
// XXX: we can block here due to: https://github.com/fsnotify/fsnotify/issues/123
|
||||
obj.watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if safename starts with event.Name, we're above, and no event should be sent
|
||||
} else if util.HasPathPrefix(obj.safename, event.Name) {
|
||||
//log.Println("Above!")
|
||||
|
||||
if deltaDepth >= 0 && (event.Op&fsnotify.Remove == fsnotify.Remove) {
|
||||
log.Println("Removal!")
|
||||
obj.watcher.Remove(current)
|
||||
index--
|
||||
}
|
||||
|
||||
if deltaDepth < 0 {
|
||||
log.Println("Parent!")
|
||||
if util.PathPrefixDelta(obj.safename, event.Name) == 1 { // we're the parent dir
|
||||
send = true
|
||||
}
|
||||
obj.watcher.Remove(current)
|
||||
index++
|
||||
}
|
||||
|
||||
// if event.Name startswith safename, send event, we're already deeper
|
||||
} else if util.HasPathPrefix(event.Name, obj.safename) {
|
||||
//log.Println("Event2!")
|
||||
send = true
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
// only invalid state on certain types of events
|
||||
obj.events <- Event{Error: nil, Body: &event}
|
||||
}
|
||||
|
||||
case err := <-obj.watcher.Errors:
|
||||
return fmt.Errorf("Unknown watcher error: %v", err)
|
||||
|
||||
case <-obj.exit:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// addSubFolders is a helper that is used to add recursive dirs to the watches.
|
||||
func (obj *RecWatcher) addSubFolders(p string) error {
|
||||
if !obj.Recurse {
|
||||
return nil // if we're not watching recursively, just exit early
|
||||
}
|
||||
// look at all subfolders...
|
||||
walkFn := func(path string, info os.FileInfo, err error) error {
|
||||
if global.DEBUG {
|
||||
log.Printf("Walk: %s (%v): %v", path, info, err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() {
|
||||
obj.watches[path] = struct{}{} // add key
|
||||
err := obj.watcher.Add(path)
|
||||
if err != nil {
|
||||
return err // TODO: will this bubble up?
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
err := filepath.Walk(p, walkFn)
|
||||
return err
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
finfo, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return finfo.IsDir()
|
||||
}
|
||||
1115
remote/remote.go
Normal file
@@ -15,18 +15,30 @@
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package main
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/purpleidea/mgmt/event"
|
||||
"github.com/purpleidea/mgmt/util"
|
||||
)
|
||||
|
||||
type ExecType struct {
|
||||
BaseType `yaml:",inline"`
|
||||
func init() {
|
||||
gob.Register(&ExecRes{})
|
||||
}
|
||||
|
||||
// ExecRes is an exec resource for running commands.
|
||||
type ExecRes struct {
|
||||
BaseRes `yaml:",inline"`
|
||||
State string `yaml:"state"` // state: exists/present?, absent, (undefined?)
|
||||
Cmd string `yaml:"cmd"` // the command to run
|
||||
Shell string `yaml:"shell"` // the (optional) shell to use to run the cmd
|
||||
@@ -38,13 +50,11 @@ type ExecType struct {
|
||||
PollInt int `yaml:"pollint"` // the poll interval for the ifcmd
|
||||
}
|
||||
|
||||
func NewExecType(name, cmd, shell string, timeout int, watchcmd, watchshell, ifcmd, ifshell string, pollint int, state string) *ExecType {
|
||||
// FIXME if path = nil, path = name ...
|
||||
return &ExecType{
|
||||
BaseType: BaseType{
|
||||
// NewExecRes is a constructor for this resource. It also calls Init() for you.
|
||||
func NewExecRes(name, cmd, shell string, timeout int, watchcmd, watchshell, ifcmd, ifshell string, pollint int, state string) *ExecRes {
|
||||
obj := &ExecRes{
|
||||
BaseRes: BaseRes{
|
||||
Name: name,
|
||||
events: make(chan Event),
|
||||
vertex: nil,
|
||||
},
|
||||
Cmd: cmd,
|
||||
Shell: shell,
|
||||
@@ -56,29 +66,33 @@ func NewExecType(name, cmd, shell string, timeout int, watchcmd, watchshell, ifc
|
||||
PollInt: pollint,
|
||||
State: state,
|
||||
}
|
||||
obj.Init()
|
||||
return obj
|
||||
}
|
||||
|
||||
func (obj *ExecType) GetType() string {
|
||||
return "Exec"
|
||||
// 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
|
||||
}
|
||||
|
||||
// validate if the params passed in are valid data
|
||||
// Validate if the params passed in are valid data.
|
||||
// FIXME: where should this get called ?
|
||||
func (obj *ExecType) Validate() bool {
|
||||
func (obj *ExecRes) Validate() error {
|
||||
if obj.Cmd == "" { // this is the only thing that is really required
|
||||
return false
|
||||
return fmt.Errorf("Command can't be empty!")
|
||||
}
|
||||
|
||||
// if we have a watch command, then we don't poll with the if command!
|
||||
if obj.WatchCmd != "" && obj.PollInt > 0 {
|
||||
return false
|
||||
return fmt.Errorf("Don't poll when we have a watch command.")
|
||||
}
|
||||
|
||||
return true
|
||||
return nil
|
||||
}
|
||||
|
||||
// wraps the scanner output in a channel
|
||||
func (obj *ExecType) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan error) {
|
||||
// BufioChanScanner wraps the scanner output in a channel.
|
||||
func (obj *ExecRes) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan error) {
|
||||
ch, errch := make(chan string), make(chan error)
|
||||
go func() {
|
||||
for scanner.Scan() {
|
||||
@@ -95,18 +109,28 @@ func (obj *ExecType) BufioChanScanner(scanner *bufio.Scanner) (chan string, chan
|
||||
return ch, errch
|
||||
}
|
||||
|
||||
// Exec watcher
|
||||
func (obj *ExecType) Watch() {
|
||||
// Watch is the primary listener for this resource and it outputs events.
|
||||
func (obj *ExecRes) Watch(processChan chan event.Event) error {
|
||||
if obj.IsWatching() {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
obj.SetWatching(true)
|
||||
defer obj.SetWatching(false)
|
||||
cuuid := obj.converger.Register()
|
||||
defer cuuid.Unregister()
|
||||
|
||||
var startup bool
|
||||
Startup := func(block bool) <-chan time.Time {
|
||||
if block {
|
||||
return nil // blocks forever
|
||||
//return make(chan time.Time) // blocks forever
|
||||
}
|
||||
return time.After(time.Duration(500) * time.Millisecond) // 1/2 the resolution of converged timeout
|
||||
}
|
||||
|
||||
var send = false // send event?
|
||||
var exit = false
|
||||
bufioch, errch := make(chan string), make(chan error)
|
||||
//vertex := obj.GetVertex() // stored with SetVertex
|
||||
|
||||
if obj.WatchCmd != "" {
|
||||
var cmdName string
|
||||
@@ -118,7 +142,7 @@ func (obj *ExecType) Watch() {
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:len(split)]
|
||||
cmdArgs = split[1:]
|
||||
} else {
|
||||
cmdName = obj.Shell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.WatchCmd}
|
||||
@@ -128,8 +152,7 @@ func (obj *ExecType) Watch() {
|
||||
|
||||
cmdReader, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("%v[%v]: Error creating StdoutPipe for Cmd: %v", obj.GetType(), obj.GetName(), err)
|
||||
log.Fatal(err) // XXX: how should we handle errors?
|
||||
return fmt.Errorf("%s[%s]: Error creating StdoutPipe for Cmd: %v", obj.Kind(), obj.GetName(), err)
|
||||
}
|
||||
scanner := bufio.NewScanner(cmdReader)
|
||||
|
||||
@@ -140,66 +163,72 @@ func (obj *ExecType) Watch() {
|
||||
cmd.Process.Kill() // TODO: is this necessary?
|
||||
}()
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("%v[%v]: Error starting Cmd: %v", obj.GetType(), obj.GetName(), err)
|
||||
log.Fatal(err) // XXX: how should we handle errors?
|
||||
return fmt.Errorf("%s[%s]: Error starting Cmd: %v", obj.Kind(), obj.GetName(), err)
|
||||
}
|
||||
|
||||
bufioch, errch = obj.BufioChanScanner(scanner)
|
||||
}
|
||||
|
||||
for {
|
||||
obj.SetState(typeWatching) // reset
|
||||
obj.SetState(ResStateWatching) // reset
|
||||
select {
|
||||
case text := <-bufioch:
|
||||
obj.SetConvergedState(typeConvergedNil)
|
||||
cuuid.SetConverged(false)
|
||||
// each time we get a line of output, we loop!
|
||||
log.Printf("%v[%v]: Watch output: %s", obj.GetType(), obj.GetName(), text)
|
||||
log.Printf("%v[%v]: Watch output: %s", obj.Kind(), obj.GetName(), text)
|
||||
if text != "" {
|
||||
send = true
|
||||
}
|
||||
|
||||
case err := <-errch:
|
||||
obj.SetConvergedState(typeConvergedNil) // XXX ?
|
||||
cuuid.SetConverged(false)
|
||||
if err == nil { // EOF
|
||||
// FIXME: add an "if watch command ends/crashes"
|
||||
// restart or generate error option
|
||||
log.Printf("%v[%v]: Reached EOF", obj.GetType(), obj.GetName())
|
||||
return
|
||||
return fmt.Errorf("%s[%s]: Reached EOF", obj.Kind(), obj.GetName())
|
||||
}
|
||||
log.Printf("%v[%v]: Error reading input?: %v", obj.GetType(), obj.GetName(), err)
|
||||
log.Fatal(err)
|
||||
// XXX: how should we handle errors?
|
||||
// error reading input?
|
||||
return fmt.Errorf("Unknown %s[%s] error: %v", obj.Kind(), obj.GetName(), err)
|
||||
|
||||
case event := <-obj.events:
|
||||
obj.SetConvergedState(typeConvergedNil)
|
||||
cuuid.SetConverged(false)
|
||||
if exit, send = obj.ReadEvent(&event); exit {
|
||||
return // exit
|
||||
return nil // exit
|
||||
}
|
||||
|
||||
case _ = <-TimeAfterOrBlock(obj.ctimeout):
|
||||
obj.SetConvergedState(typeConvergedTimeout)
|
||||
obj.converged <- true
|
||||
case <-cuuid.ConvergedTimer():
|
||||
cuuid.SetConverged(true) // converged!
|
||||
continue
|
||||
|
||||
case <-Startup(startup):
|
||||
cuuid.SetConverged(false)
|
||||
send = true
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
startup = true // startup finished
|
||||
send = false
|
||||
// it is okay to invalidate the clean state on poke too
|
||||
obj.isStateOK = false // something made state dirty
|
||||
Process(obj) // XXX: rename this function
|
||||
if exit, err := obj.DoSend(processChan, ""); exit || err != nil {
|
||||
return err // we exit or bubble up a NACK...
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply checks the resource state and applies the resource if the bool
|
||||
// input is true. It returns error info and if the state check passed or not.
|
||||
// TODO: expand the IfCmd to be a list of commands
|
||||
func (obj *ExecType) StateOK() bool {
|
||||
func (obj *ExecRes) CheckApply(apply bool) (checkok bool, err error) {
|
||||
log.Printf("%v[%v]: CheckApply(%t)", obj.Kind(), obj.GetName(), apply)
|
||||
|
||||
// if there is a watch command, but no if command, run based on state
|
||||
if b := obj.isStateOK; obj.WatchCmd != "" && obj.IfCmd == "" {
|
||||
obj.isStateOK = true // reset
|
||||
//if !obj.isStateOK { obj.isStateOK = true; return false }
|
||||
return b
|
||||
if obj.WatchCmd != "" && obj.IfCmd == "" {
|
||||
if obj.isStateOK {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// if there is no watcher, but there is an onlyif check, run it to see
|
||||
} else if obj.IfCmd != "" { // && obj.WatchCmd == ""
|
||||
@@ -221,28 +250,32 @@ func (obj *ExecType) StateOK() bool {
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:len(split)]
|
||||
cmdArgs = split[1:]
|
||||
} else {
|
||||
cmdName = obj.IfShell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.IfCmd}
|
||||
}
|
||||
err := exec.Command(cmdName, cmdArgs...).Run()
|
||||
err = exec.Command(cmdName, cmdArgs...).Run()
|
||||
if err != nil {
|
||||
// TODO: check exit value
|
||||
return true // don't run
|
||||
return true, nil // don't run
|
||||
}
|
||||
return false // just run
|
||||
|
||||
// if there is no watcher and no onlyif check, assume we should run
|
||||
} else { // if obj.WatchCmd == "" && obj.IfCmd == "" {
|
||||
b := obj.isStateOK
|
||||
obj.isStateOK = true
|
||||
return b // just run if state is dirty
|
||||
// just run if state is dirty
|
||||
if obj.isStateOK {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (obj *ExecType) Apply() bool {
|
||||
log.Printf("%v[%v]: Apply", obj.GetType(), obj.GetName())
|
||||
// state is not okay, no work done, exit, but without error
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// apply portion
|
||||
log.Printf("%v[%v]: Apply", obj.Kind(), obj.GetName())
|
||||
var cmdName string
|
||||
var cmdArgs []string
|
||||
if obj.Shell == "" {
|
||||
@@ -253,7 +286,7 @@ func (obj *ExecType) Apply() bool {
|
||||
cmdName = split[0]
|
||||
//d, _ := os.Getwd() // TODO: how does this ever error ?
|
||||
//cmdName = path.Join(d, cmdName)
|
||||
cmdArgs = split[1:len(split)]
|
||||
cmdArgs = split[1:]
|
||||
} else {
|
||||
cmdName = obj.Shell // usually bash, or sh
|
||||
cmdArgs = []string{"-c", obj.Cmd}
|
||||
@@ -263,9 +296,9 @@ func (obj *ExecType) Apply() bool {
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("%v[%v]: Error starting Cmd: %v", obj.GetType(), obj.GetName(), err)
|
||||
return false
|
||||
if err = cmd.Start(); err != nil {
|
||||
log.Printf("%v[%v]: Error starting Cmd: %v", obj.Kind(), obj.GetName(), err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
timeout := obj.Timeout
|
||||
@@ -276,16 +309,16 @@ func (obj *ExecType) Apply() bool {
|
||||
go func() { done <- cmd.Wait() }()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
case err = <-done:
|
||||
if err != nil {
|
||||
log.Printf("%v[%v]: Error waiting for Cmd: %v", obj.GetType(), obj.GetName(), err)
|
||||
return false
|
||||
log.Printf("%v[%v]: Error waiting for Cmd: %v", obj.Kind(), obj.GetName(), err)
|
||||
return false, err
|
||||
}
|
||||
|
||||
case <-TimeAfterOrBlock(timeout):
|
||||
log.Printf("%v[%v]: Timeout waiting for Cmd", obj.GetType(), obj.GetName())
|
||||
case <-util.TimeAfterOrBlock(timeout):
|
||||
log.Printf("%v[%v]: Timeout waiting for Cmd", obj.Kind(), obj.GetName())
|
||||
//cmd.Process.Kill() // TODO: is this necessary?
|
||||
return false
|
||||
return false, errors.New("Timeout waiting for Cmd!")
|
||||
}
|
||||
|
||||
// TODO: if we printed the stdout while the command is running, this
|
||||
@@ -298,38 +331,119 @@ func (obj *ExecType) Apply() bool {
|
||||
log.Printf(out.String())
|
||||
}
|
||||
// XXX: return based on exit value!!
|
||||
|
||||
// the state tracking is for exec resources that can't "detect" their
|
||||
// state, and assume it's invalid when the Watch() function triggers.
|
||||
// if we apply state successfully, we should reset it here so that we
|
||||
// know that we have applied since the state was set not ok by event!
|
||||
obj.isStateOK = true // reset
|
||||
return false, nil // success
|
||||
}
|
||||
|
||||
// ExecUUID is the UUID struct for ExecRes.
|
||||
type ExecUUID struct {
|
||||
BaseUUID
|
||||
Cmd string
|
||||
IfCmd string
|
||||
// TODO: add more elements here
|
||||
}
|
||||
|
||||
// IFF aka if and only if they are equivalent, return true. If not, false.
|
||||
func (obj *ExecUUID) IFF(uuid ResUUID) bool {
|
||||
res, ok := uuid.(*ExecUUID)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if obj.Cmd != res.Cmd {
|
||||
return false
|
||||
}
|
||||
// TODO: add more checks here
|
||||
//if obj.Shell != res.Shell {
|
||||
// return false
|
||||
//}
|
||||
//if obj.Timeout != res.Timeout {
|
||||
// return false
|
||||
//}
|
||||
//if obj.WatchCmd != res.WatchCmd {
|
||||
// return false
|
||||
//}
|
||||
//if obj.WatchShell != res.WatchShell {
|
||||
// return false
|
||||
//}
|
||||
if obj.IfCmd != res.IfCmd {
|
||||
return false
|
||||
}
|
||||
//if obj.PollInt != res.PollInt {
|
||||
// return false
|
||||
//}
|
||||
//if obj.State != res.State {
|
||||
// return false
|
||||
//}
|
||||
return true
|
||||
}
|
||||
|
||||
func (obj *ExecType) Compare(typ Type) bool {
|
||||
switch typ.(type) {
|
||||
case *ExecType:
|
||||
typ := typ.(*ExecType)
|
||||
if obj.Name != typ.Name {
|
||||
// AutoEdges returns the AutoEdge interface. In this case no autoedges are used.
|
||||
func (obj *ExecRes) AutoEdges() AutoEdge {
|
||||
// TODO: parse as many exec params to look for auto edges, for example
|
||||
// the path of the binary in the Cmd variable might be from in a pkg
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUUIDs includes all params to make a unique identification of this object.
|
||||
// Most resources only return one, although some resources can return multiple.
|
||||
func (obj *ExecRes) GetUUIDs() []ResUUID {
|
||||
x := &ExecUUID{
|
||||
BaseUUID: BaseUUID{name: obj.GetName(), kind: obj.Kind()},
|
||||
Cmd: obj.Cmd,
|
||||
IfCmd: obj.IfCmd,
|
||||
// TODO: add more params here
|
||||
}
|
||||
return []ResUUID{x}
|
||||
}
|
||||
|
||||
// GroupCmp returns whether two resources can be grouped together or not.
|
||||
func (obj *ExecRes) GroupCmp(r Res) bool {
|
||||
_, ok := r.(*ExecRes)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if obj.Cmd != typ.Cmd {
|
||||
return false // not possible atm
|
||||
}
|
||||
|
||||
// Compare two resources and return if they are equivalent.
|
||||
func (obj *ExecRes) Compare(res Res) bool {
|
||||
switch res.(type) {
|
||||
case *ExecRes:
|
||||
res := res.(*ExecRes)
|
||||
if !obj.BaseRes.Compare(res) { // call base Compare
|
||||
return false
|
||||
}
|
||||
if obj.Shell != typ.Shell {
|
||||
|
||||
if obj.Name != res.Name {
|
||||
return false
|
||||
}
|
||||
if obj.Timeout != typ.Timeout {
|
||||
if obj.Cmd != res.Cmd {
|
||||
return false
|
||||
}
|
||||
if obj.WatchCmd != typ.WatchCmd {
|
||||
if obj.Shell != res.Shell {
|
||||
return false
|
||||
}
|
||||
if obj.WatchShell != typ.WatchShell {
|
||||
if obj.Timeout != res.Timeout {
|
||||
return false
|
||||
}
|
||||
if obj.IfCmd != typ.IfCmd {
|
||||
if obj.WatchCmd != res.WatchCmd {
|
||||
return false
|
||||
}
|
||||
if obj.PollInt != typ.PollInt {
|
||||
if obj.WatchShell != res.WatchShell {
|
||||
return false
|
||||
}
|
||||
if obj.State != typ.State {
|
||||
if obj.IfCmd != res.IfCmd {
|
||||
return false
|
||||
}
|
||||
if obj.PollInt != res.PollInt {
|
||||
return false
|
||||
}
|
||||
if obj.State != res.State {
|
||||
return false
|
||||
}
|
||||
default:
|
||||