594 lines
20 KiB
Go
594 lines
20 KiB
Go
// Mgmt
|
|
// Copyright (C) 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 <https://www.gnu.org/licenses/>.
|
|
//
|
|
// Additional permission under GNU GPL version 3 section 7
|
|
//
|
|
// If you modify this program, or any covered work, by linking or combining it
|
|
// with embedded mcl code and modules (and that the embedded mcl code and
|
|
// modules which link with this program, contain a copy of their source code in
|
|
// the authoritative form) containing parts covered by the terms of any other
|
|
// license, the licensors of this program grant you additional permission to
|
|
// convey the resulting work. Furthermore, the licensors of this program grant
|
|
// the original author, James Shubin, additional permission to update this
|
|
// additional permission if he deems it necessary to achieve the goals of this
|
|
// additional permission.
|
|
|
|
package resources
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/user"
|
|
"path"
|
|
"strings"
|
|
|
|
"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/purpleidea/mgmt/util/recwatch"
|
|
|
|
sdbus "github.com/coreos/go-systemd/v22/dbus"
|
|
"github.com/coreos/go-systemd/v22/unit"
|
|
systemdUtil "github.com/coreos/go-systemd/v22/util"
|
|
"github.com/godbus/dbus/v5"
|
|
)
|
|
|
|
const (
|
|
// OnCalendar is a systemd-timer trigger, whose behaviour is defined in
|
|
// 'man systemd-timer', and whose format is defined in the 'Calendar
|
|
// Events' section of 'man systemd-time'.
|
|
OnCalendar = "OnCalendar"
|
|
// OnActiveSec is a systemd-timer trigger, whose behaviour is defined in
|
|
// 'man systemd-timer', and whose format is a time span as defined in
|
|
// 'man systemd-time'.
|
|
OnActiveSec = "OnActiveSec"
|
|
// OnBootSec is a systemd-timer trigger, whose behaviour is defined in
|
|
// 'man systemd-timer', and whose format is a time span as defined in
|
|
// 'man systemd-time'.
|
|
OnBootSec = "OnBootSec"
|
|
// OnStartupSec is a systemd-timer trigger, whose behaviour is defined in
|
|
// 'man systemd-timer', and whose format is a time span as defined in
|
|
// 'man systemd-time'.
|
|
OnStartupSec = "OnStartupSec"
|
|
// OnUnitActiveSec is a systemd-timer trigger, whose behaviour is defined
|
|
// in 'man systemd-timer', and whose format is a time span as defined in
|
|
// 'man systemd-time'.
|
|
OnUnitActiveSec = "OnUnitActiveSec"
|
|
// OnUnitInactiveSec is a systemd-timer trigger, whose behaviour is defined
|
|
// in 'man systemd-timer', and whose format is a time span as defined in
|
|
// 'man systemd-time'.
|
|
OnUnitInactiveSec = "OnUnitInactiveSec"
|
|
)
|
|
|
|
func init() {
|
|
engine.RegisterResource("cron", func() engine.Res { return &CronRes{} })
|
|
}
|
|
|
|
// CronRes is a systemd-timer cron resource.
|
|
// TODO: If we want to have an actual `crond` resource, name it LegacyCron.
|
|
type CronRes struct {
|
|
traits.Base
|
|
traits.Edgeable
|
|
traits.Recvable
|
|
traits.Refreshable // needed because we embed a svc res
|
|
|
|
init *engine.Init
|
|
|
|
// Unit is the name of the systemd service unit. It is only necessary to
|
|
// set if you want to specify a service with a different name than the
|
|
// resource.
|
|
Unit string `lang:"unit" yaml:"unit"`
|
|
|
|
// State must be 'exists' or 'absent'.
|
|
State string `lang:"state" yaml:"state"`
|
|
|
|
// Startup specifies what should happen on startup. Values can be:
|
|
// enabled, disabled, and undefined (empty string). We default to
|
|
// enabled.
|
|
Startup string `lang:"startup" yaml:"startup"`
|
|
|
|
// Session, if true, creates the timer as the current user, rather than
|
|
// root. The service it points to must also be a user unit. It defaults
|
|
// to false.
|
|
Session bool `lang:"session" yaml:"session"`
|
|
|
|
// Trigger is the type of timer. Valid types are 'OnCalendar',
|
|
// 'OnActiveSec'. 'OnBootSec'. 'OnStartupSec'. 'OnUnitActiveSec', and
|
|
// 'OnUnitInactiveSec'. For more information see 'man systemd.timer'.
|
|
Trigger string `lang:"trigger" yaml:"trigger"`
|
|
|
|
// Time must be used with all triggers. For 'OnCalendar', it must be in
|
|
// the format defined in 'man systemd-time' under the heading 'Calendar
|
|
// Events'. For all other triggers, time should be a valid time span as
|
|
// defined in 'man systemd-time'
|
|
Time string `lang:"time" yaml:"time"`
|
|
|
|
// AccuracySec is the accuracy of the timer in systemd-time time span
|
|
// format. It defaults to one minute.
|
|
AccuracySec string `lang:"accuracysec" yaml:"accuracysec"`
|
|
|
|
// RandomizedDelaySec delays the timer by a randomly selected, evenly
|
|
// distributed amount of time between 0 and the specified time value.
|
|
// The value must be a valid systemd-time time span.
|
|
RandomizedDelaySec string `lang:"randomizeddelaysec" yaml:"randomizeddelaysec"`
|
|
|
|
// Persistent, if true, means the time when the service unit was last
|
|
// triggered is stored on disk. When the timer is activated, the service
|
|
// unit is triggered immediately if it would have been triggered at
|
|
// least once during the time when the timer was inactive. It defaults
|
|
// to false.
|
|
Persistent bool `lang:"persistent" yaml:"persistent"`
|
|
|
|
// WakeSystem, if true, will cause the system to resume from suspend,
|
|
// should it be suspended and if the system supports this. It defaults
|
|
// to false.
|
|
WakeSystem bool `lang:"wakesystem" yaml:"wakesystem"`
|
|
|
|
// RemainAfterElapse, if true, means an elapsed timer will stay loaded,
|
|
// and its state remains queryable. If false, an elapsed timer unit that
|
|
// cannot elapse anymore is unloaded. It defaults to true.
|
|
RemainAfterElapse bool `lang:"remainafterelapse" yaml:"remainafterelapse"`
|
|
|
|
file *FileRes // nested file resource
|
|
recWatcher *recwatch.RecWatcher // recwatcher for nested file
|
|
}
|
|
|
|
// Default returns some sensible defaults for this resource.
|
|
func (obj *CronRes) Default() engine.Res {
|
|
return &CronRes{
|
|
State: "exists",
|
|
Startup: "enabled",
|
|
RemainAfterElapse: true,
|
|
}
|
|
}
|
|
|
|
// makeComposite creates a pointer to a FileRes. The pointer is used to validate
|
|
// and initialize the nested file resource and to apply the file state in
|
|
// CheckApply.
|
|
func (obj *CronRes) makeComposite() (*FileRes, error) {
|
|
p, err := obj.UnitFilePath()
|
|
if err != nil {
|
|
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 {
|
|
return nil, fmt.Errorf("error casting fileres")
|
|
}
|
|
file.State = obj.State
|
|
if obj.State != "absent" {
|
|
s := obj.unitFileContents()
|
|
file.Content = &s
|
|
}
|
|
return file, nil
|
|
}
|
|
|
|
// Validate if the params passed in are valid data.
|
|
func (obj *CronRes) Validate() error {
|
|
// validate state
|
|
if obj.State != "absent" && obj.State != "exists" {
|
|
return fmt.Errorf("state must be 'absent' or 'exists'")
|
|
}
|
|
if obj.Startup != "enabled" && obj.Startup != "disabled" && obj.Startup != "" {
|
|
return fmt.Errorf("startup must be either `enabled` or `disabled` or undefined")
|
|
}
|
|
|
|
// validate trigger
|
|
if obj.State == "absent" && obj.Trigger == "" {
|
|
return nil // if trigger is undefined we can't make a unit file
|
|
}
|
|
if obj.Trigger == "" || obj.Time == "" {
|
|
return fmt.Errorf("trigger and must be set together")
|
|
}
|
|
if obj.Trigger != OnCalendar &&
|
|
obj.Trigger != OnActiveSec &&
|
|
obj.Trigger != OnBootSec &&
|
|
obj.Trigger != OnStartupSec &&
|
|
obj.Trigger != OnUnitActiveSec &&
|
|
obj.Trigger != OnUnitInactiveSec {
|
|
|
|
return fmt.Errorf("invalid trigger")
|
|
}
|
|
|
|
// TODO: Validate time (regex?)
|
|
|
|
// validate nested file
|
|
file, err := obj.makeComposite()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "makeComposite failed in validate")
|
|
}
|
|
if err := file.Validate(); err != nil { // composite resource
|
|
return errwrap.Wrapf(err, "validate failed for embedded file: %s", obj.file)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Init runs some startup code for this resource.
|
|
func (obj *CronRes) Init(init *engine.Init) error {
|
|
var err error
|
|
obj.init = init // save for later
|
|
|
|
obj.file, err = obj.makeComposite()
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "makeComposite failed in init")
|
|
}
|
|
return obj.file.Init(init)
|
|
}
|
|
|
|
// Cleanup is run by the engine to clean up after the resource is done.
|
|
func (obj *CronRes) Cleanup() error {
|
|
if obj.file != nil {
|
|
return obj.file.Cleanup()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Watch for state changes and sends a message to the bus if there is a change.
|
|
func (obj *CronRes) Watch(ctx context.Context) error {
|
|
var bus *dbus.Conn
|
|
var err error
|
|
|
|
// this resource depends on systemd
|
|
if !systemdUtil.IsRunningSystemd() {
|
|
return fmt.Errorf("systemd is not running")
|
|
}
|
|
|
|
// create a private message bus
|
|
if obj.Session {
|
|
bus, err = util.SessionBusPrivateUsable()
|
|
} else {
|
|
bus, err = util.SystemBusPrivateUsable()
|
|
}
|
|
if err != nil {
|
|
return errwrap.Wrapf(err, "failed to connect to bus")
|
|
}
|
|
defer bus.Close()
|
|
|
|
// dbus addmatch arguments for the timer unit
|
|
args := []string{}
|
|
args = append(args, "type='signal'")
|
|
args = append(args, "interface='org.freedesktop.systemd1.Manager'")
|
|
//args = append(args, "eavesdrop='true'") // XXX: not allowed anymore?
|
|
args = append(args, fmt.Sprintf("arg2='%s.timer'", obj.Name()))
|
|
|
|
// match dbus messages
|
|
if call := bus.BusObject().Call(engineUtil.DBusAddMatch, 0, strings.Join(args, ",")); call.Err != nil {
|
|
return call.Err
|
|
}
|
|
defer bus.BusObject().Call(engineUtil.DBusRemoveMatch, 0, args) // ignore the error
|
|
|
|
// channels for dbus signal
|
|
dbusChan := make(chan *dbus.Signal)
|
|
defer close(dbusChan)
|
|
bus.Signal(dbusChan)
|
|
defer bus.RemoveSignal(dbusChan) // not needed here, but nice for symmetry
|
|
|
|
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(p, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer obj.recWatcher.Close()
|
|
|
|
obj.init.Running() // when started, notify engine that we're running
|
|
|
|
var send = false // send event?
|
|
for {
|
|
select {
|
|
case event := <-dbusChan:
|
|
// process dbus events
|
|
if obj.init.Debug {
|
|
obj.init.Logf("%+v", event)
|
|
}
|
|
send = true
|
|
|
|
case event, ok := <-obj.recWatcher.Events():
|
|
// process unit file recwatch events
|
|
if !ok { // channel shutdown
|
|
return nil
|
|
}
|
|
if err := event.Error; err != nil {
|
|
return errwrap.Wrapf(err, "Unknown %s watcher error", obj)
|
|
}
|
|
if obj.init.Debug {
|
|
obj.init.Logf("Event(%s): %v", event.Body.Name, event.Body.Op)
|
|
}
|
|
send = true
|
|
|
|
case <-ctx.Done(): // closed by the engine to signal shutdown
|
|
return nil
|
|
}
|
|
// do all our event sending all together to avoid duplicate msgs
|
|
if send {
|
|
send = false
|
|
obj.init.Event() // notify engine of an event (this can block)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 *CronRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
checkOK := true
|
|
// use the embedded file resource to apply the correct state
|
|
if c, err := obj.file.CheckApply(ctx, apply); err != nil {
|
|
return false, errwrap.Wrapf(err, "nested file failed")
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
// check timer state and apply the defined state if needed
|
|
if c, err := obj.unitCheckApply(ctx, apply); err != nil {
|
|
return false, errwrap.Wrapf(err, "unitCheckApply error")
|
|
} else if !c {
|
|
checkOK = false
|
|
}
|
|
return checkOK, nil
|
|
}
|
|
|
|
// unitCheckApply checks the state of the systemd-timer unit and, if apply is
|
|
// true, applies the defined state.
|
|
func (obj *CronRes) unitCheckApply(ctx context.Context, apply bool) (bool, error) {
|
|
var conn *sdbus.Conn
|
|
var godbusConn *dbus.Conn
|
|
var err error
|
|
|
|
// this resource depends on systemd to ensure that it's running
|
|
if !systemdUtil.IsRunningSystemd() {
|
|
return false, fmt.Errorf("systemd is not running")
|
|
}
|
|
// go-systemd connection
|
|
if obj.Session {
|
|
conn, err = sdbus.NewUserConnection()
|
|
} else {
|
|
conn, err = sdbus.New() // system bus
|
|
}
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error making go-systemd dbus connection")
|
|
}
|
|
defer conn.Close()
|
|
|
|
// get the load state and active state of the timer unit
|
|
loadState, err := conn.GetUnitProperty(fmt.Sprintf("%s.timer", obj.Name()), "LoadState")
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "failed to get load state")
|
|
}
|
|
activeState, err := conn.GetUnitProperty(fmt.Sprintf("%s.timer", obj.Name()), "ActiveState")
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "failed to get active state")
|
|
}
|
|
// check the timer unit state
|
|
if obj.State == "absent" && loadState.Value == dbus.MakeVariant("not-found") {
|
|
return true, nil
|
|
}
|
|
if obj.State == "exists" && activeState.Value == dbus.MakeVariant("active") {
|
|
return true, nil
|
|
}
|
|
|
|
if !apply {
|
|
return false, nil
|
|
}
|
|
|
|
// systemctl daemon-reload
|
|
if err := conn.ReloadContext(ctx); err != nil {
|
|
return false, errwrap.Wrapf(err, "error reloading daemon")
|
|
}
|
|
|
|
// godbus connection for stopping/restarting the unit
|
|
if obj.Session {
|
|
godbusConn, err = util.SessionBusPrivateUsable()
|
|
} else {
|
|
godbusConn, err = util.SystemBusPrivateUsable()
|
|
}
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "error making godbus connection")
|
|
}
|
|
defer godbusConn.Close()
|
|
|
|
// We probably always want to enable this...
|
|
svc := fmt.Sprintf("%s.timer", obj.Name()) // systemd name
|
|
files := []string{svc} // the svc represented in a list
|
|
if obj.Startup == "enabled" {
|
|
_, _, err = conn.EnableUnitFilesContext(ctx, files, false, true)
|
|
} else if obj.Startup == "disabled" {
|
|
_, err = conn.DisableUnitFilesContext(ctx, files, false)
|
|
}
|
|
if err != nil {
|
|
return false, errwrap.Wrapf(err, "unable to change startup status")
|
|
}
|
|
|
|
// stop or restart the unit
|
|
if obj.State == "absent" {
|
|
return false, engineUtil.StopUnit(ctx, godbusConn, fmt.Sprintf("%s.timer", obj.Name()))
|
|
}
|
|
return false, engineUtil.RestartUnit(ctx, godbusConn, fmt.Sprintf("%s.timer", obj.Name()))
|
|
}
|
|
|
|
// Cmp compares two resources and returns an error if they are not equivalent.
|
|
func (obj *CronRes) Cmp(r engine.Res) error {
|
|
res, ok := r.(*CronRes)
|
|
if !ok {
|
|
return fmt.Errorf("res is not the same kind")
|
|
}
|
|
|
|
if obj.State != res.State {
|
|
return fmt.Errorf("state differs: %s vs %s", obj.State, res.State)
|
|
}
|
|
if obj.Startup != res.Startup {
|
|
return fmt.Errorf("the Startup differs")
|
|
}
|
|
if obj.Trigger != res.Trigger {
|
|
return fmt.Errorf("trigger differs: %s vs %s", obj.Trigger, res.Trigger)
|
|
}
|
|
if obj.Time != res.Time {
|
|
return fmt.Errorf("time differs: %s vs %s", obj.Time, res.Time)
|
|
}
|
|
if obj.AccuracySec != res.AccuracySec {
|
|
return fmt.Errorf("accuracysec differs: %s vs %s", obj.AccuracySec, res.AccuracySec)
|
|
}
|
|
if obj.RandomizedDelaySec != res.RandomizedDelaySec {
|
|
return fmt.Errorf("randomizeddelaysec differs: %s vs %s", obj.RandomizedDelaySec, res.RandomizedDelaySec)
|
|
}
|
|
if obj.Unit != res.Unit {
|
|
return fmt.Errorf("unit differs: %s vs %s", obj.Unit, res.Unit)
|
|
}
|
|
if obj.Persistent != res.Persistent {
|
|
return fmt.Errorf("persistent differs: %t vs %t", obj.Persistent, res.Persistent)
|
|
}
|
|
if obj.WakeSystem != res.WakeSystem {
|
|
return fmt.Errorf("wakesystem differs: %t vs %t", obj.WakeSystem, res.WakeSystem)
|
|
}
|
|
if obj.RemainAfterElapse != res.RemainAfterElapse {
|
|
return fmt.Errorf("remainafterelapse differs: %t vs %t", obj.RemainAfterElapse, res.RemainAfterElapse)
|
|
}
|
|
return obj.file.Cmp(r)
|
|
}
|
|
|
|
// CronUID is a unique resource identifier.
|
|
type CronUID 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
|
|
|
|
unit string // name of target unit
|
|
session bool // user session
|
|
}
|
|
|
|
// IFF aka if and only if they are equivalent, return true. If not, false.
|
|
func (obj *CronUID) IFF(uid engine.ResUID) bool {
|
|
res, ok := uid.(*CronUID)
|
|
if !ok {
|
|
return false
|
|
}
|
|
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 {
|
|
unit := fmt.Sprintf("%s.service", obj.Name())
|
|
if obj.Unit != "" {
|
|
unit = obj.Unit
|
|
}
|
|
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. It is
|
|
// primarily useful for setting the defaults.
|
|
func (obj *CronRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|
type rawRes CronRes // indirection to avoid infinite recursion
|
|
|
|
def := obj.Default() // get the default
|
|
res, ok := def.(*CronRes) // put in the right format
|
|
if !ok {
|
|
return fmt.Errorf("could not convert to CronRes")
|
|
}
|
|
raw := rawRes(*res) // convert; the defaults go here
|
|
|
|
if err := unmarshal(&raw); err != nil {
|
|
return err
|
|
}
|
|
|
|
*obj = CronRes(raw) // restore from indirection with type conversion!
|
|
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 {
|
|
u := []*unit.UnitOption{}
|
|
|
|
// [Unit]
|
|
u = append(u, &unit.UnitOption{Section: "Unit", Name: "Description", Value: "timer generated by mgmt"})
|
|
// [Timer]
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: obj.Trigger, Value: obj.Time})
|
|
if obj.AccuracySec != "" {
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "AccuracySec", Value: obj.AccuracySec})
|
|
}
|
|
if obj.RandomizedDelaySec != "" {
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "RandomizedDelaySec", Value: obj.RandomizedDelaySec})
|
|
}
|
|
if obj.Unit != "" {
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "Unit", Value: obj.Unit})
|
|
}
|
|
if obj.Persistent != false { // defaults to false
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "Persistent", Value: "true"})
|
|
}
|
|
if obj.WakeSystem != false { // defaults to false
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "WakeSystem", Value: "true"})
|
|
}
|
|
if obj.RemainAfterElapse != true { // defaults to true
|
|
u = append(u, &unit.UnitOption{Section: "Timer", Name: "RemainAfterElapse", Value: "false"})
|
|
}
|
|
// [Install]
|
|
u = append(u, &unit.UnitOption{Section: "Install", Name: "WantedBy", Value: "timers.target"})
|
|
|
|
buf := new(bytes.Buffer)
|
|
buf.ReadFrom(unit.Serialize(u))
|
|
return buf.String()
|
|
}
|