engine: Resources package rewrite

This giant patch makes some much needed improvements to the code base.

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

Unfortunately, some code had to be temporarily removed. The remote code
had to be taken out, as did the prometheus code. We hope to have these
back in new forms as soon as possible.
This commit is contained in:
James Shubin
2018-03-13 12:02:44 -04:00
parent ef49aa7e08
commit 9969286224
140 changed files with 9130 additions and 8764 deletions

View File

@@ -122,7 +122,7 @@ lang: ## generates the lexer/parser for the language frontend
# build a `mgmt` binary for current host os/arch # build a `mgmt` binary for current host os/arch
$(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch $(PROGRAM): build/mgmt-${GOHOSTOS}-${GOHOSTARCH} ## build an mgmt binary for current host os/arch
cp $< $@ cp -a $< $@
$(PROGRAM).static: $(GO_FILES) $(PROGRAM).static: $(GO_FILES)
@echo "Building: $(PROGRAM).static, version: $(SVERSION)..." @echo "Building: $(PROGRAM).static, version: $(SVERSION)..."

View File

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

View File

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

View File

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

36
engine/autoedge_test.go Normal file
View File

@@ -0,0 +1,36 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"testing"
)
func TestIFF1(t *testing.T) {
uid := &BaseUID{Name: "/tmp/unit-test"}
same := &BaseUID{Name: "/tmp/unit-test"}
diff := &BaseUID{Name: "/tmp/other-file"}
if !uid.IFF(same) {
t.Errorf("basic resource UIDs with the same name should satisfy each other's IFF condition")
}
if uid.IFF(diff) {
t.Errorf("basic resource UIDs with different names should NOT satisfy each other's IFF condition")
}
}

84
engine/autogroup.go Normal file
View File

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

126
engine/cmp.go Normal file
View File

@@ -0,0 +1,126 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"fmt"
"github.com/purpleidea/mgmt/pgraph"
)
// ResCmp compares two resources by checking multiple aspects. This is the main
// entry point for running all the compare steps on two resource.
func ResCmp(r1, r2 Res) error {
if r1.Kind() != r2.Kind() {
return fmt.Errorf("kind differs")
}
if r1.Name() != r2.Name() {
return fmt.Errorf("name differs")
}
if err := r1.Cmp(r2); err != nil {
return err
}
// compare meta params for resources with auto edges
r1e, ok1 := r1.(EdgeableRes)
r2e, ok2 := r2.(EdgeableRes)
if ok1 != ok2 {
return fmt.Errorf("edgeable differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1e.AutoEdgeMeta().Cmp(r2e.AutoEdgeMeta()) != nil {
return fmt.Errorf("autoedge differs")
}
}
// compare meta params for resources with auto grouping
r1g, ok1 := r1.(GroupableRes)
r2g, ok2 := r2.(GroupableRes)
if ok1 != ok2 {
return fmt.Errorf("groupable differs") // they must be different (optional)
}
if ok1 && ok2 {
if r1g.AutoGroupMeta().Cmp(r2g.AutoGroupMeta()) != nil {
return fmt.Errorf("autogroup differs")
}
// if resources are grouped, are the groups the same?
if i, j := r1g.GetGroup(), r2g.GetGroup(); len(i) != len(j) {
return fmt.Errorf("autogroup groups differ")
} else if len(i) > 0 { // trick the golinter
// Sort works with Res, so convert the lists to that
iRes := []Res{}
for _, r := range i {
res := r.(Res)
iRes = append(iRes, res)
}
jRes := []Res{}
for _, r := range j {
res := r.(Res)
jRes = append(jRes, res)
}
ix, jx := Sort(iRes), Sort(jRes) // now sort :)
for k := range ix {
// compare sub resources
if err := ResCmp(ix[k], jx[k]); err != nil {
return err
}
}
}
}
return nil
}
// VertexCmpFn returns if two vertices are equivalent. It errors if they can't
// be compared because one is not a vertex. This returns true if equal.
// TODO: shouldn't the first argument be an `error` instead?
func VertexCmpFn(v1, v2 pgraph.Vertex) (bool, error) {
r1, ok := v1.(Res)
if !ok {
return false, fmt.Errorf("v1 is not a Res")
}
r2, ok := v2.(Res)
if !ok {
return false, fmt.Errorf("v2 is not a Res")
}
if ResCmp(r1, r2) != nil {
return false, nil
}
return true, nil
}
// EdgeCmpFn returns if two edges are equivalent. It errors if they can't be
// compared because one is not an edge. This returns true if equal.
// TODO: shouldn't the first argument be an `error` instead?
func EdgeCmpFn(e1, e2 pgraph.Edge) (bool, error) {
edge1, ok := e1.(*Edge)
if !ok {
return false, fmt.Errorf("e1 is not an Edge")
}
edge2, ok := e2.(*Edge)
if !ok {
return false, fmt.Errorf("e2 is not an Edge")
}
return edge1.Cmp(edge2) == nil, nil
}

View File

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

32
engine/error.go Normal file
View File

@@ -0,0 +1,32 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
// Error is a constant error type that implements error.
type Error string
// Error fulfills the error interface of this type.
func (e Error) Error() string { return string(e) }
const (
// ErrWatchExit represents an exit from the Watch loop via chan closure.
ErrWatchExit = Error("watch exit")
// ErrSignalExit represents an exit from the Watch loop via exit signal.
ErrSignalExit = Error("signal exit")
)

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

@@ -0,0 +1,33 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package event provides some primitives that are used for message passing.
package event
//go:generate stringer -type=Kind -output=kind_stringer.go
// Kind represents the type of event being passed.
type Kind int
// The different event kinds are used in different contexts.
const (
EventNil Kind = iota
EventStart
EventPause
EventPoke
EventExit
)

View File

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

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

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

View File

@@ -15,11 +15,16 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build novirt package graph
package resources import (
"github.com/purpleidea/mgmt/engine/graph/autoedge"
)
// VirtRes represents the fields of the Virt resource. Since this file is // AutoEdge adds the automatic edges to the graph.
// only invoked with the tag "novirt", we do not need any fields here. func (obj *Engine) AutoEdge() error {
type VirtRes struct { logf := func(format string, v ...interface{}) {
obj.Logf("autoedge: "+format, v...)
}
return autoedge.AutoEdge(obj.nextGraph, obj.Debug, logf)
} }

View File

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

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

@@ -0,0 +1,141 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autogroup"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
)
// AutoGroup runs the auto grouping on the loaded graph.
func (obj *Engine) AutoGroup(ag engine.AutoGrouper) error {
if obj.nextGraph == nil {
return fmt.Errorf("there is no active graph to autogroup")
}
logf := func(format string, v ...interface{}) {
obj.Logf("autogroup: "+format, v...)
}
// wrap ag with our own vertexCmp, vertexMerge and edgeMerge
wrapped := &wrappedGrouper{
AutoGrouper: ag, // pass in the existing autogrouper
}
if err := autogroup.AutoGroup(wrapped, obj.nextGraph, obj.Debug, logf); err != nil {
return errwrap.Wrapf(err, "autogrouping failed")
}
return nil
}
// wrappedGrouper is an autogrouper which adds our own Cmp and Merge functions
// on top of the desired AutoGrouper that was specified.
type wrappedGrouper struct {
engine.AutoGrouper // anonymous interface
}
func (obj *wrappedGrouper) Name() string {
return fmt.Sprintf("wrappedGrouper: %s", obj.AutoGrouper.Name())
}
func (obj *wrappedGrouper) VertexCmp(v1, v2 pgraph.Vertex) error {
// call existing vertexCmp first
if err := obj.AutoGrouper.VertexCmp(v1, v2); err != nil {
return err
}
r1, ok := v1.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v1 is not a GroupableRes")
}
r2, ok := v2.(engine.GroupableRes)
if !ok {
return fmt.Errorf("v2 is not a GroupableRes")
}
if r1.Kind() != r2.Kind() { // we must group similar kinds
// TODO: maybe future resources won't need this limitation?
return fmt.Errorf("the two resources aren't the same kind")
}
// someone doesn't want to group!
if r1.AutoGroupMeta().Disabled || r2.AutoGroupMeta().Disabled {
return fmt.Errorf("one of the autogroup flags is false")
}
if r1.IsGrouped() { // already grouped!
return fmt.Errorf("already grouped")
}
if len(r2.GetGroup()) > 0 { // already has children grouped!
return fmt.Errorf("already has groups")
}
if err := r1.GroupCmp(r2); err != nil { // resource groupcmp failed!
return errwrap.Wrapf(err, "the GroupCmp failed")
}
return nil
}
func (obj *wrappedGrouper) VertexMerge(v1, v2 pgraph.Vertex) (v pgraph.Vertex, err error) {
r1, ok := v1.(engine.GroupableRes)
if !ok {
return nil, fmt.Errorf("v1 is not a GroupableRes")
}
r2, ok := v2.(engine.GroupableRes)
if !ok {
return nil, fmt.Errorf("v2 is not a GroupableRes")
}
if err = r1.GroupRes(r2); err != nil { // GroupRes skips stupid groupings
return // return early on error
}
// merging two resources into one should yield the sum of their semas
if semas := r2.MetaParams().Sema; len(semas) > 0 {
r1.MetaParams().Sema = append(r1.MetaParams().Sema, semas...)
r1.MetaParams().Sema = util.StrRemoveDuplicatesInList(r1.MetaParams().Sema)
}
return // success or fail, and no need to merge the actual vertices!
}
func (obj *wrappedGrouper) EdgeMerge(e1, e2 pgraph.Edge) pgraph.Edge {
e1x, ok := e1.(*engine.Edge)
if !ok {
return e2 // just return something to avoid needing to error
}
e2x, ok := e2.(*engine.Edge)
if !ok {
return e1 // just return something to avoid needing to error
}
// TODO: should we merge the edge.Notify or edge.refresh values?
edge := &engine.Edge{
Notify: e1x.Notify || e2x.Notify, // TODO: should we merge this?
}
refresh := e1x.Refresh() || e2x.Refresh() // TODO: should we merge this?
edge.SetRefresh(refresh)
return edge
}

View File

@@ -0,0 +1,71 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
errwrap "github.com/pkg/errors"
)
// AutoGroup is the mechanical auto group "runner" that runs the interface spec.
// TODO: this algorithm may not be correct in all cases. replace if needed!
func AutoGroup(ag engine.AutoGrouper, g *pgraph.Graph, debug bool, logf func(format string, v ...interface{})) error {
logf("algorithm: %s...", ag.Name())
if err := ag.Init(g); err != nil {
return errwrap.Wrapf(err, "error running autoGroup(init)")
}
for {
var v, w pgraph.Vertex
v, w, err := ag.VertexNext() // get pair to compare
if err != nil {
return errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
}
merged := false
// save names since they change during the runs
vStr := fmt.Sprintf("%v", v) // valid even if it is nil
wStr := fmt.Sprintf("%v", w)
if err := ag.VertexCmp(v, w); err != nil { // cmp ?
if debug {
logf("!GroupCmp for: %s into: %s", wStr, vStr)
}
// remove grouped vertex and merge edges (res is safe)
} else if err := VertexMerge(g, v, w, ag.VertexMerge, ag.EdgeMerge); err != nil { // merge...
logf("!VertexMerge for: %s into: %s", wStr, vStr)
} else { // success!
logf("success for: %s into: %s", wStr, vStr)
merged = true // woo
}
// did these get used?
if ok, err := ag.VertexTest(merged); err != nil {
return errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
} else if !ok {
break // done!
}
}
return nil
}

View File

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

View File

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

View File

@@ -0,0 +1,73 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package autogroup
import (
"github.com/purpleidea/mgmt/pgraph"
errwrap "github.com/pkg/errors"
)
// NonReachabilityGrouper is the most straight-forward algorithm for grouping.
// TODO: this algorithm may not be correct in all cases. replace if needed!
type NonReachabilityGrouper struct {
baseGrouper // "inherit" what we want, and reimplement the rest
}
// Name returns the name for the grouper algorithm.
func (ag *NonReachabilityGrouper) Name() string {
return "NonReachabilityGrouper"
}
// VertexNext iteratively finds vertex pairs with simple graph reachability...
// This algorithm relies on the observation that if there's a path from a to b,
// then they *can't* be merged (b/c of the existing dependency) so therefore we
// merge anything that *doesn't* satisfy this condition or that of the reverse!
func (ag *NonReachabilityGrouper) VertexNext() (v1, v2 pgraph.Vertex, err error) {
for {
v1, v2, err = ag.baseGrouper.VertexNext() // get all iterable pairs
if err != nil {
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexNext)")
}
// ignore self cmp early (perf optimization)
if v1 != v2 && v1 != nil && v2 != nil {
// if NOT reachable, they're viable...
out1, e1 := ag.graph.Reachability(v1, v2)
if e1 != nil {
return nil, nil, e1
}
out2, e2 := ag.graph.Reachability(v2, v1)
if e2 != nil {
return nil, nil, e2
}
if len(out1) == 0 && len(out2) == 0 {
return // return v1 and v2, they're viable
}
}
// if we got here, it means we're skipping over this candidate!
if ok, err := ag.baseGrouper.VertexTest(false); err != nil {
return nil, nil, errwrap.Wrapf(err, "error running autoGroup(vertexTest)")
} else if !ok {
return nil, nil, nil // done!
}
// the vertexTest passed, so loop and try with a new pair...
}
}

View File

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

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

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

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

@@ -0,0 +1,59 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package graph
import (
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph"
)
// RefreshPending determines if any previous nodes have a refresh pending here.
// If this is true, it means I am expected to apply a refresh when I next run.
func (obj *Engine) RefreshPending(vertex pgraph.Vertex) bool {
var refresh bool
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
// if we asked for a notify *and* if one is pending!
edge := e.(*engine.Edge) // panic if wrong
if edge.Notify && edge.Refresh() {
refresh = true
break
}
}
return refresh
}
// SetUpstreamRefresh sets the refresh value to any upstream vertices.
func (obj *Engine) SetUpstreamRefresh(vertex pgraph.Vertex, b bool) {
for _, e := range obj.graph.IncomingGraphEdges(vertex) {
edge := e.(*engine.Edge) // panic if wrong
if edge.Notify {
edge.SetRefresh(b)
}
}
}
// SetDownstreamRefresh sets the refresh value to any downstream vertices.
func (obj *Engine) SetDownstreamRefresh(vertex pgraph.Vertex, b bool) {
for _, e := range obj.graph.OutgoingGraphEdges(vertex) {
edge := e.(*engine.Edge) // panic if wrong
// if we asked for a notify *and* if one is pending!
if edge.Notify {
edge.SetRefresh(b)
}
}
}

View File

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

View File

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

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

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

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

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

169
engine/metaparams.go Normal file
View File

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

40
engine/metaparams_test.go Normal file
View File

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"testing"
)
func TestMetaCmp1(t *testing.T) {
m1 := &MetaParams{
Noop: true,
}
m2 := &MetaParams{
Noop: false,
}
// TODO: should we allow this? Maybe only with the future Mutate API?
//if err := m2.Cmp(m1); err != nil { // going from noop(false) -> noop(true) is okay!
// t.Errorf("the two resources do not match")
//}
if m1.Cmp(m2) == nil { // going from noop(true) -> noop(false) is not okay!
t.Errorf("the two resources should not match")
}
}

32
engine/refresh.go Normal file
View File

@@ -0,0 +1,32 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
// RefreshableRes is the interface a resource must implement to support refresh
// notifications. Default implementations for all of the methods declared in
// this interface can be obtained for your resource by anonymously adding the
// traits.Refreshable struct to your resource implementation.
type RefreshableRes interface {
Res // implement everything in Res but add the additional requirements
// Refresh returns the refresh notification state.
Refresh() bool
// SetRefresh sets the refresh notification state.
SetRefresh(bool)
}

271
engine/resources.go Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -19,20 +19,30 @@ package resources
import ( import (
"testing" "testing"
"github.com/purpleidea/mgmt/engine"
) )
func fakeInit(t *testing.T) *engine.Init {
debug := true
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
return &engine.Init{
Running: func() error {
return nil
},
Debug: debug,
Logf: logf,
}
}
func TestExecSendRecv1(t *testing.T) { func TestExecSendRecv1(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world", Cmd: "echo hello world",
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -41,7 +51,7 @@ func TestExecSendRecv1(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine
@@ -73,16 +83,10 @@ func TestExecSendRecv1(t *testing.T) {
func TestExecSendRecv2(t *testing.T) { func TestExecSendRecv2(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world 1>&2", // to stderr Cmd: "echo hello world 1>&2", // to stderr
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -91,7 +95,7 @@ func TestExecSendRecv2(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine
@@ -123,16 +127,10 @@ func TestExecSendRecv2(t *testing.T) {
func TestExecSendRecv3(t *testing.T) { func TestExecSendRecv3(t *testing.T) {
r1 := &ExecRes{ r1 := &ExecRes{
BaseRes: BaseRes{
Name: "exec1",
Kind: "exec",
MetaParams: DefaultMetaParams,
},
Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr Cmd: "echo hello world && echo goodbye world 1>&2", // to stdout && stderr
Shell: "/bin/bash", Shell: "/bin/bash",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -141,7 +139,7 @@ func TestExecSendRecv3(t *testing.T) {
t.Errorf("close failed with: %v", err) t.Errorf("close failed with: %v", err)
} }
}() }()
if err := r1.Init(); err != nil { if err := r1.Init(fakeInit(t)); err != nil {
t.Errorf("init failed with: %v", err) t.Errorf("init failed with: %v", err)
} }
// run artificially without the entire engine // run artificially without the entire engine

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,146 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"bytes"
"encoding/base64"
"encoding/gob"
"testing"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/graph/autoedge"
"github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/pgraph"
)
func TestFileAutoEdge1(t *testing.T) {
g, err := pgraph.NewGraph("TestGraph")
if err != nil {
t.Errorf("error creating graph: %v", err)
return
}
r1 := &FileRes{
Path: "/tmp/a/b/", // some dir
}
r2 := &FileRes{
Path: "/tmp/a/", // some parent dir
}
r3 := &FileRes{
Path: "/tmp/a/b/c", // some child file
}
g.AddVertex(r1, r2, r3)
if i := g.NumEdges(); i != 0 {
t.Errorf("should have 0 edges instead of: %d", i)
}
debug := true
logf := func(format string, v ...interface{}) {
t.Logf("test: "+format, v...)
}
// run artificially without the entire engine
if err := autoedge.AutoEdge(g, debug, logf); err != nil {
t.Errorf("error running autoedges: %v", err)
}
// two edges should have been added
if i := g.NumEdges(); i != 2 {
t.Errorf("should have 2 edges instead of: %d", i)
}
}
func TestMiscEncodeDecode1(t *testing.T) {
var err error
// encode
var input interface{} = &FileRes{}
b1 := bytes.Buffer{}
e := gob.NewEncoder(&b1)
err = e.Encode(&input) // pass with &
if err != nil {
t.Errorf("Gob failed to Encode: %v", err)
}
str := base64.StdEncoding.EncodeToString(b1.Bytes())
// decode
var output interface{}
bb, err := base64.StdEncoding.DecodeString(str)
if err != nil {
t.Errorf("Base64 failed to Decode: %v", err)
}
b2 := bytes.NewBuffer(bb)
d := gob.NewDecoder(b2)
err = d.Decode(&output) // pass with &
if err != nil {
t.Errorf("Gob failed to Decode: %v", err)
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
}
}
func TestMiscEncodeDecode2(t *testing.T) {
var err error
// encode
input, err := engine.NewNamedResource("file", "file1")
if err != nil {
t.Errorf("Can't create: %v", err)
return
}
b64, err := util.ResToB64(input)
if err != nil {
t.Errorf("Can't encode: %v", err)
return
}
output, err := util.B64ToRes(b64)
if err != nil {
t.Errorf("Can't decode: %v", err)
return
}
res1, ok := input.(engine.Res)
if !ok {
t.Errorf("Input %v is not a Res", res1)
return
}
res2, ok := output.(engine.Res)
if !ok {
t.Errorf("Output %v is not a Res", res2)
return
}
if err := res1.Cmp(res2); err != nil {
t.Errorf("The input and output Res values do not match: %+v", err)
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,15 +23,9 @@ import (
func TestMsgValidate1(t *testing.T) { func TestMsgValidate1(t *testing.T) {
r1 := &MsgRes{ r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "Debug", Priority: "Debug",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err != nil { if err := r1.Validate(); err != nil {
t.Errorf("validate failed with: %v", err) t.Errorf("validate failed with: %v", err)
} }
@@ -39,15 +33,9 @@ func TestMsgValidate1(t *testing.T) {
func TestMsgValidate2(t *testing.T) { func TestMsgValidate2(t *testing.T) {
r1 := &MsgRes{ r1 := &MsgRes{
BaseRes: BaseRes{
Name: "msg1",
Kind: "msg",
MetaParams: DefaultMetaParams,
},
Priority: "UnrealPriority", Priority: "UnrealPriority",
} }
r1.Setup(nil, r1, r1)
if err := r1.Validate(); err == nil { if err := r1.Validate(); err == nil {
t.Errorf("validation error is nil") t.Errorf("validation error is nil")
} }

View File

@@ -15,13 +15,14 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !darwin
package resources package resources
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log"
"net" "net"
"os" "os"
"path" "path"
@@ -29,8 +30,8 @@ import (
"sync" "sync"
"syscall" "syscall"
"golang.org/x/sys/unix" "github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/traits"
"github.com/purpleidea/mgmt/recwatch" "github.com/purpleidea/mgmt/recwatch"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
@@ -40,10 +41,11 @@ import (
// do not clean up spawned goroutines. Should be replaced when a suitable // do not clean up spawned goroutines. Should be replaced when a suitable
// alternative is available. // alternative is available.
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix"
) )
func init() { func init() {
RegisterResource("net", func() Res { return &NetRes{} }) engine.RegisterResource("net", func() engine.Res { return &NetRes{} })
} }
const ( const (
@@ -75,21 +77,26 @@ const (
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h // https://github.com/torvalds/linux/blob/master/include/uapi/linux/rtnetlink.h
rtProtoKernel = 2 // kernel rtProtoKernel = 2 // kernel
rtProtoStatic = 4 // static rtProtoStatic = 4 // static
socketFile = "pipe.sock" // path in vardir to store our socket file
) )
// NetRes is a network interface resource based on netlink. It manages the // NetRes is a network interface resource based on netlink. It manages the
// state of a network link. Configuration is also stored in a networkd // state of a network link. Configuration is also stored in a networkd
// configuration file, so the network is available upon reboot. // configuration file, so the network is available upon reboot.
type NetRes struct { type NetRes struct {
BaseRes `yaml:",inline"` traits.Base // add the base methods without re-implementation
init *engine.Init
State string `yaml:"state"` // up, down, or empty State string `yaml:"state"` // up, down, or empty
Addrs []string `yaml:"addrs"` // list of addresses in cidr format Addrs []string `yaml:"addrs"` // list of addresses in cidr format
Gateway string `yaml:"gateway"` // gateway address Gateway string `yaml:"gateway"` // gateway address
iface *iface // a struct containing the net.Interface and netlink.Link iface *iface // a struct containing the net.Interface and netlink.Link
unitFilePath string // the interface unit file path unitFilePath string // the interface unit file path
// XXX: replace TempDir with VarDir
tempDir string // temporary directory for storing the pipe socket file socketFile string // path for storing the pipe socket file
} }
// nlChanStruct defines the channel used to send netlink messages and errors // nlChanStruct defines the channel used to send netlink messages and errors
@@ -100,12 +107,8 @@ type nlChanStruct struct {
} }
// Default returns some sensible defaults for this resource. // Default returns some sensible defaults for this resource.
func (obj *NetRes) Default() Res { func (obj *NetRes) Default() engine.Res {
return &NetRes{ return &NetRes{}
BaseRes: BaseRes{
MetaParams: DefaultMetaParams, // force a default
},
}
} }
// Validate if the params passed in are valid data. // Validate if the params passed in are valid data.
@@ -133,37 +136,57 @@ func (obj *NetRes) Validate() error {
} }
// validate the interface name // validate the interface name
_, err := net.InterfaceByName(obj.GetName()) _, err := net.InterfaceByName(obj.Name())
if err != nil { if err != nil {
return errwrap.Wrapf(err, "error finding interface: %s", obj.GetName()) return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
} }
return obj.BaseRes.Validate() return nil
} }
// Init runs some startup code for this resource. // Init runs some startup code for this resource.
func (obj *NetRes) Init() error { func (obj *NetRes) Init(init *engine.Init) error {
obj.init = init // save for later
var err error var err error
// tmp directory for pipe socket // tmp directory for pipe socket
// XXX: Replace with obj.VarDir dir, err := obj.init.VarDir("")
if obj.tempDir, err = ioutil.TempDir("", "pipe"); err != nil { if err != nil {
return errwrap.Wrapf(err, "could not get TempDir") return errwrap.Wrapf(err, "could not get VarDir in Init()")
} }
obj.socketFile = path.Join(dir, socketFile) // return a unique file
// store the network interface in the struct // store the network interface in the struct
obj.iface = &iface{} obj.iface = &iface{}
if obj.iface.iface, err = net.InterfaceByName(obj.GetName()); err != nil { if obj.iface.iface, err = net.InterfaceByName(obj.Name()); err != nil {
return errwrap.Wrapf(err, "error finding interface: %s", obj.GetName()) return errwrap.Wrapf(err, "error finding interface: %s", obj.Name())
} }
// store the netlink link to use as interface input in netlink functions // store the netlink link to use as interface input in netlink functions
if obj.iface.link, err = netlink.LinkByName(obj.GetName()); err != nil { if obj.iface.link, err = netlink.LinkByName(obj.Name()); err != nil {
return errwrap.Wrapf(err, "error finding link: %s", obj.GetName()) return errwrap.Wrapf(err, "error finding link: %s", obj.Name())
} }
// build the path to the networkd configuration file // build the path to the networkd configuration file
obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.GetName() + networkdUnitFileExt obj.unitFilePath = networkdUnitFileDir + IfacePrefix + obj.Name() + networkdUnitFileExt
return obj.BaseRes.Init() return nil
}
// Close cleans up when we're done.
func (obj *NetRes) Close() error {
var errList error
if obj.socketFile == "/" {
return fmt.Errorf("socket file should not be the root path")
}
if obj.socketFile != "" { // safety
if err := os.Remove(obj.socketFile); err != nil {
errList = multierr.Append(errList, err)
}
}
return errList
} }
// Watch listens for events from the specified interface via a netlink socket. // Watch listens for events from the specified interface via a netlink socket.
@@ -175,7 +198,7 @@ func (obj *NetRes) Watch() error {
defer wg.Wait() defer wg.Wait()
// create a netlink socket for receiving network interface events // create a netlink socket for receiving network interface events
conn, err := newSocketSet(rtmGrps, path.Join(obj.tempDir, "pipe.sock")) conn, err := newSocketSet(rtmGrps, obj.socketFile)
if err != nil { if err != nil {
return errwrap.Wrapf(err, "error creating socket set") return errwrap.Wrapf(err, "error creating socket set")
} }
@@ -224,14 +247,12 @@ func (obj *NetRes) Watch() error {
}() }()
// notify engine that we're running // notify engine that we're running
if err := obj.Running(); err != nil { if err := obj.init.Running(); err != nil {
return err // bubble up a NACK... return err // exit if requested
} }
var exit *error var send = false // send event?
var send bool
var done bool var done bool
for { for {
select { select {
case s, ok := <-nlChan: case s, ok := <-nlChan:
@@ -245,12 +266,12 @@ func (obj *NetRes) Watch() error {
if err := s.err; err != nil { if err := s.err; err != nil {
return errwrap.Wrapf(s.err, "unknown netlink error") return errwrap.Wrapf(s.err, "unknown netlink error")
} }
if obj.debug { if obj.init.Debug {
log.Printf("%s: Event: %+v", obj, s.msg) obj.init.Logf("Event: %+v", s.msg)
} }
send = true send = true
obj.StateOK(false) obj.init.Dirty() // dirty
case event, ok := <-recWatcher.Events(): case event, ok := <-recWatcher.Events():
if !ok { if !ok {
@@ -263,23 +284,28 @@ func (obj *NetRes) Watch() error {
if err := event.Error; err != nil { if err := event.Error; err != nil {
return errwrap.Wrapf(err, "unknown recwatcher error") return errwrap.Wrapf(err, "unknown recwatcher error")
} }
if obj.debug { if obj.init.Debug {
log.Printf("%s: Event(%s): %v", obj, event.Body.Name, event.Body.Op) obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
} }
send = true send = true
obj.StateOK(false) // dirty obj.init.Dirty() // dirty
case event := <-obj.Events(): case event, ok := <-obj.init.Events:
if exit, send = obj.ReadEvent(event); exit != nil { if !ok {
return *exit // exit return nil
}
if err := obj.init.Read(event); err != nil {
return err
} }
} }
// do all our event sending all together to avoid duplicate msgs // do all our event sending all together to avoid duplicate msgs
if send { if send {
send = false send = false
obj.Event() if err := obj.init.Event(); err != nil {
return err // exit if requested
}
} }
} }
} }
@@ -290,7 +316,7 @@ func (obj *NetRes) ifaceCheckApply(apply bool) (bool, error) {
// check the interface state // check the interface state
state, err := obj.iface.state() state, err := obj.iface.state()
if err != nil { if err != nil {
return false, errwrap.Wrapf(err, "error checking %s state", obj.GetName()) return false, errwrap.Wrapf(err, "error checking %s state", obj.Name())
} }
// if the state is correct or unspecified, we're done // if the state is correct or unspecified, we're done
if obj.State == state || obj.State == "" { if obj.State == state || obj.State == "" {
@@ -301,11 +327,11 @@ func (obj *NetRes) ifaceCheckApply(apply bool) (bool, error) {
if !apply { if !apply {
return false, nil return false, nil
} }
log.Printf("%s: ifaceCheckApply(%t)", obj, apply) obj.init.Logf("ifaceCheckApply(%t)", apply)
// ip link set up/down // ip link set up/down
if err := obj.iface.linkUpDown(obj.State); err != nil { if err := obj.iface.linkUpDown(obj.State); err != nil {
return false, errwrap.Wrapf(err, "error setting %s up or down", obj.GetName()) return false, errwrap.Wrapf(err, "error setting %s up or down", obj.Name())
} }
return false, nil return false, nil
@@ -317,7 +343,7 @@ func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
// get the link's addresses // get the link's addresses
ifaceAddrs, err := obj.iface.getAddrs() ifaceAddrs, err := obj.iface.getAddrs()
if err != nil { if err != nil {
return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.GetName()) return false, errwrap.Wrapf(err, "error getting addresses from %s", obj.Name())
} }
// if state is not defined // if state is not defined
if obj.Addrs == nil { if obj.Addrs == nil {
@@ -341,7 +367,7 @@ func (obj *NetRes) addrCheckApply(apply bool) (bool, error) {
if !apply { if !apply {
return false, nil return false, nil
} }
log.Printf("%s: addrCheckApply(%t)", obj, apply) obj.init.Logf("addrCheckApply(%t)", apply)
// check each address and delete the ones that aren't in the definition // check each address and delete the ones that aren't in the definition
if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil { if err := obj.iface.addrApplyDelete(obj.Addrs); err != nil {
@@ -391,7 +417,7 @@ func (obj *NetRes) gatewayCheckApply(apply bool) (bool, error) {
if !apply { if !apply {
return false, nil return false, nil
} }
log.Printf("%s: gatewayCheckApply(%t)", obj, apply) obj.init.Logf("gatewayCheckApply(%t)", apply)
// delete all but one default route // delete all but one default route
for i := 1; i < len(defRoutes); i++ { for i := 1; i < len(defRoutes); i++ {
@@ -436,7 +462,7 @@ func (obj *NetRes) fileCheckApply(apply bool) (bool, error) {
if !apply { if !apply {
return false, nil return false, nil
} }
log.Printf("%s: fileCheckApply(%t)", obj, apply) obj.init.Logf("fileCheckApply(%t)", apply)
// write the file // write the file
if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil { if err := ioutil.WriteFile(obj.unitFilePath, contents, networkdUnitFileUmask); err != nil {
@@ -492,70 +518,22 @@ func (obj *NetRes) CheckApply(apply bool) (checkOK bool, err error) {
return checkOK, nil return checkOK, nil
} }
// Close cleans up when we're done. // Cmp compares two resources and returns an error if they are not equivalent.
func (obj *NetRes) Close() error { func (obj *NetRes) Cmp(r engine.Res) error {
var errList error if !obj.Compare(r) {
// XXX: replace TempDir with VarDir return fmt.Errorf("did not compare")
if err := os.RemoveAll(obj.tempDir); err != nil {
errList = multierr.Append(errList, err)
} }
if err := obj.BaseRes.Close(); err != nil { return nil
errList = multierr.Append(errList, err)
}
return errList
}
// NetUID is a unique resource identifier.
type NetUID struct {
// NOTE: There is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
BaseUID
name string // the network interface name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *NetUID) IFF(uid ResUID) bool {
res, ok := uid.(*NetUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *NetRes) UIDs() []ResUID {
x := &NetUID{
BaseUID: BaseUID{Name: obj.GetName(), Kind: obj.GetKind()},
name: obj.Name,
}
return []ResUID{x}
}
// GroupCmp returns whether two resources can be grouped together or not.
func (obj *NetRes) GroupCmp(r Res) bool {
_, ok := r.(*NetRes)
if !ok {
return false
}
return false
} }
// Compare two resources and return if they are equivalent. // Compare two resources and return if they are equivalent.
func (obj *NetRes) Compare(r Res) bool { func (obj *NetRes) Compare(r engine.Res) bool {
// we can only compare NetRes to others of the same resource kind // we can only compare NetRes to others of the same resource kind
res, ok := r.(*NetRes) res, ok := r.(*NetRes)
if !ok { if !ok {
return false return false
} }
if !obj.BaseRes.Compare(res) {
return false
}
if obj.Name != res.Name {
return false
}
if obj.State != res.State { if obj.State != res.State {
return false return false
} }
@@ -572,6 +550,36 @@ func (obj *NetRes) Compare(r Res) bool {
return true return true
} }
// NetUID is a unique resource identifier.
type NetUID struct {
// NOTE: There is also a name variable in the BaseUID struct, this is
// information about where this UID came from, and is unrelated to the
// information about the resource we're matching. That data which is
// used in the IFF function, is what you see in the struct fields here.
engine.BaseUID
name string // the network interface name
}
// IFF aka if and only if they are equivalent, return true. If not, false.
func (obj *NetUID) IFF(uid engine.ResUID) bool {
res, ok := uid.(*NetUID)
if !ok {
return false
}
return obj.name == res.name
}
// UIDs includes all params to make a unique identification of this object.
// Most resources only return one although some resources can return multiple.
func (obj *NetRes) UIDs() []engine.ResUID {
x := &NetUID{
BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()},
name: obj.Name(),
}
return []engine.ResUID{x}
}
// UnmarshalYAML is the custom unmarshal handler for this struct. // UnmarshalYAML is the custom unmarshal handler for this struct.
// It is primarily useful for setting the defaults. // It is primarily useful for setting the defaults.
func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error { func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
@@ -596,7 +604,7 @@ func (obj *NetRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
func (obj *NetRes) unitFileContents() []byte { func (obj *NetRes) unitFileContents() []byte {
// build the unit file contents // build the unit file contents
u := []string{"[Match]"} u := []string{"[Match]"}
u = append(u, fmt.Sprintf("Name=%s", obj.GetName())) u = append(u, fmt.Sprintf("Name=%s", obj.Name()))
u = append(u, "[Network]") u = append(u, "[Network]")
for _, addr := range obj.Addrs { for _, addr := range obj.Addrs {
u = append(u, fmt.Sprintf("Address=%s", addr)) u = append(u, fmt.Sprintf("Address=%s", addr))

View File

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

View File

@@ -0,0 +1,102 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"reflect"
"testing"
"github.com/purpleidea/mgmt/engine"
)
func TestCmp1(t *testing.T) {
r1, err := engine.NewResource("noop")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
r2, err := engine.NewResource("noop")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
r3, err := engine.NewResource("file")
if err != nil {
t.Errorf("could not create resource: %+v", err)
}
if err := r1.Cmp(r2); err != nil {
t.Errorf("the two resources do not match: %+v", err)
}
if err := r2.Cmp(r1); err != nil {
t.Errorf("the two resources do not match: %+v", err)
}
if r1.Cmp(r3) == nil {
t.Errorf("the two resources should not match")
}
if r3.Cmp(r1) == nil {
t.Errorf("the two resources should not match")
}
}
func TestSort0(t *testing.T) {
rs := []engine.Res{}
s := engine.Sort(rs)
if !reflect.DeepEqual(s, []engine.Res{}) {
t.Errorf("sort failed!")
if s == nil {
t.Logf("output is nil!")
} else {
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
}
}
func TestSort1(t *testing.T) {
r1, _ := engine.NewNamedResource("noop", "noop1")
r2, _ := engine.NewNamedResource("noop", "noop2")
r3, _ := engine.NewNamedResource("noop", "noop3")
r4, _ := engine.NewNamedResource("noop", "noop4")
r5, _ := engine.NewNamedResource("noop", "noop5")
r6, _ := engine.NewNamedResource("noop", "noop6")
rs := []engine.Res{r3, r2, r6, r1, r5, r4}
s := engine.Sort(rs)
if !reflect.DeepEqual(s, []engine.Res{r1, r2, r3, r4, r5, r6}) {
t.Errorf("sort failed!")
str := "Got:"
for _, r := range s {
str += " " + r.String()
}
t.Errorf(str)
}
if !reflect.DeepEqual(rs, []engine.Res{r3, r2, r6, r1, r5, r4}) {
t.Errorf("sort modified input!")
str := "Got:"
for _, r := range rs {
str += " " + r.String()
}
t.Errorf(str)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,175 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package resources
import (
"reflect"
"testing"
"github.com/purpleidea/mgmt/engine/util"
)
func TestStructTagToFieldName0(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool `lang:"alpha" yaml:"nope"`
Beta string `yaml:"beta"`
Gamma string
Delta int `lang:"surprise"`
}
mapping, err := util.StructTagToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"alpha": "Alpha",
"surprise": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName0(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := util.LowerStructFieldNameToFieldName(&TestStruct{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"testres": "TestRes", // hide by specifying `lang:""` on it
"alpha": "Alpha",
//"skipme": "skipMe",
"beta": "Beta",
"iamacamel": "IAmACamel",
//"pass": "pass",
"gamma": "Gamma",
"delta": "Delta",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}
func TestLowerStructFieldNameToFieldName1(t *testing.T) {
type TestStruct struct {
TestRes // so that this struct implements `Res`
Alpha bool
skipMe bool
Beta string
// these two should collide
DoubleWord bool
Doubleword string
IAmACamel uint
pass *string
Gamma string
Delta int
}
mapping, err := util.LowerStructFieldNameToFieldName(&TestStruct{})
if err == nil {
t.Errorf("expected failure, but passed with: %+v", mapping)
return
}
}
func TestLowerStructFieldNameToFieldName2(t *testing.T) {
mapping, err := util.LowerStructFieldNameToFieldName(&TestRes{})
if err != nil {
t.Errorf("failed: %+v", err)
return
}
expected := map[string]string{
"base": "Base", // all resources have this trait
"groupable": "Groupable", // the TestRes has this trait
"refreshable": "Refreshable", // the TestRes has this trait
"sendable": "Sendable",
"recvable": "Recvable",
"bool": "Bool",
"str": "Str",
"int": "Int",
"int8": "Int8",
"int16": "Int16",
"int32": "Int32",
"int64": "Int64",
"uint": "Uint",
"uint8": "Uint8",
"uint16": "Uint16",
"uint32": "Uint32",
"uint64": "Uint64",
"byte": "Byte",
"rune": "Rune",
"float32": "Float32",
"float64": "Float64",
"complex64": "Complex64",
"complex128": "Complex128",
"boolptr": "BoolPtr",
"stringptr": "StringPtr",
"int64ptr": "Int64Ptr",
"int8ptr": "Int8Ptr",
"uint8ptr": "Uint8Ptr",
"int8ptrptrptr": "Int8PtrPtrPtr",
"slicestring": "SliceString",
"mapintfloat": "MapIntFloat",
"mixedstruct": "MixedStruct",
"interface": "Interface",
"anotherstr": "AnotherStr",
"validatebool": "ValidateBool",
"validateerror": "ValidateError",
"alwaysgroup": "AlwaysGroup",
"comparefail": "CompareFail",
"sendvalue": "SendValue",
"comment": "Comment",
}
if !reflect.DeepEqual(mapping, expected) {
t.Errorf("expected: %+v", expected)
t.Errorf("received: %+v", mapping)
}
}

View File

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

View File

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

View File

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

67
engine/sendrecv.go Normal file
View File

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

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

@@ -0,0 +1,42 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Edgeable contains a general implementation with some of the properties and
// methods needed to support autoedges on resources. It may be used as a start
// point to avoid re-implementing the straightforward methods.
type Edgeable struct {
meta *engine.AutoEdgeMeta
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// AutoEdgeMeta lets you get or set meta params for the automatic edges trait.
func (obj *Edgeable) AutoEdgeMeta() *engine.AutoEdgeMeta {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = &engine.AutoEdgeMeta{
Disabled: false,
}
}
return obj.meta
}

View File

@@ -0,0 +1,89 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"fmt"
"github.com/purpleidea/mgmt/engine"
)
// Groupable contains a general implementation with most of the properties and
// methods needed to support autogrouping on resources. It may be used as a
// starting point to avoid re-implementing the straightforward methods.
type Groupable struct {
meta *engine.AutoGroupMeta
isGrouped bool // am i contained within a group?
grouped []engine.GroupableRes // list of any grouped resources
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// AutoGroupMeta lets you get or set meta params for the automatic grouping
// trait.
func (obj *Groupable) AutoGroupMeta() *engine.AutoGroupMeta {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = &engine.AutoGroupMeta{
Disabled: false,
}
}
return obj.meta
}
// GroupCmp compares two resources and decides if they're suitable for grouping.
// You'll probably want to override this method when implementing a resource...
// This base implementation assumes not, so override me!
func (obj *Groupable) GroupCmp(res engine.GroupableRes) error {
return fmt.Errorf("the default grouping compare is not nil")
}
// GroupRes groups resource argument (res) into self.
func (obj *Groupable) GroupRes(res engine.GroupableRes) error {
if l := len(res.GetGroup()); l > 0 {
return fmt.Errorf("the `%s` resource already contains %d grouped resources", res, l)
}
if res.IsGrouped() {
return fmt.Errorf("the `%s` resource is already grouped", res)
}
obj.grouped = append(obj.grouped, res)
res.SetGrouped(true) // i am contained _in_ a group
return nil
}
// IsGrouped determines if we are grouped.
func (obj *Groupable) IsGrouped() bool { // am I grouped?
return obj.isGrouped
}
// SetGrouped sets a flag to tell if we are grouped.
func (obj *Groupable) SetGrouped(b bool) {
obj.isGrouped = b
}
// GetGroup returns everyone grouped inside me.
func (obj *Groupable) GetGroup() []engine.GroupableRes {
return obj.grouped
}
// SetGroup sets the grouped resources into me.
func (obj *Groupable) SetGroup(grouped []engine.GroupableRes) {
obj.grouped = grouped
}

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

@@ -0,0 +1,36 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Base contains all the minimum necessary structs to build a resource. It
// should be used as a starting point to avoid re-implementing the
// straightforward methods.
type Base struct {
Kinded
Named
Meta
}
// String returns a string representation of a resource.
func (obj *Base) String() string {
return engine.Repr(obj.Kind(), obj.Name())
}

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

@@ -0,0 +1,39 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Kinded contains a general implementation of the properties and methods needed
// to support the resource kind. It should be used as a starting point to avoid
// re-implementing the straightforward kind methods.
type Kinded struct {
kind string
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Kind returns the string representation for the kind this resource is.
func (obj *Kinded) Kind() string {
return obj.kind
}
// SetKind sets the kind string for this resource. It must only be set by the
// engine.
func (obj *Kinded) SetKind(kind string) {
obj.kind = kind
}

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

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Meta contains a general implementation of the properties and methods needed
// to support meta parameters. It should be used as a starting point to avoid
// re-implementing the straightforward meta methods.
type Meta struct {
meta *engine.MetaParams
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// MetaParams lets you get or set meta params for this trait.
func (obj *Meta) MetaParams() *engine.MetaParams {
if obj.meta == nil { // set the defaults if previously empty
obj.meta = engine.DefaultMetaParams.Copy()
}
return obj.meta
}

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

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Named contains a general implementation of the properties and methods needed
// to support named resources. It should be used as a starting point to avoid
// re-implementing the straightforward name methods.
type Named struct {
name string
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Name returns the unique name this resource has. It is only unique within its
// own kind.
func (obj *Named) Name() string {
return obj.name
}
// SetName sets the unique name for this resource. It must only be unique within
// its own kind.
func (obj *Named) SetName(name string) {
obj.name = name
}

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

@@ -0,0 +1,40 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
// Refreshable functions as flag storage for resources to signal that they
// support receiving refresh notifications, and what that value is. These are
// commonly used to send information that some aspect of the state is invalid
// due to an unlinked change. The canonical example is a svc resource that needs
// reloading after a configuration file changes.
type Refreshable struct {
refresh bool
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Refresh returns the refresh notification state.
func (obj *Refreshable) Refresh() bool {
return obj.refresh
}
// SetRefresh sets the refresh notification state.
func (obj *Refreshable) SetRefresh(b bool) {
obj.refresh = b
}

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

@@ -0,0 +1,75 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package traits
import (
"github.com/purpleidea/mgmt/engine"
)
// Sendable contains a general implementation with some of the properties and
// methods needed to implement sending from resources. You'll need to implement
// the Sends method, and call the Send method in CheckApply via the Init API.
type Sendable struct {
send interface{}
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// Sends returns a struct containing the defaults of the type we send. This
// needs to be implemented (overridden) by the struct with the Sendable trait to
// be able to send any values. The public struct field names are the keys used.
func (obj *Sendable) Sends() interface{} {
return nil
}
// Send is used to send a struct in CheckApply. This is typically wrapped in the
// resource API and consumed that way.
func (obj *Sendable) Send(st interface{}) error {
// TODO: can we (or should we) run the type checking here instead?
obj.send = st
return nil
}
// Sent returns the struct of values that have been sent by this resource.
func (obj *Sendable) Sent() interface{} {
return obj.send
}
// Recvable contains a general implementation with some of the properties and
// methods needed to implement receiving from resources.
type Recvable struct {
recv map[string]*engine.Send
// Bug5819 works around issue https://github.com/golang/go/issues/5819
Bug5819 interface{} // XXX: workaround
}
// SetRecv is used to inject incoming values into the resource.
func (obj *Recvable) SetRecv(recv map[string]*engine.Send) {
//if obj.recv == nil {
// obj.recv = make(map[string]*engine.Send)
//}
obj.recv = recv
}
// Recv is used to get information that was passed in. This data can then be
// used to run the Send/Recv data transfer.
func (obj *Recvable) Recv() map[string]*engine.Send {
return obj.recv
}

41
engine/util.go Normal file
View File

@@ -0,0 +1,41 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"sort"
)
// ResourceSlice is a linear list of resources. It can be sorted.
type ResourceSlice []Res
func (rs ResourceSlice) Len() int { return len(rs) }
func (rs ResourceSlice) Swap(i, j int) { rs[i], rs[j] = rs[j], rs[i] }
func (rs ResourceSlice) Less(i, j int) bool { return rs[i].String() < rs[j].String() }
// Sort the list of resources and return a copy without modifying the input.
func Sort(rs []Res) []Res {
resources := []Res{}
for _, r := range rs { // copy
resources = append(resources, r)
}
sort.Sort(ResourceSlice(resources))
return resources
// sort.Sort(ResourceSlice(rs)) // this is wrong, it would modify input!
//return rs
}

View File

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

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

@@ -0,0 +1,105 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package util
import (
"os/user"
"strconv"
"testing"
)
func TestUnknownGroup(t *testing.T) {
gid, err := GetGID("unknowngroup")
if err == nil {
t.Errorf("expected failure, but passed with: %d", gid)
}
}
func TestUnknownUser(t *testing.T) {
uid, err := GetUID("unknownuser")
if err == nil {
t.Errorf("expected failure, but passed with: %d", uid)
}
}
func TestCurrentUserGroupByName(t *testing.T) {
// get current user
userObj, err := user.Current()
if err != nil {
t.Errorf("error trying to lookup current user: %s", err.Error())
}
currentUID := userObj.Uid
currentGID := userObj.Gid
var uid int
var gid int
// now try to get the uid/gid via our API (via username and group name)
if uid, err = GetUID(userObj.Username); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(uid) != currentUID {
t.Errorf("uid didn't match current user's: %s vs %s", strconv.Itoa(uid), currentUID)
}
// macOS users do not have a group with their name on it, so not assuming this here
group, err := user.LookupGroupId(currentGID)
if err != nil {
t.Errorf("failed to lookup group by id: %s", currentGID)
}
if gid, err = GetGID(group.Name); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(gid) != currentGID {
t.Errorf("gid didn't match current user's: %s vs %s", strconv.Itoa(gid), currentGID)
}
}
func TestCurrentUserGroupById(t *testing.T) {
// get current user
userObj, err := user.Current()
if err != nil {
t.Errorf("error trying to lookup current user: %s", err.Error())
}
currentUID := userObj.Uid
currentGID := userObj.Gid
var uid int
var gid int
// now try to get the uid/gid via our API (via uid and gid)
if uid, err = GetUID(currentUID); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(uid) != currentUID {
t.Errorf("uid didn't match current user's: %s vs %s", strconv.Itoa(uid), currentUID)
}
if gid, err = GetGID(currentGID); err != nil {
t.Errorf("error trying to lookup current user UID: %s", err.Error())
}
if strconv.Itoa(gid) != currentGID {
t.Errorf("gid didn't match current user's: %s vs %s", strconv.Itoa(gid), currentGID)
}
}

48
engine/world.go Normal file
View File

@@ -0,0 +1,48 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package engine
import (
"github.com/purpleidea/mgmt/etcd/scheduler"
)
// World is an interface to the rest of the different graph state. It allows
// the GAPI to store state and exchange information throughout the cluster. It
// is the interface each machine uses to communicate with the rest of the world.
type World interface { // TODO: is there a better name for this interface?
ResWatch() chan error
ResExport([]Res) error
// FIXME: should this method take a "filter" data struct instead of many args?
ResCollect(hostnameFilter, kindFilter []string) ([]Res, error)
StrWatch(namespace string) chan error
StrIsNotExist(error) bool
StrGet(namespace string) (string, error)
StrSet(namespace, value string) error
StrDel(namespace string) error
// XXX: add the exchange primitives in here directly?
StrMapWatch(namespace string) chan error
StrMapGet(namespace string) (map[string]string, error)
StrMapSet(namespace, value string) error
StrMapDel(namespace string) error
Scheduler(namespace string, opts ...scheduler.Option) (*scheduler.Result, error)
Fs(uri string) (Fs, error)
}

View File

@@ -32,7 +32,7 @@
// * If a seed is given, connect as a client, and optionally volunteer to be a server. // * If a seed is given, connect as a client, and optionally volunteer to be a server.
// * All volunteering clients should listen for a message from the master for nomination. // * All volunteering clients should listen for a message from the master for nomination.
// * If a client has been nominated, it should startup a server. // * If a client has been nominated, it should startup a server.
// * All servers should list for their nomination to be removed and shutdown if so. // * All servers should listen for their nomination to be removed and shutdown if so.
// * The elected leader should decide who to nominate/unnominate to keep the right number of servers. // * The elected leader should decide who to nominate/unnominate to keep the right number of servers.
// //
// Smoke testing: // Smoke testing:
@@ -64,7 +64,7 @@ import (
"time" "time"
"github.com/purpleidea/mgmt/converger" "github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/event" "github.com/purpleidea/mgmt/etcd/event"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
etcd "github.com/coreos/etcd/clientv3" // "clientv3" etcd "github.com/coreos/etcd/clientv3" // "clientv3"
@@ -283,6 +283,13 @@ func NewEmbdEtcd(hostname string, seeds, clientURLs, serverURLs, advertiseClient
obj.serverURLs = []url.URL{*u} obj.serverURLs = []url.URL{*u}
} }
if converger != nil {
converger.AddStateFn("etcd-hostname", func(converged bool) error {
// send our individual state into etcd for others to see
return SetHostnameConverged(obj, hostname, converged) // TODO: what should happen on error?
})
}
return obj return obj
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib" mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -52,7 +52,7 @@ func NewMyGAPI(data gapi.Data, name string, interval uint, count uint) (*MyGAPI,
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *MyGAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) { if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" { if s != "" {
return nil, fmt.Errorf("input is not empty") return nil, fmt.Errorf("input is not empty")
@@ -107,13 +107,13 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
} }
var vertex pgraph.Vertex var vertex pgraph.Vertex
for i := uint(0); i < obj.Count; i++ { for i := uint(0); i < obj.Count; i++ {
n, err := resources.NewNamedResource("noop", fmt.Sprintf("noop%d", i)) n, err := engine.NewNamedResource("noop", fmt.Sprintf("noop%d", i))
if err != nil { if err != nil {
return nil, err return nil, err
} }
g.AddVertex(n) g.AddVertex(n)
if i > 0 { if i > 0 {
g.AddEdge(vertex, n, &resources.Edge{Name: fmt.Sprintf("e%d", i)}) g.AddEdge(vertex, n, &engine.Edge{Name: fmt.Sprintf("e%d", i)})
} }
vertex = n // save vertex = n // save
} }

View File

@@ -10,10 +10,11 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
mgmt "github.com/purpleidea/mgmt/lib" mgmt "github.com/purpleidea/mgmt/lib"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -49,7 +50,7 @@ func NewMyGAPI(data gapi.Data, name string, interval uint) (*MyGAPI, error) {
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *MyGAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *MyGAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(obj.Name); c.IsSet(obj.Name) { if s := c.String(obj.Name); c.IsSet(obj.Name) {
if s != "" { if s != "" {
return nil, fmt.Errorf("input is not empty") return nil, fmt.Errorf("input is not empty")
@@ -103,15 +104,8 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
return nil, err return nil, err
} }
metaparams := resources.DefaultMetaParams
content := "Delete me to trigger a notification!\n" content := "Delete me to trigger a notification!\n"
f0 := &resources.FileRes{ f0 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "README",
Kind: "file",
MetaParams: metaparams,
},
Path: "/tmp/mgmt/README", Path: "/tmp/mgmt/README",
Content: &content, Content: &content,
State: "present", State: "present",
@@ -120,50 +114,34 @@ func (obj *MyGAPI) Graph() (*pgraph.Graph, error) {
g.AddVertex(f0) g.AddVertex(f0)
p1 := &resources.PasswordRes{ p1 := &resources.PasswordRes{
BaseRes: resources.BaseRes{
Name: "password1",
Kind: "password",
MetaParams: metaparams,
},
Length: 8, // generated string will have this many characters Length: 8, // generated string will have this many characters
Saved: true, // this causes passwords to be stored in plain text! Saved: true, // this causes passwords to be stored in plain text!
} }
g.AddVertex(p1) g.AddVertex(p1)
f1 := &resources.FileRes{ f1 := &resources.FileRes{
BaseRes: resources.BaseRes{
Name: "file1",
Kind: "file",
MetaParams: metaparams,
// send->recv!
Recv: map[string]*resources.Send{
"Content": {Res: p1, Key: "Password"},
},
},
Path: "/tmp/mgmt/secret", Path: "/tmp/mgmt/secret",
//Content: p1.Password, // won't work //Content: p1.Password, // won't work
State: "present", State: "present",
} }
// XXX: add send->recv!
//Recv: map[string]*engine.Send{
// "Content": {Res: p1, Key: "Password"},
//},
g.AddVertex(f1) g.AddVertex(f1)
n1 := &resources.NoopRes{ n1 := &resources.NoopRes{}
BaseRes: resources.BaseRes{
Name: "noop1",
Kind: "noop",
MetaParams: metaparams,
},
}
g.AddVertex(n1) g.AddVertex(n1)
e0 := &resources.Edge{Name: "e0"} e0 := &engine.Edge{Name: "e0"}
e0.Notify = true // send a notification from f0 to p1 e0.Notify = true // send a notification from f0 to p1
g.AddEdge(f0, p1, e0) g.AddEdge(f0, p1, e0)
g.AddEdge(p1, f1, &resources.Edge{Name: "e1"}) g.AddEdge(p1, f1, &engine.Edge{Name: "e1"})
e2 := &resources.Edge{Name: "e2"} e2 := &engine.Edge{Name: "e2"}
e2.Notify = true // send a notification from f1 to n1 e2.Notify = true // send a notification from f1 to n1
g.AddEdge(f1, n1, e2) g.AddEdge(f1, n1, e2)

View File

@@ -34,7 +34,7 @@ func init() {
// TODO: add staged rollout functionality to this struct // TODO: add staged rollout functionality to this struct
// TODO: add proper authentication with gpg key signing // TODO: add proper authentication with gpg key signing
type Deploy struct { type Deploy struct {
Name string // lang, hcl, puppet, yaml, yaml2, etc... Name string // lang, puppet, yaml, etc...
//Sync bool // wait for everyone to close previous GAPI before switching //Sync bool // wait for everyone to close previous GAPI before switching
Noop bool Noop bool
Sema int // sema override Sema int // sema override

150
gapi/empty/empty.go Normal file
View File

@@ -0,0 +1,150 @@
// Mgmt
// Copyright (C) 2013-2018+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package empty
import (
"fmt"
"sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/gapi"
"github.com/purpleidea/mgmt/pgraph"
"github.com/urfave/cli"
)
const (
// Name is the name of this frontend.
Name = "empty"
)
func init() {
gapi.Register(Name, func() gapi.GAPI { return &GAPI{} }) // register
}
// GAPI implements the main lang GAPI interface.
type GAPI struct {
data gapi.Data
initialized bool
closeChan chan struct{}
wg *sync.WaitGroup // sync group for tunnel go routines
}
// Cli takes a cli.Context, and returns our GAPI if activated. All arguments
// should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error.
func (obj *GAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(Name); c.IsSet(Name) {
if s == "" {
return nil, fmt.Errorf("input code is empty")
}
return &gapi.Deploy{
Name: Name,
//Noop: false,
GAPI: &GAPI{},
}, nil
}
return nil, nil // we weren't activated!
}
// CliFlags returns a list of flags used by this deploy subcommand.
func (obj *GAPI) CliFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: Name,
Value: "",
Usage: "empty graph to deploy",
},
}
}
// Init initializes the lang GAPI struct.
func (obj *GAPI) Init(data gapi.Data) error {
if obj.initialized {
return fmt.Errorf("already initialized")
}
obj.data = data // store for later
obj.closeChan = make(chan struct{})
obj.wg = &sync.WaitGroup{}
obj.initialized = true
return nil
}
// Graph returns a current Graph.
func (obj *GAPI) Graph() (*pgraph.Graph, error) {
if !obj.initialized {
return nil, fmt.Errorf("%s: GAPI is not initialized", Name)
}
obj.data.Logf("generating empty graph...")
g, err := pgraph.NewGraph("empty")
if err != nil {
return nil, err
}
return g, nil
}
// Next returns nil errors every time there could be a new graph.
func (obj *GAPI) Next() chan gapi.Next {
ch := make(chan gapi.Next)
obj.wg.Add(1)
go func() {
defer obj.wg.Done()
defer close(ch) // this will run before the obj.wg.Done()
if !obj.initialized {
next := gapi.Next{
Err: fmt.Errorf("%s: GAPI is not initialized", Name),
Exit: true, // exit, b/c programming error?
}
select {
case ch <- next:
case <-obj.closeChan:
}
return
}
// send only one event
next := gapi.Next{
Exit: false,
Err: nil,
}
select {
case ch <- next: // trigger a run (send a msg)
// pass
// unblock if we exit while waiting to send!
case <-obj.closeChan:
return
}
}()
return ch
}
// Close shuts down the lang GAPI.
func (obj *GAPI) Close() error {
if !obj.initialized {
return fmt.Errorf("%s: GAPI is not initialized", Name)
}
close(obj.closeChan)
obj.wg.Wait()
obj.initialized = false // closed = true
return nil
}

View File

@@ -22,8 +22,8 @@ import (
"encoding/gob" "encoding/gob"
"fmt" "fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@@ -46,7 +46,7 @@ func Register(name string, fn func() GAPI) {
type Data struct { type Data struct {
Program string // name of the originating program Program string // name of the originating program
Hostname string // uuid for the host, required for GAPI Hostname string // uuid for the host, required for GAPI
World resources.World World engine.World
Noop bool Noop bool
NoConfigWatch bool NoConfigWatch bool
NoStreamWatch bool NoStreamWatch bool
@@ -70,7 +70,7 @@ type Next struct {
// GAPI is a Graph API that represents incoming graphs and change streams. // GAPI is a Graph API that represents incoming graphs and change streams.
type GAPI interface { type GAPI interface {
Cli(c *cli.Context, fs resources.Fs) (*Deploy, error) Cli(c *cli.Context, fs engine.Fs) (*Deploy, error)
CliFlags() []cli.Flag CliFlags() []cli.Flag
Init(Data) error // initializes the GAPI and passes in useful data Init(Data) error // initializes the GAPI and passes in useful data

View File

@@ -20,7 +20,7 @@ package gapi
import ( import (
"io/ioutil" "io/ioutil"
"github.com/purpleidea/mgmt/resources" "github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -31,7 +31,7 @@ import (
const Umask = 0666 const Umask = 0666
// CopyFileToFs copies a file from src path on the local fs to a dst path on fs. // CopyFileToFs copies a file from src path on the local fs to a dst path on fs.
func CopyFileToFs(fs resources.Fs, src, dst string) error { func CopyFileToFs(fs engine.Fs, src, dst string) error {
data, err := ioutil.ReadFile(src) data, err := ioutil.ReadFile(src)
if err != nil { if err != nil {
return errwrap.Wrapf(err, "can't read from file `%s`", src) return errwrap.Wrapf(err, "can't read from file `%s`", src)
@@ -44,7 +44,7 @@ func CopyFileToFs(fs resources.Fs, src, dst string) error {
// CopyStringToFs copies a file from src path on the local fs to a dst path on // CopyStringToFs copies a file from src path on the local fs to a dst path on
// fs. // fs.
func CopyStringToFs(fs resources.Fs, str, dst string) error { func CopyStringToFs(fs engine.Fs, str, dst string) error {
if err := fs.WriteFile(dst, []byte(str), Umask); err != nil { if err := fs.WriteFile(dst, []byte(str), Umask); err != nil {
return errwrap.Wrapf(err, "can't write to file `%s`", dst) return errwrap.Wrapf(err, "can't write to file `%s`", dst)
} }
@@ -52,6 +52,6 @@ func CopyStringToFs(fs resources.Fs, str, dst string) error {
} }
// CopyDirToFs copies a dir from src path on the local fs to a dst path on fs. // CopyDirToFs copies a dir from src path on the local fs to a dst path on fs.
func CopyDirToFs(fs resources.Fs, src, dst string) error { func CopyDirToFs(fs engine.Fs, src, dst string) error {
return util.CopyDiskToFs(fs, src, dst, false) return util.CopyDiskToFs(fs, src, dst, false)
} }

View File

@@ -24,6 +24,8 @@ import (
"path" "path"
"sort" "sort"
"testing" "testing"
"github.com/purpleidea/mgmt/util"
) )
func TestInstance0(t *testing.T) { func TestInstance0(t *testing.T) {
@@ -67,7 +69,7 @@ func TestInstance1(t *testing.T) {
values := []test{} values := []test{}
{ {
code := Code(` code := util.Code(`
$root = getenv("MGMT_TEST_ROOT") $root = getenv("MGMT_TEST_ROOT")
file "${root}/mgmt-hello-world" { file "${root}/mgmt-hello-world" {
@@ -150,7 +152,7 @@ func TestCluster1(t *testing.T) {
values := []test{} values := []test{}
{ {
code := Code(` code := util.Code(`
$root = getenv("MGMT_TEST_ROOT") $root = getenv("MGMT_TEST_ROOT")
file "${root}/mgmt-hostname" { file "${root}/mgmt-hostname" {
@@ -174,7 +176,7 @@ func TestCluster1(t *testing.T) {
}) })
} }
{ {
code := Code(` code := util.Code(`
$root = getenv("MGMT_TEST_ROOT") $root = getenv("MGMT_TEST_ROOT")
file "${root}/mgmt-hostname" { file "${root}/mgmt-hostname" {

View File

@@ -52,11 +52,11 @@ const (
// longTimeout is a high bound of time we're willing to wait for events. // longTimeout is a high bound of time we're willing to wait for events.
// If we exceed this timeout, then it's likely we are blocked somewhere. // If we exceed this timeout, then it's likely we are blocked somewhere.
longTimeout = 30 // seconds longTimeout = 60 // seconds
// convergedTimeout is the number of seconds we wait for our instance to // convergedTimeout is the number of seconds we wait for our instance to
// remain unchanged to be considered as converged. // remain unchanged to be considered as converged.
convergedTimeout = 5 // seconds convergedTimeout = 15 // seconds
// dirMode is the the mode used when making directories. // dirMode is the the mode used when making directories.
dirMode = 0755 dirMode = 0755
@@ -297,7 +297,7 @@ func (obj *Instance) Wait(ctx context.Context) error {
for { for {
select { select {
// FIXME: instead of sending one event here, the recwatch // FIXME: instead of sending one event here, the recwatch
// library should sent one initial event at startup... // library should send one initial event at startup...
case <-startup: case <-startup:
startup = nil startup = nil
// send an initial event // send an initial event

View File

@@ -25,7 +25,6 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv" "strconv"
"strings"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
@@ -48,37 +47,6 @@ func BinaryPath() (string, error) {
return path.Join(root, binaryName), nil return path.Join(root, binaryName), nil
} }
// Code takes a code block as a backtick enclosed `heredoc` and removes any
// common indentation from each line. This helps inline code as strings to be
// formatted nicely without unnecessary indentation. It also drops the very
// first line of code if it has zero length.
func Code(code string) string {
output := []string{}
lines := strings.Split(code, "\n")
var found bool
var strip string // prefix to remove
for i, x := range lines {
if !found && len(x) > 0 {
for j := 0; j < len(x); j++ {
if x[j] != '\t' {
break
}
strip += "\t"
}
// otherwise, there's no indentation
found = true
}
if i == 0 && len(x) == 0 { // drop first line if it's empty
continue
}
s := strings.TrimPrefix(x, strip)
output = append(output, s)
}
return strings.Join(output, "\n")
}
// ParsePort parses a URL and returns the port that was found. // ParsePort parses a URL and returns the port that was found.
func ParsePort(input string) (int, error) { func ParsePort(input string) (int, error) {
u, err := url.Parse(input) u, err := url.Parse(input)

View File

@@ -41,29 +41,6 @@ func TestBinaryPath(t *testing.T) {
_ = fi _ = fi
} }
func TestCodeIndent(t *testing.T) {
c1 := Code(
`
$root = getenv("MGMT_TEST_ROOT")
file "${root}/mgmt-hello-world" {
content => "hello world from @purpleidea\n",
state => "exists",
}
`)
c2 :=
`$root = getenv("MGMT_TEST_ROOT")
file "${root}/mgmt-hello-world" {
content => "hello world from @purpleidea\n",
state => "exists",
}
`
if c1 != c2 {
t.Errorf("code samples differ")
}
}
func TestParsePort(t *testing.T) { func TestParsePort(t *testing.T) {
if port, err := ParsePort("http://127.0.0.1:2379"); err != nil { if port, err := ParsePort("http://127.0.0.1:2379"); err != nil {
t.Errorf("could not determine port: %+v", err) t.Errorf("could not determine port: %+v", err)

View File

@@ -22,10 +22,10 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
multierr "github.com/hashicorp/go-multierror" multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -103,7 +103,7 @@ func (obj *Edge) String() string {
type Engine struct { type Engine struct {
Graph *pgraph.Graph Graph *pgraph.Graph
Hostname string Hostname string
World resources.World World engine.World
Debug bool Debug bool
Logf func(format string, v ...interface{}) Logf func(format string, v ...interface{})
@@ -161,15 +161,15 @@ func (obj *Engine) Init() error {
return fmt.Errorf("vertex (%+v) was not an expr", vertex) return fmt.Errorf("vertex (%+v) was not an expr", vertex)
} }
if obj.Debug {
obj.Logf("Loading func `%s`", vertex)
}
obj.state[vertex] = &State{Expr: expr} // store some state! obj.state[vertex] = &State{Expr: expr} // store some state!
if e := obj.state[vertex].Init(); e != nil { if e := obj.state[vertex].Init(); e != nil {
err = multierr.Append(err, e) // list of errors err = multierr.Append(err, e) // list of errors
} }
if obj.Debug {
obj.Logf("Loading func `%s`", vertex)
}
} }
if err != nil { // usually due to `not found` errors if err != nil { // usually due to `not found` errors
return errwrap.Wrapf(err, "could not load requested funcs") return errwrap.Wrapf(err, "could not load requested funcs")

View File

@@ -21,10 +21,10 @@ package facts
import ( import (
"fmt" "fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/resources"
) )
// RegisteredFacts is a global map of all possible facts which can be used. You // RegisteredFacts is a global map of all possible facts which can be used. You
@@ -61,7 +61,7 @@ type Init struct {
Hostname string // uuid for the host Hostname string // uuid for the host
//Noop bool //Noop bool
Output chan types.Value // Stream must close `output` chan Output chan types.Value // Stream must close `output` chan
World resources.World World engine.World
} }
// Fact is the interface that any valid fact must fulfill. It is very simple, // Fact is the interface that any valid fact must fulfill. It is very simple,

View File

@@ -19,13 +19,12 @@ package lang
import ( import (
"fmt" "fmt"
"log"
"strings" "strings"
"sync" "sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
multierr "github.com/hashicorp/go-multierror" multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -59,7 +58,7 @@ type GAPI struct {
// should take the prefix of the registered name. On activation, if there are // should take the prefix of the registered name. On activation, if there are
// any validation problems, you should return an error. If this was not // any validation problems, you should return an error. If this was not
// activated, then you should return a nil GAPI and a nil error. // activated, then you should return a nil GAPI and a nil error.
func (obj *GAPI) Cli(c *cli.Context, fs resources.Fs) (*gapi.Deploy, error) { func (obj *GAPI) Cli(c *cli.Context, fs engine.Fs) (*gapi.Deploy, error) {
if s := c.String(Name); c.IsSet(Name) { if s := c.String(Name); c.IsSet(Name) {
if s == "" { if s == "" {
return nil, fmt.Errorf("input code is empty") return nil, fmt.Errorf("input code is empty")
@@ -135,6 +134,10 @@ func (obj *GAPI) LangInit() error {
Hostname: obj.data.Hostname, Hostname: obj.data.Hostname,
World: obj.data.World, World: obj.data.World,
Debug: obj.data.Debug, Debug: obj.data.Debug,
Logf: func(format string, v ...interface{}) {
// TODO: add the Name prefix in parent logger
obj.data.Logf(Name+": "+format, v...)
},
} }
if err := obj.lang.Init(); err != nil { if err := obj.lang.Init(); err != nil {
return errwrap.Wrapf(err, "can't init the lang") return errwrap.Wrapf(err, "can't init the lang")
@@ -209,11 +212,11 @@ func (obj *GAPI) Next() chan gapi.Next {
case <-obj.closeChan: case <-obj.closeChan:
return return
} }
log.Printf("%s: Generating new graph...", Name) obj.data.Logf("generating new graph...")
// skip this to pass through the err if present // skip this to pass through the err if present
if langSwap && err == nil { if langSwap && err == nil {
log.Printf("%s: Swap!", Name) obj.data.Logf("swap!")
// run up to these three but fail on err // run up to these three but fail on err
if e := obj.LangClose(); e != nil { // close any old lang if e := obj.LangClose(); e != nil { // close any old lang
err = e // pass through the err err = e // pass through the err
@@ -230,7 +233,29 @@ func (obj *GAPI) Next() chan gapi.Next {
} else { } else {
if obj.data.NoStreamWatch { // TODO: do we want to allow this for the lang? if obj.data.NoStreamWatch { // TODO: do we want to allow this for the lang?
streamChan = nil obj.data.Logf("warning: language will not stream")
// send only one event
limitChan := make(chan error)
obj.wg.Add(1)
go func() {
defer obj.wg.Done()
defer close(limitChan)
select {
// only one
case err, ok := <-obj.lang.Stream():
if !ok {
return
}
select {
case limitChan <- err:
case <-obj.closeChan:
return
}
case <-obj.closeChan:
return
}
}()
streamChan = limitChan
} else { } else {
// stream for lang events // stream for lang events
streamChan = obj.lang.Stream() // update stream streamChan = obj.lang.Stream() // update stream

View File

@@ -18,9 +18,9 @@
package interfaces package interfaces
import ( import (
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
) )
const ( const (
@@ -111,7 +111,7 @@ type Edge struct {
// Output is a collection of data returned by a Stmt. // Output is a collection of data returned by a Stmt.
type Output struct { // returned by Stmt type Output struct { // returned by Stmt
Resources []resources.Res Resources []engine.Res
Edges []*Edge Edges []*Edge
//Exported []*Exports // TODO: add exported resources //Exported []*Exports // TODO: add exported resources
} }
@@ -120,7 +120,7 @@ type Output struct { // returned by Stmt
// lists initialized appropriately. // lists initialized appropriately.
func (obj *Output) Empty() *Output { func (obj *Output) Empty() *Output {
return &Output{ return &Output{
Resources: []resources.Res{}, Resources: []engine.Res{},
Edges: []*Edge{}, Edges: []*Edge{},
} }
} }

View File

@@ -18,8 +18,8 @@
package interfaces package interfaces
import ( import (
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/resources"
) )
// Info is a static representation of some information about the function. It is // Info is a static representation of some information about the function. It is
@@ -39,7 +39,7 @@ type Init struct {
//Noop bool //Noop bool
Input chan types.Value // Engine will close `input` chan Input chan types.Value // Engine will close `input` chan
Output chan types.Value // Stream must close `output` chan Output chan types.Value // Stream must close `output` chan
World resources.World World engine.World
Debug bool Debug bool
Logf func(format string, v ...interface{}) Logf func(format string, v ...interface{})
} }

View File

@@ -20,9 +20,9 @@ package lang // TODO: move this into a sub package of lang/$name?
import ( import (
"fmt" "fmt"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
@@ -42,26 +42,26 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
return nil, errwrap.Wrapf(err, "could not create new graph") return nil, errwrap.Wrapf(err, "could not create new graph")
} }
var lookup = make(map[string]map[string]resources.Res) // map[kind]map[name]Res var lookup = make(map[string]map[string]engine.Res) // map[kind]map[name]Res
// build the send/recv mapping; format: map[kind]map[name]map[field]*Send // build the send/recv mapping; format: map[kind]map[name]map[field]*Send
var receive = make(map[string]map[string]map[string]*resources.Send) var receive = make(map[string]map[string]map[string]*engine.Send)
for _, res := range output.Resources { for _, res := range output.Resources {
graph.AddVertex(res) graph.AddVertex(res)
kind := res.GetKind() kind := res.Kind()
name := res.GetName() name := res.Name()
if _, exists := lookup[kind]; !exists { if _, exists := lookup[kind]; !exists {
lookup[kind] = make(map[string]resources.Res) lookup[kind] = make(map[string]engine.Res)
receive[kind] = make(map[string]map[string]*resources.Send) receive[kind] = make(map[string]map[string]*engine.Send)
} }
if _, exists := receive[kind][name]; !exists { if _, exists := receive[kind][name]; !exists {
receive[kind][name] = make(map[string]*resources.Send) receive[kind][name] = make(map[string]*engine.Send)
} }
if r, exists := lookup[kind][name]; exists { // found same name if r, exists := lookup[kind][name]; exists { // found same name
if !r.Compare(res) { if err := engine.ResCmp(r, res); err != nil {
// TODO: print a diff of the two resources // TODO: print a diff of the two resources
return nil, fmt.Errorf("incompatible duplicate resource `%s` found", res) return nil, errwrap.Wrapf(err, "incompatible duplicate resource `%s` found", res)
} }
// more than one compatible resource exists... we allow // more than one compatible resource exists... we allow
// duplicates, if they're going to not conflict... // duplicates, if they're going to not conflict...
@@ -71,9 +71,9 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
} }
for _, e := range output.Edges { for _, e := range output.Edges {
var v1, v2 resources.Res var v1, v2 engine.Res
var exists bool // = true var exists bool // = true
var m map[string]resources.Res var m map[string]engine.Res
var notify = e.Notify var notify = e.Notify
if m, exists = lookup[e.Kind1]; exists { if m, exists = lookup[e.Kind1]; exists {
@@ -91,10 +91,10 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
if existingEdge := graph.FindEdge(v1, v2); existingEdge != nil { if existingEdge := graph.FindEdge(v1, v2); existingEdge != nil {
// collate previous Notify signals to this edge with OR // collate previous Notify signals to this edge with OR
notify = notify || (existingEdge.(*resources.Edge)).Notify notify = notify || (existingEdge.(*engine.Edge)).Notify
} }
edge := &resources.Edge{ edge := &engine.Edge{
Name: fmt.Sprintf("%s -> %s", v1, v2), Name: fmt.Sprintf("%s -> %s", v1, v2),
Notify: notify, Notify: notify,
} }
@@ -117,8 +117,14 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
} }
} }
res, ok := v1.(engine.SendableRes)
if !ok {
return nil, fmt.Errorf("cannot send from resource: %s", engine.Stringer(v1))
}
// XXX: type check the send/recv relationship somewhere
// store mapping for later // store mapping for later
receive[e.Kind2][e.Name2][e.Recv] = &resources.Send{Res: v1, Key: e.Send} receive[e.Kind2][e.Name2][e.Recv] = &engine.Send{Res: res, Key: e.Send}
} }
// we need to first build up a map of all the resources handles, because // we need to first build up a map of all the resources handles, because
@@ -130,7 +136,15 @@ func interpret(ast interfaces.Stmt) (*pgraph.Graph, error) {
// TODO: do this in a deterministic order // TODO: do this in a deterministic order
for kind, x := range receive { for kind, x := range receive {
for name, recv := range x { for name, recv := range x {
lookup[kind][name].SetRecv(recv) // set it! if len(recv) == 0 { // skip empty maps from allocation!
continue
}
r := lookup[kind][name]
res, ok := r.(engine.RecvableRes)
if !ok {
return nil, fmt.Errorf("cannot recv to resource: %s", engine.Repr(kind, name))
}
res.SetRecv(recv) // set it!
} }
} }

View File

@@ -22,10 +22,11 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/lang/unification"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
) )
func vertexAstCmpFn(v1, v2 pgraph.Vertex) (bool, error) { func vertexAstCmpFn(v1, v2 pgraph.Vertex) (bool, error) {
@@ -496,7 +497,7 @@ func TestAstInterpret0(t *testing.T) {
} }
{ {
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
t1, _ := resources.NewNamedResource("test", "t1") t1, _ := engine.NewNamedResource("test", "t1")
x := t1.(*resources.TestRes) x := t1.(*resources.TestRes)
int64ptr := int64(42) int64ptr := int64(42)
x.Int64Ptr = &int64ptr x.Int64Ptr = &int64ptr
@@ -522,7 +523,7 @@ func TestAstInterpret0(t *testing.T) {
} }
{ {
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
t1, _ := resources.NewNamedResource("test", "t1") t1, _ := engine.NewNamedResource("test", "t1")
x := t1.(*resources.TestRes) x := t1.(*resources.TestRes)
stringptr := "wow" stringptr := "wow"
x.StringPtr = &stringptr x.StringPtr = &stringptr
@@ -540,21 +541,21 @@ func TestAstInterpret0(t *testing.T) {
{ {
// FIXME: add a better vertexCmpFn so we can compare send/recv! // FIXME: add a better vertexCmpFn so we can compare send/recv!
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
t1, _ := resources.NewNamedResource("test", "t1") t1, _ := engine.NewNamedResource("test", "t1")
{ {
x := t1.(*resources.TestRes) x := t1.(*resources.TestRes)
int64Ptr := int64(42) int64Ptr := int64(42)
x.Int64Ptr = &int64Ptr x.Int64Ptr = &int64Ptr
graph.AddVertex(t1) graph.AddVertex(t1)
} }
t2, _ := resources.NewNamedResource("test", "t2") t2, _ := engine.NewNamedResource("test", "t2")
{ {
x := t2.(*resources.TestRes) x := t2.(*resources.TestRes)
int64Ptr := int64(13) int64Ptr := int64(13)
x.Int64Ptr = &int64Ptr x.Int64Ptr = &int64Ptr
graph.AddVertex(t2) graph.AddVertex(t2)
} }
edge := &resources.Edge{ edge := &engine.Edge{
Name: fmt.Sprintf("%s -> %s", t1, t2), Name: fmt.Sprintf("%s -> %s", t1, t2),
Notify: false, Notify: false,
} }

View File

@@ -20,9 +20,9 @@ package lang // TODO: move this into a sub package of lang/$name?
import ( import (
"fmt" "fmt"
"io" "io"
"log"
"sync" "sync"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs"
_ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register _ "github.com/purpleidea/mgmt/lang/funcs/core" // import so the funcs register
_ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // import so the facts register _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // import so the facts register
@@ -31,7 +31,6 @@ import (
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/lang/unification"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
) )
@@ -50,8 +49,9 @@ const (
type Lang struct { type Lang struct {
Input io.Reader // os.Stdin or anything that satisfies this interface Input io.Reader // os.Stdin or anything that satisfies this interface
Hostname string Hostname string
World resources.World World engine.World
Debug bool Debug bool
Logf func(format string, v ...interface{})
ast interfaces.Stmt // store main prog AST here ast interfaces.Stmt // store main prog AST here
funcs *funcs.Engine // function event engine funcs *funcs.Engine // function event engine
@@ -79,13 +79,13 @@ func (obj *Lang) Init() error {
loadedSignal := func() { close(obj.loadedChan) } // only run once! loadedSignal := func() { close(obj.loadedChan) } // only run once!
// run the lexer/parser and build an AST // run the lexer/parser and build an AST
log.Printf("%s: Lexing/Parsing...", Name) obj.Logf("lexing/parsing...")
ast, err := LexParse(obj.Input) ast, err := LexParse(obj.Input)
if err != nil { if err != nil {
return errwrap.Wrapf(err, "could not generate AST") return errwrap.Wrapf(err, "could not generate AST")
} }
if obj.Debug { if obj.Debug {
log.Printf("%s: behold, the AST: %+v", Name, ast) obj.Logf("behold, the AST: %+v", ast)
} }
// TODO: should we validate the structure of the AST? // TODO: should we validate the structure of the AST?
@@ -94,7 +94,7 @@ func (obj *Lang) Init() error {
// return errwrap.Wrapf(err, "could not validate AST") // return errwrap.Wrapf(err, "could not validate AST")
//} //}
log.Printf("%s: Interpolating...", Name) obj.Logf("interpolating...")
// interpolate strings and other expansionable nodes in AST // interpolate strings and other expansionable nodes in AST
interpolated, err := ast.Interpolate() interpolated, err := ast.Interpolate()
if err != nil { if err != nil {
@@ -111,7 +111,7 @@ func (obj *Lang) Init() error {
}, },
} }
log.Printf("%s: Building Scope...", Name) obj.Logf("building scope...")
// propagate the scope down through the AST... // propagate the scope down through the AST...
if err := obj.ast.SetScope(scope); err != nil { if err := obj.ast.SetScope(scope); err != nil {
return errwrap.Wrapf(err, "could not set scope") return errwrap.Wrapf(err, "could not set scope")
@@ -120,15 +120,15 @@ func (obj *Lang) Init() error {
// apply type unification // apply type unification
logf := func(format string, v ...interface{}) { logf := func(format string, v ...interface{}) {
if obj.Debug { // unification only has debug messages... if obj.Debug { // unification only has debug messages...
log.Printf(Name+": unification: "+format, v...) obj.Logf("unification: "+format, v...)
} }
} }
log.Printf("%s: Running Type Unification...", Name) obj.Logf("running type unification...")
if err := unification.Unify(obj.ast, unification.SimpleInvariantSolverLogger(logf)); err != nil { if err := unification.Unify(obj.ast, unification.SimpleInvariantSolverLogger(logf)); err != nil {
return errwrap.Wrapf(err, "could not unify types") return errwrap.Wrapf(err, "could not unify types")
} }
log.Printf("%s: Building Function Graph...", Name) obj.Logf("building function graph...")
// we assume that for some given code, the list of funcs doesn't change // we assume that for some given code, the list of funcs doesn't change
// iow, we don't support variable, variables or absurd things like that // iow, we don't support variable, variables or absurd things like that
graph, err := obj.ast.Graph() // build the graph of functions graph, err := obj.ast.Graph() // build the graph of functions
@@ -137,24 +137,24 @@ func (obj *Lang) Init() error {
} }
if obj.Debug { if obj.Debug {
log.Printf("%s: function graph: %+v", Name, graph) obj.Logf("function graph: %+v", graph)
graph.Logf("%s: ", Name) // log graph with this printf prefix... graph.Logf(obj.Logf) // log graph output with this logger...
} }
if graph.NumVertices() == 0 { // no funcs to load! if graph.NumVertices() == 0 { // no funcs to load!
// send only one signal since we won't ever send after this! // send only one signal since we won't ever send after this!
log.Printf("%s: Static graph found", Name) obj.Logf("static graph found")
obj.wg.Add(1) obj.wg.Add(1)
go func() { go func() {
defer obj.wg.Done() defer obj.wg.Done()
defer close(obj.streamChan) // no more events are coming! defer close(obj.streamChan) // no more events are coming!
close(obj.loadedChan) // signal
select { select {
case obj.streamChan <- nil: // send one signal case obj.streamChan <- nil: // send one signal
// pass // pass
case <-obj.closeChan: case <-obj.closeChan:
return return
} }
close(obj.loadedChan) // signal
}() }()
return nil // exit early, no funcs to load! return nil // exit early, no funcs to load!
} }
@@ -165,22 +165,22 @@ func (obj *Lang) Init() error {
World: obj.World, World: obj.World,
Debug: obj.Debug, Debug: obj.Debug,
Logf: func(format string, v ...interface{}) { Logf: func(format string, v ...interface{}) {
log.Printf(Name+": funcs: "+format, v...) obj.Logf("funcs: "+format, v...)
}, },
Glitch: false, // FIXME: verify this functionality is perfect! Glitch: false, // FIXME: verify this functionality is perfect!
} }
log.Printf("%s: Function Engine Initializing...", Name) obj.Logf("function engine initializing...")
if err := obj.funcs.Init(); err != nil { if err := obj.funcs.Init(); err != nil {
return errwrap.Wrapf(err, "init error with func engine") return errwrap.Wrapf(err, "init error with func engine")
} }
log.Printf("%s: Function Engine Validating...", Name) obj.Logf("function engine validating...")
if err := obj.funcs.Validate(); err != nil { if err := obj.funcs.Validate(); err != nil {
return errwrap.Wrapf(err, "validate error with func engine") return errwrap.Wrapf(err, "validate error with func engine")
} }
log.Printf("%s: Function Engine Starting...", Name) obj.Logf("function engine starting...")
// On failure, we expect the caller to run Close() to shutdown all of // On failure, we expect the caller to run Close() to shutdown all of
// the currently initialized (and running) funcs... This is needed if // the currently initialized (and running) funcs... This is needed if
// we successfully ran `Run` but isn't needed only for Init/Validate. // we successfully ran `Run` but isn't needed only for Init/Validate.
@@ -189,11 +189,11 @@ func (obj *Lang) Init() error {
} }
// wait for some activity // wait for some activity
log.Printf("%s: Stream...", Name) obj.Logf("stream...")
stream := obj.funcs.Stream() stream := obj.funcs.Stream()
obj.wg.Add(1) obj.wg.Add(1)
go func() { go func() {
log.Printf("%s: Loop...", Name) obj.Logf("loop...")
defer obj.wg.Done() defer obj.wg.Done()
defer close(obj.streamChan) // no more events are coming! defer close(obj.streamChan) // no more events are coming!
for { for {
@@ -202,7 +202,7 @@ func (obj *Lang) Init() error {
select { select {
case err, ok = <-stream: case err, ok = <-stream:
if !ok { if !ok {
log.Printf("%s: Stream closed", Name) obj.Logf("stream closed")
return return
} }
if err == nil { if err == nil {
@@ -217,7 +217,7 @@ func (obj *Lang) Init() error {
select { select {
case obj.streamChan <- err: // send case obj.streamChan <- err: // send
if err != nil { if err != nil {
log.Printf("%s: Stream error: %+v", Name, err) obj.Logf("Stream error: %+v", err)
return return
} }
@@ -246,7 +246,7 @@ func (obj *Lang) Interpret() (*pgraph.Graph, error) {
return nil, fmt.Errorf("funcs aren't loaded yet") return nil, fmt.Errorf("funcs aren't loaded yet")
} }
log.Printf("%s: Running interpret...", Name) obj.Logf("running interpret...")
if obj.funcs != nil { // no need to rlock if we have a static graph if obj.funcs != nil { // no need to rlock if we have a static graph
obj.funcs.RLock() obj.funcs.RLock()
} }

View File

@@ -22,9 +22,10 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/purpleidea/mgmt/engine"
"github.com/purpleidea/mgmt/engine/resources"
_ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // load facts _ "github.com/purpleidea/mgmt/lang/funcs/facts/core" // load facts
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
multierr "github.com/hashicorp/go-multierror" multierr "github.com/hashicorp/go-multierror"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -63,8 +64,8 @@ func vertexCmpFn(v1, v2 pgraph.Vertex) (bool, error) {
return false, fmt.Errorf("oops, empty vertex") return false, fmt.Errorf("oops, empty vertex")
} }
r1, r2 := v1.(resources.Res), v2.(resources.Res) r1, r2 := v1.(engine.Res), v2.(engine.Res)
if !r1.Compare(r2) { if err := r1.Cmp(r2); err != nil {
//fmt.Printf("r1: %+v\n", *(r1.(*resources.TestRes).Int64Ptr)) //fmt.Printf("r1: %+v\n", *(r1.(*resources.TestRes).Int64Ptr))
//fmt.Printf("r2: %+v\n", *(r2.(*resources.TestRes).Int64Ptr)) //fmt.Printf("r2: %+v\n", *(r2.(*resources.TestRes).Int64Ptr))
return false, nil return false, nil
@@ -80,11 +81,15 @@ func edgeCmpFn(e1, e2 pgraph.Edge) (bool, error) {
return e1.String() == e2.String(), nil return e1.String() == e2.String(), nil
} }
func runInterpret(code string) (*pgraph.Graph, error) { func runInterpret(t *testing.T, code string) (*pgraph.Graph, error) {
str := strings.NewReader(code) str := strings.NewReader(code)
logf := func(format string, v ...interface{}) {
t.Logf("test: lang: "+format, v...)
}
lang := &Lang{ lang := &Lang{
Input: str, // string as an interface that satisfies io.Reader Input: str, // string as an interface that satisfies io.Reader
Debug: true, Debug: true,
Logf: logf,
} }
if err := lang.Init(); err != nil { if err := lang.Init(); err != nil {
return nil, errwrap.Wrapf(err, "init failed") return nil, errwrap.Wrapf(err, "init failed")
@@ -119,7 +124,7 @@ func runInterpret(code string) (*pgraph.Graph, error) {
func TestInterpret0(t *testing.T) { func TestInterpret0(t *testing.T) {
code := `` code := ``
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
@@ -132,13 +137,13 @@ func TestInterpret0(t *testing.T) {
func TestInterpret1(t *testing.T) { func TestInterpret1(t *testing.T) {
code := `noop "n1" {}` code := `noop "n1" {}`
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
} }
n1, _ := resources.NewNamedResource("noop", "n1") n1, _ := engine.NewNamedResource("noop", "n1")
expected := &pgraph.Graph{} expected := &pgraph.Graph{}
expected.AddVertex(n1) expected.AddVertex(n1)
@@ -151,14 +156,14 @@ func TestInterpret2(t *testing.T) {
noop "n1" {} noop "n1" {}
noop "n2" {} noop "n2" {}
` `
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
} }
n1, _ := resources.NewNamedResource("noop", "n1") n1, _ := engine.NewNamedResource("noop", "n1")
n2, _ := resources.NewNamedResource("noop", "n2") n2, _ := engine.NewNamedResource("noop", "n2")
expected := &pgraph.Graph{} expected := &pgraph.Graph{}
expected.AddVertex(n1) expected.AddVertex(n1)
@@ -174,7 +179,7 @@ func TestInterpret3(t *testing.T) {
int8 => 88888888, int8 => 88888888,
} }
` `
_, err := runInterpret(code) _, err := runInterpret(t, code)
if err == nil { if err == nil {
t.Errorf("expected overflow failure, but it passed") t.Errorf("expected overflow failure, but it passed")
} }
@@ -194,13 +199,13 @@ func TestInterpret4(t *testing.T) {
comment => "☺\thello\u263a\nwo\"rld\\2\u263a", # must escape these comment => "☺\thello\u263a\nwo\"rld\\2\u263a", # must escape these
} }
` `
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
} }
t1, _ := resources.NewNamedResource("test", "t1") t1, _ := engine.NewNamedResource("test", "t1")
x := t1.(*resources.TestRes) x := t1.(*resources.TestRes)
str := " !\"#$%&'()*+,-./0123456790:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~" str := " !\"#$%&'()*+,-./0123456790:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_abcdefghijklmnopqrstuvwxyz{|}~"
x.StringPtr = &str x.StringPtr = &str
@@ -228,13 +233,13 @@ func TestInterpret5(t *testing.T) {
} }
} }
` `
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
} }
t1, _ := resources.NewNamedResource("test", "t1") t1, _ := engine.NewNamedResource("test", "t1")
x := t1.(*resources.TestRes) x := t1.(*resources.TestRes)
x.Int64 = 42 x.Int64 = 42
str := "hello!" str := "hello!"
@@ -262,7 +267,7 @@ func TestInterpret6(t *testing.T) {
} }
} }
` `
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if err != nil { if err != nil {
t.Errorf("runInterpret failed: %+v", err) t.Errorf("runInterpret failed: %+v", err)
return return
@@ -271,7 +276,7 @@ func TestInterpret6(t *testing.T) {
expected := &pgraph.Graph{} expected := &pgraph.Graph{}
{ {
r, _ := resources.NewNamedResource("test", "t1") r, _ := engine.NewNamedResource("test", "t1")
x := r.(*resources.TestRes) x := r.(*resources.TestRes)
x.Int64 = 42 x.Int64 = 42
str := "hello" str := "hello"
@@ -279,7 +284,7 @@ func TestInterpret6(t *testing.T) {
expected.AddVertex(x) expected.AddVertex(x)
} }
{ {
r, _ := resources.NewNamedResource("test", "t2") r, _ := engine.NewNamedResource("test", "t2")
x := r.(*resources.TestRes) x := r.(*resources.TestRes)
x.Int64 = 13 x.Int64 = 13
str := "world" str := "world"
@@ -319,7 +324,7 @@ func TestInterpretMany(t *testing.T) {
} }
{ {
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
r, _ := resources.NewNamedResource("test", "t") r, _ := engine.NewNamedResource("test", "t")
x := r.(*resources.TestRes) x := r.(*resources.TestRes)
i := int64(42 + 13) i := int64(42 + 13)
x.Int64Ptr = &i x.Int64Ptr = &i
@@ -337,7 +342,7 @@ func TestInterpretMany(t *testing.T) {
} }
{ {
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
r, _ := resources.NewNamedResource("test", "t") r, _ := engine.NewNamedResource("test", "t")
x := r.(*resources.TestRes) x := r.(*resources.TestRes)
i := int64(42 + 13 + 99) i := int64(42 + 13 + 99)
x.Int64Ptr = &i x.Int64Ptr = &i
@@ -355,7 +360,7 @@ func TestInterpretMany(t *testing.T) {
} }
{ {
graph, _ := pgraph.NewGraph("g") graph, _ := pgraph.NewGraph("g")
r, _ := resources.NewNamedResource("test", "t") r, _ := engine.NewNamedResource("test", "t")
x := r.(*resources.TestRes) x := r.(*resources.TestRes)
i := int64(42 + 13 - 99) i := int64(42 + 13 - 99)
x.Int64Ptr = &i x.Int64Ptr = &i
@@ -386,7 +391,7 @@ func TestInterpretMany(t *testing.T) {
t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name) t.Logf("\n\ntest #%d (%s) ----------------\n\n", index, name)
graph, err := runInterpret(code) graph, err := runInterpret(t, code)
if !fail && err != nil { if !fail && err != nil {
t.Errorf("test #%d: FAIL", index) t.Errorf("test #%d: FAIL", index)
t.Errorf("test #%d: runInterpret failed with: %+v", index, err) t.Errorf("test #%d: runInterpret failed with: %+v", index, err)

View File

@@ -23,13 +23,14 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/purpleidea/mgmt/engine"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/lang/funcs" "github.com/purpleidea/mgmt/lang/funcs"
"github.com/purpleidea/mgmt/lang/funcs/structs" "github.com/purpleidea/mgmt/lang/funcs/structs"
"github.com/purpleidea/mgmt/lang/interfaces" "github.com/purpleidea/mgmt/lang/interfaces"
"github.com/purpleidea/mgmt/lang/types" "github.com/purpleidea/mgmt/lang/types"
"github.com/purpleidea/mgmt/lang/unification" "github.com/purpleidea/mgmt/lang/unification"
"github.com/purpleidea/mgmt/pgraph" "github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/resources"
"github.com/purpleidea/mgmt/util" "github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors" errwrap "github.com/pkg/errors"
@@ -240,7 +241,7 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
// TODO: test for []str instead, and loop // TODO: test for []str instead, and loop
name := nameValue.Str() // must not panic name := nameValue.Str() // must not panic
res, err := resources.NewNamedResource(obj.Kind, name) res, err := engine.NewNamedResource(obj.Kind, name)
if err != nil { if err != nil {
return nil, errwrap.Wrapf(err, "cannot create resource kind `%s` with named `%s`", obj.Kind, name) return nil, errwrap.Wrapf(err, "cannot create resource kind `%s` with named `%s`", obj.Kind, name)
} }
@@ -250,7 +251,7 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
panic(fmt.Sprintf("expected struct, got: %s", k)) panic(fmt.Sprintf("expected struct, got: %s", k))
} }
mapping, err := resources.LangFieldNameToStructFieldName(obj.Kind) mapping, err := engineUtil.LangFieldNameToStructFieldName(obj.Kind)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -377,7 +378,7 @@ func (obj *StmtRes) Output() (*interfaces.Output, error) {
} }
return &interfaces.Output{ return &interfaces.Output{
Resources: []resources.Res{res}, Resources: []engine.Res{res},
Edges: edges, Edges: edges,
}, nil }, nil
} }
@@ -580,7 +581,7 @@ func (obj *StmtResField) Unify(kind string) ([]interfaces.Invariant, error) {
// TODO: unfortunately this gets called separately for each field... if // TODO: unfortunately this gets called separately for each field... if
// we could cache this, it might be worth looking into for performance! // we could cache this, it might be worth looking into for performance!
typMap, err := resources.LangFieldNameToStructType(kind) typMap, err := engineUtil.LangFieldNameToStructType(kind)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -1142,7 +1143,7 @@ func (obj *StmtIf) Output() (*interfaces.Output, error) {
return nil, err return nil, err
} }
resources := []resources.Res{} resources := []engine.Res{}
if output != nil { if output != nil {
resources = append(resources, output.Resources...) resources = append(resources, output.Resources...)
//edges = output.Edges //edges = output.Edges
@@ -1267,7 +1268,7 @@ func (obj *StmtProg) Graph() (*pgraph.Graph, error) {
// analogous function for expressions is Value. Those Value functions might get // analogous function for expressions is Value. Those Value functions might get
// called by this Output function if they are needed to produce the output. // called by this Output function if they are needed to produce the output.
func (obj *StmtProg) Output() (*interfaces.Output, error) { func (obj *StmtProg) Output() (*interfaces.Output, error) {
resources := []resources.Res{} resources := []engine.Res{}
edges := []*interfaces.Edge{} edges := []*interfaces.Edge{}
for _, stmt := range obj.Prog { for _, stmt := range obj.Prog {

View File

@@ -23,6 +23,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"sort" "sort"
"sync"
"syscall" "syscall"
"github.com/purpleidea/mgmt/bindata" "github.com/purpleidea/mgmt/bindata"
@@ -126,44 +127,72 @@ func run(c *cli.Context) error {
obj.PgpIdentity = &us obj.PgpIdentity = &us
} }
obj.Prometheus = c.Bool("prometheus")
obj.PrometheusListen = c.String("prometheus-listen")
if err := obj.Validate(); err != nil {
return err
}
if err := obj.Init(); err != nil { if err := obj.Init(); err != nil {
return err return err
} }
obj.Prometheus = c.Bool("prometheus")
obj.PrometheusListen = c.String("prometheus-listen")
// install the exit signal handler // install the exit signal handler
wg := &sync.WaitGroup{}
defer wg.Wait()
exit := make(chan struct{}) exit := make(chan struct{})
defer close(exit) defer close(exit)
wg.Add(1)
go func() { go func() {
signals := make(chan os.Signal, 1) defer wg.Done()
// must have buffer for max number of signals
signals := make(chan os.Signal, 3+1) // 3 * ^C + 1 * SIGTERM
signal.Notify(signals, os.Interrupt) // catch ^C signal.Notify(signals, os.Interrupt) // catch ^C
//signal.Notify(signals, os.Kill) // catch signals //signal.Notify(signals, os.Kill) // catch signals
signal.Notify(signals, syscall.SIGTERM) signal.Notify(signals, syscall.SIGTERM)
var count uint8
for {
select { select {
case sig := <-signals: // any signal will do case sig := <-signals: // any signal will do
if sig == os.Interrupt { if sig != os.Interrupt {
log.Println("Interrupted by ^C") log.Printf("Interrupted by signal")
obj.Exit(nil) obj.Interrupt(fmt.Errorf("killed by %v", sig))
return return
} }
log.Println("Interrupted by signal")
obj.Exit(fmt.Errorf("killed by %v", sig)) switch count {
return case 0:
log.Printf("Interrupted by ^C")
obj.Exit(nil)
case 1:
log.Printf("Interrupted by ^C (fast pause)")
obj.FastExit(nil)
case 2:
log.Printf("Interrupted by ^C (hard interrupt)")
obj.Interrupt(nil)
}
count++
case <-exit: case <-exit:
return return
} }
}
}() }()
if err := obj.Run(); err != nil { reterr := obj.Run()
if reterr != nil {
// log the error message returned // log the error message returned
log.Printf("Main: Error: %v", err) log.Printf("Main: Error: %v", reterr)
}
if err := obj.Close(); err != nil {
log.Printf("Main: Close: %v", err)
//return cli.NewExitError(err.Error(), 1) // TODO: ? //return cli.NewExitError(err.Error(), 1) // TODO: ?
return cli.NewExitError("", 1) return cli.NewExitError("", 1)
} }
return nil
return reterr
} }
// CLI is the entry point for using mgmt normally from the CLI. // CLI is the entry point for using mgmt normally from the CLI.

View File

@@ -25,7 +25,6 @@ import (
"github.com/purpleidea/mgmt/etcd" "github.com/purpleidea/mgmt/etcd"
etcdfs "github.com/purpleidea/mgmt/etcd/fs" etcdfs "github.com/purpleidea/mgmt/etcd/fs"
"github.com/purpleidea/mgmt/gapi" "github.com/purpleidea/mgmt/gapi"
// these imports are so that GAPIs register themselves in init() // these imports are so that GAPIs register themselves in init()
_ "github.com/purpleidea/mgmt/lang" _ "github.com/purpleidea/mgmt/lang"
_ "github.com/purpleidea/mgmt/puppet" _ "github.com/purpleidea/mgmt/puppet"

View File

@@ -44,5 +44,5 @@ func hello(program, version string, flags Flags) {
} }
log.Printf("This is: %s, version: %s", program, version) log.Printf("This is: %s, version: %s", program, version)
log.Printf("Main: Start: %v", start) log.Printf("main: Start: %v", start)
} }

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