service.go

v0.3.0
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
}
30
31
// Name returns the step name.
32
func (s Service) Name() string { return "service:" + s.Unit }
33
34
// Check reports a change is needed when the unit's active or enabled state
35
// differs from the desired state. ServiceRestarted always needs a change.
36
func (s Service) Check(fc *legatus.FlowCtx) (bool, error) {
37
	if s.State == ServiceRestarted {
38
		return true, nil
39
	}
40
41
	active, err := s.isActive(fc)
42
	if err != nil {
43
		return false, err
44
	}
45
	switch s.State {
46
	case ServiceRunning:
47
		if !active {
48
			return true, nil
49
		}
50
	case ServiceStopped:
51
		if active {
52
			return true, nil
53
		}
54
	default:
55
		return false, culpa.WithCode(culpa.Errorf("service %s: unknown state %q", s.Unit, s.State), "SERVICE_STATE")
56
	}
57
58
	if s.Enabled != nil {
59
		enabled, err := s.isEnabled(fc)
60
		if err != nil {
61
			return false, err
62
		}
63
		if enabled != *s.Enabled {
64
			return true, nil
65
		}
66
	}
67
	return false, nil
68
}
69
70
// Apply converges run state and enablement.
71
func (s Service) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
72
	var actions []string
73
74
	switch s.State {
75
	case ServiceRunning:
76
		if err := s.systemctl(fc, "start"); err != nil {
77
			return legatus.Outcome{}, err
78
		}
79
		actions = append(actions, "start")
80
	case ServiceStopped:
81
		if err := s.systemctl(fc, "stop"); err != nil {
82
			return legatus.Outcome{}, err
83
		}
84
		actions = append(actions, "stop")
85
	case ServiceRestarted:
86
		if err := s.systemctl(fc, "restart"); err != nil {
87
			return legatus.Outcome{}, err
88
		}
89
		actions = append(actions, "restart")
90
	default:
91
		return legatus.Outcome{}, culpa.WithCode(culpa.Errorf("service %s: unknown state %q", s.Unit, s.State), "SERVICE_STATE")
92
	}
93
94
	if s.Enabled != nil {
95
		verb := "disable"
96
		if *s.Enabled {
97
			verb = "enable"
98
		}
99
		if err := s.systemctl(fc, verb); err != nil {
100
			return legatus.Outcome{}, err
101
		}
102
		actions = append(actions, verb)
103
	}
104
105
	return legatus.Outcome{Changed: true, Message: s.Unit + ": " + strings.Join(actions, ",")}, nil
106
}
107
108
func (s Service) isActive(fc *legatus.FlowCtx) (bool, error) {
109
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{"is-active", s.Unit}})
110
	if err != nil {
111
		return false, culpa.WithCode(culpa.Wrapf(err, "is-active %s", s.Unit), "SERVICE_PROBE")
112
	}
113
	return strings.TrimSpace(string(res.Stdout)) == "active", nil
114
}
115
116
func (s Service) isEnabled(fc *legatus.FlowCtx) (bool, error) {
117
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{"is-enabled", s.Unit}})
118
	if err != nil {
119
		return false, culpa.WithCode(culpa.Wrapf(err, "is-enabled %s", s.Unit), "SERVICE_PROBE")
120
	}
121
	return strings.TrimSpace(string(res.Stdout)) == "enabled", nil
122
}
123
124
func (s Service) systemctl(fc *legatus.FlowCtx, verb string) error {
125
	res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "systemctl", Args: []string{verb, s.Unit}, Sudo: true})
126
	if err != nil {
127
		return culpa.WithCode(culpa.Wrapf(err, "systemctl %s %s", verb, s.Unit), "SERVICE_APPLY")
128
	}
129
	if res.ExitCode != 0 {
130
		return culpa.WithCode(
131
			culpa.Errorf("systemctl %s %s: exit %d: %s", verb, s.Unit, res.ExitCode, strings.TrimSpace(string(res.Combined()))),
132
			"SERVICE_APPLY")
133
	}
134
	return nil
135
}
136

Source Files