Files
mgmt/resources/resources.go
James Shubin 4d9d0d4548 resources: Improve AutoEdge API and pkg breakage
I previously broke the pkg auto edges because the package list wasn't
available by the time it was called. This fixes the pkg resource so that
it gets the necessary list of packages when needed. Since this means
that a possible failure could happen, we also update the AutoEdges API
to support errors. Errors can only be generated at AutoEdge struct
creation, once the struct has been returned (right before modification
of the graph structure) there is no possibility to return any errors.

It's important to remember that the AutoEdges stuff gets called because
the Init of each resource, so make sure it doesn't depend on anything
that happens there or that gets cached as a result of Init.

This is all much nicer now and has a test too :)
2017-06-02 22:15:28 -04:00

642 lines
20 KiB
Go

// Mgmt
// Copyright (C) 2013-2017+ James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// Package resources provides the resource framework and idempotent primitives.
package resources
import (
"fmt"
"log"
"math"
"os"
"path"
"sort"
"sync"
"time"
// TODO: should each resource be a sub-package?
"github.com/purpleidea/mgmt/converger"
"github.com/purpleidea/mgmt/event"
"github.com/purpleidea/mgmt/pgraph"
"github.com/purpleidea/mgmt/prometheus"
"github.com/purpleidea/mgmt/util"
errwrap "github.com/pkg/errors"
"golang.org/x/time/rate"
)
var registeredResources = map[string]func() Res{}
// RegisterResource registers a new resource by providing a constructor
// function that returns a resource object ready to be unmarshalled from YAML.
func RegisterResource(name string, creator func() Res) {
registeredResources[name] = creator
}
// NewEmptyNamedResource returns an empty resource object from a registered
// type, ready to be unmarshalled.
func NewEmptyNamedResource(name string) (Res, error) {
fn, ok := registeredResources[name]
if !ok {
return nil, fmt.Errorf("no resource named %s available", name)
}
return fn(), nil
}
//go:generate stringer -type=ResState -output=resstate_stringer.go
// The ResState type represents the current activity state of each resource.
type ResState int
// Each ResState should be set properly in the relevant part of the resource.
const (
ResStateNil ResState = iota
ResStateProcess // we're in process, but we haven't done much yet
ResStateCheckApply // we're about to run CheckApply
ResStatePoking // we're done CheckApply, and we're about to poke
)
const refreshPathToken = "refresh"
// 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
StrMapWatch(namespace string) chan error
StrMapGet(namespace string) (map[string]string, error)
StrMapSet(namespace, value string) error
StrMapDel(namespace string) error
}
// ResData is the set of input values passed into the pgraph for the resources.
type ResData struct {
Hostname string // uuid for the host
//Noop bool
Converger converger.Converger
Prometheus *prometheus.Prometheus
World World
Prefix string // the prefix to be used for the pgraph namespace
Debug bool
// NOTE: we can add more fields here if needed for the resources.
}
// The Base interface is everything that is common to all resources.
// Everything here only needs to be implemented once, in the BaseRes.
type Base interface {
GetName() string // can't be named "Name()" because of struct field
SetName(string)
SetKind(string)
GetKind() string
String() string
Meta() *MetaParams
Events() chan *event.Event
Data() *ResData
Working() *bool
Setup(*MGraph, pgraph.Vertex, Res)
Reset()
Exit()
GetState() ResState
SetState(ResState)
Timestamp() int64
UpdateTimestamp() int64
Event() error
SendEvent(event.Kind, error) error
ReadEvent(*event.Event) (*error, bool)
Refresh() bool // is there a pending refresh to run?
SetRefresh(bool) // set the refresh state of this resource
SendRecv(Res) (map[string]bool, error) // send->recv data passing function
IsStateOK() bool
StateOK(b bool)
GroupCmp(Res) bool // TODO: is there a better name for this?
GroupRes(Res) error // group resource (arg) into self
IsGrouped() bool // am I grouped?
SetGrouped(bool) // set grouped bool
GetGroup() []Res // return everyone grouped inside me
SetGroup([]Res)
VarDir(string) (string, error)
Running() error // notify the engine that Watch started
Started() <-chan struct{} // returns when the resource has started
Stopped() <-chan struct{} // returns when the resource has stopped
Starter(bool)
Poll() error // poll alternative to watching :(
Worker() error
}
// Res is the minimum interface you need to implement to define a new resource.
type Res interface {
Base // include everything from the Base interface
Default() Res // return a struct with sane defaults as a Res
Validate() error
Init() error
Close() error
UIDs() []ResUID // most resources only return one
Watch() error // send on channel to signal process() events
CheckApply(apply bool) (checkOK bool, err error)
AutoEdges() (AutoEdge, error)
Compare(Res) bool
CollectPattern(string) // XXX: temporary until Res collection is more advanced
//UnmarshalYAML(unmarshal func(interface{}) error) error // optional
}
// BaseRes is the base struct that gets used in every resource.
type BaseRes struct {
Res Res // pointer to full res
Graph *MGraph // pointer to graph I'm currently in
Vertex pgraph.Vertex // pointer to vertex I currently am
Recv map[string]*Send // mapping of key to receive on from value
Kind string
Name string `yaml:"name"`
MetaParams MetaParams `yaml:"meta"` // struct of all the metaparams
timestamp int64 // last updated timestamp
state ResState
isStateOK bool // whether the state is okay based on events or not
prefix string // base prefix for this resource
data ResData
eventsLock *sync.Mutex // locks around sending and closing of events channel
eventsDone bool
eventsChan chan *event.Event
processLock *sync.Mutex
processDone bool
processChan chan *event.Event // chan that resources send events to
processSync *sync.WaitGroup // blocks until the innerWorker closes
cuid converger.UID
wcuid converger.UID
pcuid converger.UID
started chan struct{} // closed when worker is started/running
stopped chan struct{} // closed when worker is stopped/exited
isStarted bool // did the started chan already close?
starter bool // does this have indegree == 0 ? XXX: usually?
quiescing bool // are we quiescing (pause or exit), tell event replay
quiesceGroup *sync.WaitGroup
waitGroup *sync.WaitGroup
working bool // is the Worker() loop running ?
isGrouped bool // am i contained within a group?
grouped []Res // list of any grouped resources
refresh bool // does this resource have a refresh to run?
//refreshState StatefulBool // TODO: future stateful bool
debug bool
}
// Validate reports any problems with the struct definition.
func (obj *BaseRes) Validate() error {
isInf := obj.Meta().Limit == rate.Inf || math.IsInf(float64(obj.Meta().Limit), 1)
if obj.Meta().Burst == 0 && !isInf { // blocked
return fmt.Errorf("Permanently limited (rate != Inf, burst: 0)")
}
return nil
}
// Init initializes structures like channels if created without New constructor.
func (obj *BaseRes) Init() error {
if obj.debug {
log.Printf("%s: Init()", obj)
}
if obj.Kind == "" {
return fmt.Errorf("resource did not set kind")
}
if converger := obj.Data().Converger; converger != nil {
obj.cuid = converger.Register()
obj.wcuid = converger.Register() // get a cuid for the worker!
obj.pcuid = converger.Register() // get a cuid for the process
}
obj.processLock = &sync.Mutex{} // lock around processChan closing and sending
obj.processDone = false // did we close processChan ?
obj.processChan = make(chan *event.Event)
obj.processSync = &sync.WaitGroup{}
obj.quiescing = false // no quiesce operation is happening at the moment
obj.quiesceGroup = &sync.WaitGroup{}
// more useful than a closed channel signal, since it can be re-used
// safely without having to recreate it and worry about stale handles
obj.waitGroup = &sync.WaitGroup{} // Init and Close must be 1-1 matched!
obj.waitGroup.Add(1)
//obj.working = true // Worker method should now be running...
// FIXME: force a sane default until UnmarshalYAML on *BaseRes works...
if obj.Meta().Burst == 0 && obj.Meta().Limit == 0 { // blocked
obj.Meta().Limit = rate.Inf
}
if math.IsInf(float64(obj.Meta().Limit), 1) { // yaml `.inf` -> rate.Inf
obj.Meta().Limit = rate.Inf
}
//dir, err := obj.VarDir("")
//if err != nil {
// return errwrap.Wrapf(err, "the VarDir failed in Init()")
//}
// TODO: this StatefulBool implementation could be eventually swappable
//obj.refreshState = &DiskBool{Path: path.Join(dir, refreshPathToken)}
if err := obj.Data().Prometheus.AddManagedResource(obj.String(), obj.GetKind()); err != nil {
return errwrap.Wrapf(err, "could not increase prometheus counter!")
}
return nil
}
// Close shuts down and performs any cleanup.
func (obj *BaseRes) Close() error {
if obj.debug {
log.Printf("%s: Close()", obj)
}
if converger := obj.Data().Converger; converger != nil {
obj.pcuid.Unregister()
obj.wcuid.Unregister()
obj.cuid.Unregister()
}
//obj.working = false // Worker method should now be closing...
close(obj.stopped)
obj.waitGroup.Done()
if err := obj.Data().Prometheus.RemoveManagedResource(obj.String(), obj.GetKind()); err != nil {
return errwrap.Wrapf(err, "could not decrease prometheus counter!")
}
return nil
}
// GetName is used by all the resources to Get the name.
func (obj *BaseRes) GetName() string {
return obj.Name
}
// SetName is used to set the name of the resource.
func (obj *BaseRes) SetName(name string) {
obj.Name = name
}
// SetKind sets the kind. This is used internally for exported resources.
func (obj *BaseRes) SetKind(kind string) {
obj.Kind = kind
}
// GetKind returns the kind of resource this is.
func (obj *BaseRes) GetKind() string {
return obj.Kind
}
// String returns the canonical string representation for a resource.
func (obj *BaseRes) String() string {
return fmt.Sprintf("%s[%s]", obj.GetKind(), obj.GetName())
}
// Meta returns the MetaParams as a reference, which we can then get/set on.
func (obj *BaseRes) Meta() *MetaParams {
return &obj.MetaParams
}
// Events returns the channel of events to listen on.
func (obj *BaseRes) Events() chan *event.Event {
return obj.eventsChan
}
// Data returns an associable handle to some data passed in to the resource.
func (obj *BaseRes) Data() *ResData {
return &obj.data
}
// Working returns a pointer to the bool which should track Worker run state.
func (obj *BaseRes) Working() *bool {
return &obj.working
}
// Setup does some work which must happen before the Worker starts. It happens
// once per Worker startup. It can happen in parallel with other Setup calls, so
// add locks around any operation that's not thread-safe.
func (obj *BaseRes) Setup(mgraph *MGraph, vertex pgraph.Vertex, res Res) {
obj.started = make(chan struct{}) // closes when started
obj.stopped = make(chan struct{}) // closes when stopped
obj.eventsLock = &sync.Mutex{}
obj.eventsDone = false
obj.eventsChan = make(chan *event.Event) // unbuffered chan to avoid stale events
obj.Res = res // store a pointer to the full object
obj.Vertex = vertex // store a pointer to the vertex i'm
obj.Graph = mgraph // store a pointer to the graph we're in
}
// Reset from Setup. These can get called for different vertices in parallel.
func (obj *BaseRes) Reset() {
obj.Res = nil
obj.Vertex = nil
obj.Graph = nil
return
}
// Exit the resource. Wrapper function to keep the logic in one place for now.
func (obj *BaseRes) Exit() {
// XXX: consider instead doing this by closing the Res.events channel instead?
// XXX: do this by sending an exit signal, and then returning
// when we hit the 'default' in the select statement!
// XXX: we can do this to quiesce, but it's not necessary now
obj.SendEvent(event.EventExit, nil) // sync
obj.waitGroup.Wait()
}
// GetState returns the state of the resource.
func (obj *BaseRes) GetState() ResState {
return obj.state
}
// SetState sets the state of the resource.
func (obj *BaseRes) SetState(state ResState) {
if obj.debug {
log.Printf("%s: State: %v -> %v", obj, obj.GetState(), state)
}
obj.state = state
}
// Timestamp returns the timestamp of a resource.
func (obj *BaseRes) Timestamp() int64 {
return obj.timestamp
}
// UpdateTimestamp updates the timestamp and returns the new value.
func (obj *BaseRes) UpdateTimestamp() int64 {
obj.timestamp = time.Now().UnixNano() // update
return obj.timestamp
}
// IsStateOK returns the cached state value.
func (obj *BaseRes) IsStateOK() bool {
return obj.isStateOK
}
// StateOK sets the cached state value.
func (obj *BaseRes) StateOK(b bool) {
obj.isStateOK = b
}
// ProcessExit causes the innerWorker to close and waits until it does so.
func (obj *BaseRes) ProcessExit() {
obj.processLock.Lock() // lock to avoid a send when closed!
obj.processDone = true
close(obj.processChan)
obj.processLock.Unlock()
obj.processSync.Wait()
}
// GroupCmp compares two resources and decides if they're suitable for grouping
// You'll probably want to override this method when implementing a resource...
func (obj *BaseRes) GroupCmp(res Res) bool {
return false // base implementation assumes false, override me!
}
// GroupRes groups resource (arg) into self.
func (obj *BaseRes) GroupRes(res Res) error {
if l := len(res.GetGroup()); l > 0 {
return fmt.Errorf("the %v resource already contains %d grouped resources", res, l)
}
if res.IsGrouped() {
return fmt.Errorf("the %v resource is already grouped", res)
}
// merging two resources into one should yield the sum of their semas
if semas := res.Meta().Sema; len(semas) > 0 {
obj.Meta().Sema = append(obj.Meta().Sema, semas...)
obj.Meta().Sema = util.StrRemoveDuplicatesInList(obj.Meta().Sema)
}
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 *BaseRes) IsGrouped() bool { // am I grouped?
return obj.isGrouped
}
// SetGrouped sets a flag to tell if we are grouped.
func (obj *BaseRes) SetGrouped(b bool) {
obj.isGrouped = b
}
// GetGroup returns everyone grouped inside me.
func (obj *BaseRes) GetGroup() []Res { // return everyone grouped inside me
return obj.grouped
}
// SetGroup sets the grouped resources into me.
func (obj *BaseRes) SetGroup(g []Res) {
obj.grouped = g
}
// AutoEdges returns the AutoEdge interface. By default, none are created. This
// should be implemented by the specific resource to be used. This base method
// does not need to be called by the resource specific implementing method.
func (obj *BaseRes) AutoEdges() (AutoEdge, error) {
return nil, nil
}
// Compare is the base compare method, which also handles the metaparams cmp.
func (obj *BaseRes) Compare(res Res) bool {
// TODO: should the AutoEdge values be compared?
if obj.Meta().AutoEdge != res.Meta().AutoEdge {
return false
}
if obj.Meta().AutoGroup != res.Meta().AutoGroup {
return false
}
if obj.Meta().Noop != res.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.Meta().Noop { // asymmetrical
return false // going from noop to no-noop!
}
}
if obj.Meta().Retry != res.Meta().Retry {
return false
}
if obj.Meta().Delay != res.Meta().Delay {
return false
}
if obj.Meta().Poll != res.Meta().Poll {
return false
}
if obj.Meta().Limit != res.Meta().Limit {
return false
}
if obj.Meta().Burst != res.Meta().Burst {
return false
}
// are the two slices the same?
cmpSlices := func(a, b []string) bool {
if len(a) != len(b) {
return false
}
sort.Strings(a)
sort.Strings(b)
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
if !cmpSlices(obj.Meta().Sema, res.Meta().Sema) {
return false
}
return true
}
// CollectPattern is used for resource collection.
func (obj *BaseRes) CollectPattern(pattern string) {
// XXX: default method is empty
}
// 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.
func (obj *BaseRes) VarDir(extra string) (string, error) {
// Using extra adds additional dirs onto our namespace. An empty extra
// adds no additional directories.
if obj.prefix == "" {
return "", fmt.Errorf("the VarDir prefix is empty")
}
if obj.GetKind() == "" {
return "", fmt.Errorf("the VarDir kind is empty")
}
if obj.GetName() == "" {
return "", fmt.Errorf("the VarDir name is empty")
}
// FIXME: is obj.GetName() sufficiently unique to use as a UID here?
uid := obj.GetName()
p := fmt.Sprintf("%s/", path.Join(obj.prefix, obj.GetKind(), uid, extra))
if err := os.MkdirAll(p, 0770); err != nil {
return "", errwrap.Wrapf(err, "can't create prefix for %s", obj)
}
return p, nil
}
// Started returns a channel that closes when the resource has started up.
func (obj *BaseRes) Started() <-chan struct{} { return obj.started }
// Stopped returns a channel that closes when the worker has finished running.
func (obj *BaseRes) Stopped() <-chan struct{} { return obj.stopped }
// Starter sets the starter bool. This defines if a vertex has an indegree of 0.
// If we have an indegree of 0, we'll need to be a poke initiator in the graph.
func (obj *BaseRes) Starter(b bool) { obj.starter = b }
// Poll is the watch replacement for when we want to poll, which outputs events.
func (obj *BaseRes) Poll() error {
// create a time.Ticker for the given interval
ticker := time.NewTicker(time.Duration(obj.Meta().Poll) * time.Second)
defer ticker.Stop()
// notify engine that we're running
if err := obj.Running(); err != nil {
return err // bubble up a NACK...
}
obj.cuid.SetConverged(false) // quickly stop any converge due to Running()
var send = false
var exit *error
for {
select {
case <-ticker.C: // received the timer event
log.Printf("%s: polling...", obj)
send = true
obj.StateOK(false) // dirty
case event := <-obj.Events():
obj.cuid.ResetTimer() // important
if exit, send = obj.ReadEvent(event); exit != nil {
return *exit // exit
}
}
if send {
send = false
obj.Event()
}
}
}
// UnmarshalYAML is the custom unmarshal handler for the BaseRes struct. It is
// primarily useful for setting the defaults, in particular if meta is absent!
// FIXME: how come we can't get this to work properly without dropping fields?
//func (obj *BaseRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
// DefaultBaseRes := BaseRes{
// // without specifying a default here, if we don't specify *any*
// // meta parameters in the yaml file, then the UnmarshalYAML for
// // the MetaParams struct won't run, and we won't get defaults!
// MetaParams: DefaultMetaParams, // force a default
// }
// type rawBaseRes BaseRes // indirection to avoid infinite recursion
// raw := rawBaseRes(DefaultBaseRes) // convert; the defaults go here
// //raw := rawBaseRes{}
// if err := unmarshal(&raw); err != nil {
// return err
// }
// *obj = BaseRes(raw) // restore from indirection with type conversion!
// return nil
//}
// VtoR casts the Vertex into a Res for use. It panics if it can't convert.
func VtoR(v pgraph.Vertex) Res {
res, ok := v.(Res)
if !ok {
panic("not a Res")
}
return res
}
// TODO: consider adding a mutate API.
//func (g *Graph) MutateMatch(obj resources.Res) Vertex {
// for v := range g.adjacency {
// if err := v.Res.Mutate(obj); err == nil {
// // transmogrified!
// return v
// }
// }
// return nil
//}