/
resource.go
420 lines (379 loc) · 14.4 KB
/
resource.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package unit
import (
"fmt"
"github.com/pkg/errors"
log "github.com/Sirupsen/logrus"
"github.com/asteris-llc/converge/resource"
"golang.org/x/net/context"
)
// Resource is the resource struct for systemd.unit.
//
// Many of the exported fields are derived from the systemd dbus api; see
// https://www.freedesktop.org/wiki/Software/systemd/dbus/ for a full
// description of the potential values for this fields and their meanings.
type Resource struct {
// The name of the unit, including the unit type.
Name string `export:"unit"`
// The desired state of the unit as configured by the user. It will be one of
// `running`, `stopped`, or `restarted` if it was configured by the user, and
// an empty string otherwise.
State string `export:"state"`
// This field is set to true if the reload flag was configured by the user.
Reload bool `export:"reload"`
// The human-readable name of a unix signal that will be sent to the process.
// If this is set the name will match the field set in SignalNumber. See the
// man pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a
// full explanation of these signals.
SignalName string `export:"signal_name"`
// The numeric identifier of a unix signal that will be sent to the process.
// If this is set it will match the field set in SignalName. See the man
// pages for `signal(3)` on BSD/Darwin or `signal(7)` on GNU Linux for a full
// explanation of these signals.
SignalNumber uint `export:"signal_number"`
// The full path to the unit file on disk. This field will be empty if the
// unit was not started from a systemd unit file on disk.
Path string `export:"path"`
// Description of the services. This field will be empty unless a description
// has been provided in the systemd unit file.
Description string `export:"description"`
// The active state of the unit. It will always be one of: `active`,
// `reloading`, `inactive`, `failed`, `activating`, `deactivating`.
ActiveState string `export:"activestate"`
// The load state of the unit.
LoadState string `export:"loadstate"`
// The type of the unit as an enumerated value. See TypeStr for a human
// readable type.
Type UnitType `export:"type"`
// The type of the unit as a human readable string. See the man page for
// `systemd(1)` for a full description of the types and their meaning.
TypeStr string `export:"typestr"`
// The status represents the current status of the process. It will be
// initialized during planning and updated after apply to reflect any changes.
Status string `export:"status"`
// Properties are the global systemd unit properties and will be set for all
// unit types. See the [systemd_Properties]({{< ref "properties.md" >}}) docs
// for more information.
Properties Properties `re-export-as:"global_properties"`
// ServiceProperties contain properties specific to Service unit types. This
// field is only exported when the unit type is `service`. See the
// [systemd_ServiceTypeProperties]({{< ref "service_properties.md" >}}) docs
// for more information.
ServiceProperties *ServiceTypeProperties `re-export-as:"service_properties"`
// SocketProperties contain properties specific to Socket unit types. This
// field is only exported when the unit type is `socket`. See the
// [systemd_SocketTypeProperties]({{< ref "socket_properties.md" >}}) docs for
// more information.
SocketProperties *SocketTypeProperties `re-export-as:"socket_properties"`
// DeviceProperties contain properties specific to Device unit types. This
// field is only exported when the unit type is `device`. See the
// [systemd_DeviceTypeProperties]({{< ref "device_properties.md" >}}) docs for
// more information.
DeviceProperties *DeviceTypeProperties `re-export-as:"device_properties"`
// MountProperties contain properties specific to Mount unit types. This field
// is only exported when the unit type is `mount`. See the
// [systemd_MountTypeProperties]({{< ref "mount_properties.md" >}}) docs for
// more information.
MountProperties *MountTypeProperties `re-export-as:"mount_properties"`
// AutomountProperties contain properties specific to Autoumount unit types.
// This field is only exported when the unit type is`automount`. See the
// [systemd_AutomountTypeProperties]({{< ref "automount_properties.md" >}})
// docs for more information.
AutomountProperties *AutomountTypeProperties `re-export-as:"automount_properties"`
// SwapProperties contain properties specific to Swap unit types. This field
// is only exported when the unit type is `swap`. See the
// [systemd_SwapTypeProperties]({{< ref "swap_properties.md" >}}) docs for
// more information.
SwapProperties *SwapTypeProperties `re-export-as:"swap_properties"`
// PathProperties contain properties specific to Path unit types. This field
// is only exported when the unit type is `path`. See the
// [systemd_PathTypeProperties]({{< ref "path_properties.md" >}}) docs for
// more information.
PathProperties *PathTypeProperties `re-export-as:"path_properties"`
// TimerProperties contain properties specific to Timer unit types. This field
// is only exported when the unit type is `timer`. See the
// [systemd_TimerTypeProperties]({{< ref "timer_properties.md" >}}) docs for
// more information.
TimerProperties *TimerTypeProperties `re-export-as:"timer_properties"`
// SliceProperties contain properties specific to Slice unit types. This field
// is only exported when the unit type is `slice`. See the
// [systemd_SliceTypeProperties]({{< ref "slice_properties.md" >}}) docs for
// more information.
SliceProperties *SliceTypeProperties `re-export-as:"slice_properties"`
// ScopeProperties contain properties specific to Scope unit types. This field
// is only exported when the unit type is `scope`. See the
// [systemd_ScopeTypeProperties]({{< ref "scope_properties.md" >}}) docs for
// more information.
ScopeProperties *ScopeTypeProperties `re-export-as:"scope_properties"`
sendSignal bool
systemdExecutor SystemdExecutor
hasRun bool
}
type response struct {
status resource.TaskStatus
err error
}
func wrapCall(f func() (resource.TaskStatus, error)) <-chan response {
resp := make(chan response)
go func() {
st, err := f()
resp <- response{st, err}
}()
return resp
}
// Check implements resource.Task
func (r *Resource) Check(ctx context.Context, _ resource.Renderer) (resource.TaskStatus, error) {
ch := wrapCall(r.runCheck)
select {
case <-ctx.Done():
return nil, errors.New("context was cancelled")
case results := <-ch:
return results.status, results.err
}
}
// Apply implemnts resource.Task
func (r *Resource) Apply(ctx context.Context) (resource.TaskStatus, error) {
ch := wrapCall(r.runApply)
select {
case <-ctx.Done():
return nil, errors.New("context was cancelled")
case results := <-ch:
return results.status, results.err
}
}
func (r *Resource) runCheck() (resource.TaskStatus, error) {
status := resource.NewStatus()
u, err := r.systemdExecutor.QueryUnit(r.Name, false)
if err != nil {
status.AddMessage("query unit returned an error: " + err.Error())
return nil, err
}
r.populateFromUnit(u)
if r.sendSignal && !r.hasRun {
status.RaiseLevel(resource.StatusWillChange)
status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
}
if r.Reload && !r.hasRun {
status.RaiseLevel(resource.StatusWillChange)
status.AddMessage("Reloading unit configuration")
status.AddDifference("state", u.ActiveState, "reloaded", "")
}
switch r.State {
case "restarted":
status.RaiseLevel(resource.StatusWillChange)
status.AddMessage("Restarting unit")
status.AddDifference("state", u.ActiveState, "restarted", "")
case "running":
r.shouldStart(u, status)
case "stopped":
r.shouldStop(u, status)
}
r.hasRun = true
return status, nil
}
func (r *Resource) runApply() (resource.TaskStatus, error) {
log.WithField("Unit Name: ", r.Name).Infof("calling runApply()....")
status := resource.NewStatus()
tempStatus := resource.NewStatus()
u, err := r.systemdExecutor.QueryUnit(r.Name, false)
if err != nil {
return nil, err
}
if u.ActiveState == "unknown" {
log.Infof("unable to query information about the unit. " +
"Making a best guess based on configured data")
u.Name = r.Name
}
if r.sendSignal {
status.AddMessage(fmt.Sprintf("Sending signal `%s` to unit", r.SignalName))
r.systemdExecutor.SendSignal(u, Signal(r.SignalNumber))
}
if r.Reload {
status.AddMessage("Reloading unit configuration")
status.AddDifference("state", u.ActiveState, "reloaded", "")
if err := r.systemdExecutor.ReloadUnit(u); err != nil {
return nil, err
}
}
var runstateErr error
switch r.State {
case "running":
if r.shouldStart(u, tempStatus) {
runstateErr = r.systemdExecutor.StartUnit(u)
}
case "stopped":
if r.shouldStop(u, tempStatus) {
runstateErr = r.systemdExecutor.StopUnit(u)
}
case "restarted":
runstateErr = r.systemdExecutor.RestartUnit(u)
}
return status, runstateErr
}
// We copy data from the unit into the resource to make the UX nicer for users
// who want to access systemd information.
func (r *Resource) populateFromUnit(u *Unit) {
r.Description = u.Description
r.Path = u.Path
r.Type = u.Type
r.TypeStr = u.Type.String()
r.Status = u.ActiveState
r.Properties = u.Properties
r.ServiceProperties = u.ServiceProperties
r.SocketProperties = u.SocketProperties
r.DeviceProperties = u.DeviceProperties
r.MountProperties = u.MountProperties
r.AutomountProperties = u.AutomountProperties
r.SwapProperties = u.SwapProperties
r.PathProperties = u.PathProperties
r.TimerProperties = u.TimerProperties
r.SliceProperties = u.SliceProperties
r.ScopeProperties = u.ScopeProperties
}
func (r *Resource) shouldStart(u *Unit, st *resource.Status) bool {
switch u.ActiveState {
case "active":
st.AddMessage("already running")
return false
case "reloading":
st.AddMessage("unit is reloading, will re-check status during apply")
st.RaiseLevel(resource.StatusMayChange)
return true
case "inactive":
st.RaiseLevel(resource.StatusWillChange)
st.AddDifference("state", "inactive", "active", "")
return true
case "failed":
st.AddMessage("unit has failed; will attempt to restart")
if reason, err := getFailedReason(u); err != nil {
st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
} else {
st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
}
st.RaiseLevel(resource.StatusWillChange)
st.AddDifference("state", "failed", "active", "")
return true
case "activating":
st.AddMessage("unit is alread activating, will re-check status during apply")
st.RaiseLevel(resource.StatusMayChange)
return true
case "deactivating":
st.AddMessage("unit is currently deactivating, will re-check status during apply")
st.RaiseLevel(resource.StatusMayChange)
st.AddDifference("state", "deactivating", "active", "")
return true
case "unknown":
st.AddMessage("unit was in an unknown state, will attempt to start")
st.RaiseLevel(resource.StatusMayChange)
st.AddDifference("state", "unknown", "active", "")
return true
}
return true
}
func (r *Resource) shouldStop(u *Unit, st *resource.Status) bool {
switch u.ActiveState {
case "active":
st.AddDifference("state", "active", "inactive", "")
st.RaiseLevel(resource.StatusWillChange)
return true
case "reloading":
st.AddMessage("unit is reloading; will re-check status during apply")
st.AddDifference("state", "reloading", "inactive", "")
st.RaiseLevel(resource.StatusMayChange)
return true
case "inactive":
st.AddMessage("unit is already inactive")
return false
case "failed":
st.AddMessage("unit is not running because it has failed. Will not restart")
if reason, err := getFailedReason(u); err != nil {
st.AddMessage(fmt.Sprintf("cannot determine root cause of failure: %v", err))
} else {
st.AddMessage(fmt.Sprintf("unit previously failed, the message was: %s", reason))
}
return false
case "activating":
st.AddDifference("state", "active", "inactive", "")
st.RaiseLevel(resource.StatusMayChange)
return true
case "deactivating":
st.AddMessage("unit is deactivating. Will re-check status during apply")
st.RaiseLevel(resource.StatusMayChange)
return true
case "unknown":
st.AddMessage("unit was in an unknown state, will attempt to stop")
st.RaiseLevel(resource.StatusMayChange)
st.AddDifference("state", "unknown", "inactive", "")
return true
}
return true
}
func getFailedReason(u *Unit) (string, error) {
err := errors.New("unable to determine cause of failure: no properties available")
var reason string
switch u.Type {
case UnitTypeService:
if u.ServiceProperties == nil {
return "", err
}
reason = u.ServiceProperties.Result
case UnitTypeSocket:
if u.SocketProperties == nil {
return "", err
}
reason = u.SocketProperties.Result
case UnitTypeMount:
if u.MountProperties == nil {
return "", err
}
reason = u.MountProperties.Result
case UnitTypeAutoMount:
if u.AutomountProperties == nil {
return "", err
}
reason = u.AutomountProperties.Result
case UnitTypeSwap:
if u.SwapProperties == nil {
return "", err
}
reason = u.SwapProperties.Result
case UnitTypeTimer:
if u.TimerProperties == nil {
return "", err
}
reason = u.TimerProperties.Result
}
switch reason {
case "success":
return "the unit was activated successfully", nil
case "resources":
return "not enough resources available to create the process", nil
case "timeout":
return "a timeout occurred while starting the unit", nil
case "exit-code":
return "unit exited with a non-zero exit code", nil
case "signal":
return "unit exited due to an unhandled signal", nil
case "core-dump":
return "unit exited and dumped core", nil
case "watchdog":
return "watchdog terminated the service due to slow or missing responses", nil
case "start-limit":
return "process has been restarted too many times", nil
case "service-failed-permanent":
return "continual failure of this socket", nil
}
return "unknown reason", nil
}