service.go

v0.6.1
Doc Versions Source
1
package steps
2
3
import (
4
	"strings"
5
6
	"go.bigb.es/auxilia/culpa"
7
	"go.bigb.es/auxilia/legatus"
8
)
9
10
// ServiceState is the desired run state of a systemd unit.
11
type ServiceState string
12
13
const (
14
	// ServiceRunning requires the unit to be active.
15
	ServiceRunning ServiceState = "running"
16
	// ServiceStopped requires the unit to be inactive.
17
	ServiceStopped ServiceState = "stopped"
18
	// ServiceRestarted forces a restart on every apply (no skip).
19
	ServiceRestarted ServiceState = "restarted"
20
)
21
22
// Service manages a systemd unit (PC3: this battery is opinionated about
23
// systemd). State controls active/inactive; Enabled, when non-nil, controls the
24
// boot-time enablement. Check probes is-active/is-enabled; Apply converges.
25
type Service struct {
26
	Unit    string
27
	State   ServiceState
28
	Enabled *bool
29
	// Sudo controls privilege-wrapping of the systemctl mutations; the zero value
30
	// (SudoDefault) sudo-wraps, preserving the previous always-sudo behavior.
31
	Sudo SudoPolicy
32
}
33
34
// Name returns the step name.
35
func (s Service) Name() string { return "service:" + s.Unit }
36
37
// Check reports a change is needed when the unit's active or enabled state
38
// differs from the desired state. ServiceRestarted always needs a change.
39
func (s Service) Check(fc *legatus.FlowCtx) (bool, error) {
40
	if s.State == ServiceRestarted {
41
		return true, nil
42
	}
43
44
	active, err := s.isActive(fc)
45
	if err != nil {
46
		return false, err
47
	}
48
	switch s.State {
49
	case ServiceRunning:
50
		if !active {
51
			return true, nil
52
		}
53
	case ServiceStopped:
54
		if active {
55
			return true, nil
56
		}
57
	default:
58
		return false, culpa.WithCode(culpa.Errorf("service %s: unknown state %q", s.Unit, s.State), "SERVICE_STATE")
59
	}
60
61
	if s.Enabled != nil {
62
		enabled, err := s.isEnabled(fc)
63
		if err != nil {
64
			return false, err
65
		}
66
		if enabled != *s.Enabled {
67
			return true, nil
68
		}
69
	}
70
	return false, nil
71
}
72
73
// Apply converges run state and enablement.
74
func (s Service) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
75
	var actions []string
76
77
	switch s.State {
78
	case ServiceRunning:
79
		if err := s.systemctl(fc, "start"); err != nil {
80
			return legatus.Outcome{}, err
81
		}
82
		actions = append(actions, "start")
83
	case ServiceStopped:
84
		if err := s.systemctl(fc, "stop"); err != nil {
85
			return legatus.Outcome{}, err
86
		}
87
		actions = append(actions, "stop")
88
	case ServiceRestarted:
89
		if err := s.systemctl(fc, "restart"); err != nil {
90
			return legatus.Outcome{}, err
91
		}
92
		actions = append(actions, "restart")
93
	default:
94
		return legatus.Outcome{}, culpa.WithCode(culpa.Errorf("service %s: unknown state %q", s.Unit, s.State), "SERVICE_STATE")
95
	}
96
97
	if s.Enabled != nil {
98
		verb := "disable"
99
		if *s.Enabled {
100
			verb = "enable"
101
		}
102
		if err := s.systemctl(fc, verb); err != nil {
103
			return legatus.Outcome{}, err
104
		}
105
		actions = append(actions, verb)
106
	}
107
108
	return legatus.Outcome{Changed: true, Message: s.Unit + ": " + strings.Join(actions, ",")}, nil
109
}
110
111
func (s Service) isActive(fc *legatus.FlowCtx) (bool, error) {
112
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{"is-active", s.Unit}})
113
	if err != nil {
114
		return false, culpa.WithCode(culpa.Wrapf(err, "is-active %s", s.Unit), "SERVICE_PROBE")
115
	}
116
	return strings.TrimSpace(string(res.Stdout)) == "active", nil
117
}
118
119
func (s Service) isEnabled(fc *legatus.FlowCtx) (bool, error) {
120
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{"is-enabled", s.Unit}})
121
	if err != nil {
122
		return false, culpa.WithCode(culpa.Wrapf(err, "is-enabled %s", s.Unit), "SERVICE_PROBE")
123
	}
124
	return strings.TrimSpace(string(res.Stdout)) == "enabled", nil
125
}
126
127
func (s Service) systemctl(fc *legatus.FlowCtx, verb string) error {
128
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{verb, s.Unit}, Sudo: s.Sudo.resolve(fc.Host(), true)})
129
	if err != nil {
130
		return culpa.WithCode(culpa.Wrapf(err, "systemctl %s %s", verb, s.Unit), "SERVICE_APPLY")
131
	}
132
	if res.ExitCode != 0 {
133
		return culpa.WithCode(
134
			culpa.Errorf("systemctl %s %s: exit %d: %s", verb, s.Unit, res.ExitCode, strings.TrimSpace(string(res.Combined()))),
135
			"SERVICE_APPLY")
136
	}
137
	return nil
138
}
139

Source Files