From da0ffa5e5686de236c13938a461e4b30ed4259f8 Mon Sep 17 00:00:00 2001 From: Jonathan Gold Date: Fri, 14 Dec 2018 23:07:45 -0500 Subject: [PATCH] engine: resources: cron: Add auto edges from SvcRes --- engine/resources/cron.go | 78 +++++++++++++++++++++++++++++----------- engine/resources/svc.go | 75 ++++++++++++++++++++++++++++++++++---- examples/lang/cron4.mcl | 17 +++++++++ 3 files changed, 142 insertions(+), 28 deletions(-) create mode 100644 examples/lang/cron4.mcl diff --git a/engine/resources/cron.go b/engine/resources/cron.go index 6df63412..f1b37746 100644 --- a/engine/resources/cron.go +++ b/engine/resources/cron.go @@ -21,7 +21,8 @@ import ( "bytes" "context" "fmt" - "os" + "os/user" + "path" "strings" "time" @@ -141,15 +142,13 @@ func (obj *CronRes) Default() engine.Res { // validate and initialize the nested file resource and to apply the file state // in CheckApply. func (obj *CronRes) makeComposite() (*FileRes, error) { - // root timer - path := fmt.Sprintf("/etc/systemd/system/%s.timer", obj.Name()) - if obj.Session { - // user timer - path = fmt.Sprintf("%s/.config/systemd/user/%s.timer", os.Getenv("HOME"), obj.Name()) - } - res, err := engine.NewNamedResource("file", path) + p, err := obj.UnitFilePath() if err != nil { - return nil, err + return nil, errwrap.Wrapf(err, "error generating unit file path") + } + res, err := engine.NewNamedResource("file", p) + if err != nil { + return nil, errwrap.Wrapf(err, "error creating nested file resource") } file, ok := res.(*FileRes) if !ok { @@ -260,14 +259,12 @@ func (obj *CronRes) Watch() error { bus.Signal(dbusChan) defer bus.RemoveSignal(dbusChan) // not needed here, but nice for symmetry - // root timer - path := fmt.Sprintf("/etc/systemd/system/%s.timer", obj.Name()) - if obj.Session { - // user timer - path = fmt.Sprintf("%s/.config/systemd/user/%s.timer", os.Getenv("HOME"), obj.Name()) + p, err := obj.UnitFilePath() + if err != nil { + return errwrap.Wrapf(err, "error generating unit file path") } // recwatcher for the systemd-timer unit file - obj.recWatcher, err = recwatch.NewRecWatcher(path, false) + obj.recWatcher, err = recwatch.NewRecWatcher(p, false) if err != nil { return err } @@ -453,7 +450,8 @@ type CronUID struct { // used in the IFF function, is what you see in the struct fields here. engine.BaseUID - name string // the machine name + unit string // name of target unit + session bool // user session } // IFF aka if and only if they are equivalent, return true. If not, false. @@ -462,17 +460,38 @@ func (obj *CronUID) IFF(uid engine.ResUID) bool { if !ok { return false } - return obj.name == res.name + if obj.unit != res.unit { + return false + } + if obj.session != res.session { + return false + } + return true +} + +// AutoEdges returns the AutoEdge interface. +func (obj *CronRes) AutoEdges() (engine.AutoEdge, error) { + return nil, 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 *CronRes) UIDs() []engine.ResUID { - x := &CronUID{ - BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()}, - name: obj.Name(), // svc name + unit := fmt.Sprintf("%s.service", obj.Name()) + if obj.Unit != "" { + unit = obj.Unit } - return append([]engine.ResUID{x}, obj.file.UIDs()...) + uids := []engine.ResUID{ + &CronUID{ + BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()}, + unit: unit, // name of target unit + session: obj.Session, // user session + }, + } + if file, err := obj.makeComposite(); err == nil { + uids = append(uids, file.UIDs()...) // add the file uid if we can + } + return uids } // UnmarshalYAML is the custom unmarshal handler for this struct. @@ -495,6 +514,23 @@ func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } +// UnitFilePath returns the path to the systemd-timer unit file. +func (obj *CronRes) UnitFilePath() (string, error) { + // root timer + if !obj.Session { + return fmt.Sprintf("/etc/systemd/system/%s.timer", obj.Name()), nil + } + // user timer + u, err := user.Current() + if err != nil { + return "", errwrap.Wrapf(err, "error getting current user") + } + if u.HomeDir == "" { + return "", fmt.Errorf("user has no home directory") + } + return path.Join(u.HomeDir, "/.config/systemd/user/", fmt.Sprintf("%s.timer", obj.Name())), nil +} + // unitFileContents returns the contents of the unit file representing the // CronRes struct. func (obj *CronRes) unitFileContents() string { diff --git a/engine/resources/svc.go b/engine/resources/svc.go index d01006f2..5c0d888b 100644 --- a/engine/resources/svc.go +++ b/engine/resources/svc.go @@ -21,9 +21,12 @@ package resources import ( "fmt" + "os/user" + "path" "github.com/purpleidea/mgmt/engine" "github.com/purpleidea/mgmt/engine/traits" + engineUtil "github.com/purpleidea/mgmt/engine/util" "github.com/purpleidea/mgmt/util" systemd "github.com/coreos/go-systemd/dbus" // change namespace @@ -380,7 +383,8 @@ type SvcUID struct { // 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 + name string // the svc name + session bool // user session } // IFF aka if and only if they are equivalent, return true. If not, false. @@ -389,7 +393,13 @@ func (obj *SvcUID) IFF(uid engine.ResUID) bool { if !ok { return false } - return obj.name == res.name + if obj.name != res.name { + return false + } + if obj.session != res.session { + return false + } + return true } // SvcResAutoEdges holds the state of the auto edge generator. @@ -431,13 +441,56 @@ func (obj *SvcResAutoEdges) Test(input []bool) bool { return true // keep going } -// AutoEdges returns the AutoEdge interface. In this case the systemd units. +// SvcResAutoEdgesCron holds the state of the svc -> cron auto edge generator. +type SvcResAutoEdgesCron struct { + unit string // target unit + session bool // user session +} + +// Next returns the next automatic edge. +func (obj *SvcResAutoEdgesCron) Next() []engine.ResUID { + // XXX: should this be true if SvcRes State == "stopped"? + reversed := false + value := &CronUID{ + BaseUID: engine.BaseUID{ + Kind: "CronRes", + Reversed: &reversed, + }, + unit: obj.unit, // target unit + session: obj.session, // user session + } + return []engine.ResUID{value} // we return one, even though api supports N +} + +// Test takes the output of the last call to Next() and outputs true if we +// should continue. +func (obj *SvcResAutoEdgesCron) Test([]bool) bool { + return false // only get one svc -> cron edge +} + +// AutoEdges returns the AutoEdge interface. In this case, systemd unit file +// resources and cron (systemd-timer) resources. func (obj *SvcRes) AutoEdges() (engine.AutoEdge, error) { var data []engine.ResUID - svcFiles := []string{ + var svcFiles []string + svcFiles = []string{ + // root svc fmt.Sprintf("/etc/systemd/system/%s.service", obj.Name()), // takes precedence fmt.Sprintf("/usr/lib/systemd/system/%s.service", obj.Name()), // pkg default } + if obj.Session { + // user svc + u, err := user.Current() + if err != nil { + return nil, errwrap.Wrapf(err, "error getting current user") + } + if u.HomeDir == "" { + return nil, fmt.Errorf("user has no home directory") + } + svcFiles = []string{ + path.Join(u.HomeDir, "/.config/systemd/user/", fmt.Sprintf("%s.service", obj.Name())), + } + } for _, x := range svcFiles { var reversed = true data = append(data, &FileUID{ @@ -449,11 +502,18 @@ func (obj *SvcRes) AutoEdges() (engine.AutoEdge, error) { path: x, // what matters }) } - return &FileResAutoEdges{ + + fileEdge := &FileResAutoEdges{ data: data, pointer: 0, found: false, - }, nil + } + cronEdge := &SvcResAutoEdgesCron{ + session: obj.Session, + unit: fmt.Sprintf("%s.service", obj.Name()), + } + + return engineUtil.AutoEdgeCombiner(fileEdge, cronEdge) } // UIDs includes all params to make a unique identification of this object. @@ -461,7 +521,8 @@ func (obj *SvcRes) AutoEdges() (engine.AutoEdge, error) { func (obj *SvcRes) UIDs() []engine.ResUID { x := &SvcUID{ BaseUID: engine.BaseUID{Name: obj.Name(), Kind: obj.Kind()}, - name: obj.Name(), // svc name + name: obj.Name(), // svc name + session: obj.Session, // user session } return []engine.ResUID{x} } diff --git a/examples/lang/cron4.mcl b/examples/lang/cron4.mcl new file mode 100644 index 00000000..86ebf2a8 --- /dev/null +++ b/examples/lang/cron4.mcl @@ -0,0 +1,17 @@ +$home = getenv("HOME") + +cron "purpleidea-oneshot" { + state => "absent", + session => true, + trigger => "OnCalendar", + time => "*:*:0", +} + +svc "purpleidea-oneshot" { + state => "stopped", + session => true, +} + +file printf("%s/.config/systemd/user/purpleidea-oneshot.service", $home) { + state => "absent", +}