| 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 | |