~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/service/systemd/conf.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package systemd
 
5
 
 
6
import (
 
7
        "bytes"
 
8
        "fmt"
 
9
        "io/ioutil"
 
10
        "strconv"
 
11
        "strings"
 
12
 
 
13
        "github.com/coreos/go-systemd/unit"
 
14
        "github.com/juju/errors"
 
15
        "github.com/juju/utils/os"
 
16
        "github.com/juju/utils/shell"
 
17
 
 
18
        "github.com/juju/juju/service/common"
 
19
)
 
20
 
 
21
var limitMap = map[string]string{
 
22
        "as":         "LimitAS",
 
23
        "core":       "LimitCORE",
 
24
        "cpu":        "LimitCPU",
 
25
        "data":       "LimitDATA",
 
26
        "fsize":      "LimitFSIZE",
 
27
        "memlock":    "LimitMEMLOCK",
 
28
        "msgqueue":   "LimitMSGQUEUE",
 
29
        "nice":       "LimitNICE",
 
30
        "nofile":     "LimitNOFILE",
 
31
        "nproc":      "LimitNPROC",
 
32
        "rss":        "LimitRSS",
 
33
        "rtprio":     "LimitRTPRIO",
 
34
        "sigpending": "LimitSIGPENDING",
 
35
        "stack":      "LimitSTACK",
 
36
}
 
37
 
 
38
// TODO(ericsnow) Move normalize to common.Conf.Normalize.
 
39
 
 
40
type confRenderer interface {
 
41
        shell.Renderer
 
42
        shell.ScriptRenderer
 
43
}
 
44
 
 
45
func syslogUserGroup() (string, string) {
 
46
        switch os.HostOS() {
 
47
        case os.CentOS:
 
48
                return "root", "adm"
 
49
        default:
 
50
                return "syslog", "syslog"
 
51
        }
 
52
}
 
53
 
 
54
// normalize adjusts the conf to more standardized content and
 
55
// returns a new Conf with that updated content. It also returns the
 
56
// content of any script file that should accompany the conf.
 
57
func normalize(name string, conf common.Conf, scriptPath string, renderer confRenderer) (common.Conf, []byte) {
 
58
        var data []byte
 
59
 
 
60
        var cmds []string
 
61
        if conf.Logfile != "" {
 
62
                filename := conf.Logfile
 
63
                cmds = append(cmds, "# Set up logging.")
 
64
                cmds = append(cmds, renderer.Touch(filename, nil)...)
 
65
                // TODO(ericsnow) We should drop the assumption that the logfile
 
66
                // is syslog.
 
67
                user, group := syslogUserGroup()
 
68
                cmds = append(cmds, renderer.Chown(filename, user, group)...)
 
69
                cmds = append(cmds, renderer.Chmod(filename, 0600)...)
 
70
                cmds = append(cmds, renderer.RedirectOutput(filename)...)
 
71
                cmds = append(cmds, renderer.RedirectFD("out", "err")...)
 
72
                cmds = append(cmds,
 
73
                        "",
 
74
                        "# Run the script.",
 
75
                )
 
76
                // We leave conf.Logfile alone (it will be ignored during validation).
 
77
        }
 
78
        cmds = append(cmds, conf.ExecStart)
 
79
 
 
80
        if conf.ExtraScript != "" {
 
81
                cmds = append([]string{conf.ExtraScript}, cmds...)
 
82
                conf.ExtraScript = ""
 
83
        }
 
84
        if !isSimpleCommand(strings.Join(cmds, "\n")) {
 
85
                data = renderer.RenderScript(cmds)
 
86
                conf.ExecStart = scriptPath
 
87
        }
 
88
 
 
89
        if len(conf.Env) == 0 {
 
90
                conf.Env = nil
 
91
        }
 
92
 
 
93
        if len(conf.Limit) == 0 {
 
94
                conf.Limit = nil
 
95
        }
 
96
 
 
97
        if conf.Transient {
 
98
                // TODO(ericsnow) Handle Transient via systemd-run command?
 
99
                conf.ExecStopPost = commands{}.disable(name)
 
100
        }
 
101
 
 
102
        return conf, data
 
103
}
 
104
 
 
105
func isSimpleCommand(cmd string) bool {
 
106
        if strings.ContainsAny(cmd, "\n;|><&") {
 
107
                return false
 
108
        }
 
109
 
 
110
        return true
 
111
}
 
112
 
 
113
func validate(name string, conf common.Conf, renderer shell.Renderer) error {
 
114
        if name == "" {
 
115
                return errors.NotValidf("missing service name")
 
116
        }
 
117
 
 
118
        if err := conf.Validate(renderer); err != nil {
 
119
                return errors.Trace(err)
 
120
        }
 
121
 
 
122
        if conf.ExtraScript != "" {
 
123
                return errors.NotValidf("unexpected ExtraScript")
 
124
        }
 
125
 
 
126
        // We ignore Desc and Logfile.
 
127
 
 
128
        for k := range conf.Limit {
 
129
                if _, ok := limitMap[k]; !ok {
 
130
                        return errors.NotValidf("conf.Limit key %q", k)
 
131
                }
 
132
        }
 
133
 
 
134
        return nil
 
135
}
 
136
 
 
137
// serialize returns the data that should be written to disk for the
 
138
// provided Conf, rendered in the systemd unit file format.
 
139
func serialize(name string, conf common.Conf, renderer shell.Renderer) ([]byte, error) {
 
140
        if err := validate(name, conf, renderer); err != nil {
 
141
                return nil, errors.Trace(err)
 
142
        }
 
143
 
 
144
        var unitOptions []*unit.UnitOption
 
145
        unitOptions = append(unitOptions, serializeUnit(conf)...)
 
146
        unitOptions = append(unitOptions, serializeService(conf)...)
 
147
        unitOptions = append(unitOptions, serializeInstall(conf)...)
 
148
        // Don't use unit.Serialize because it has map ordering issues.
 
149
        // Serialize copied locally, and outputs sections in alphabetical order.
 
150
        data, err := ioutil.ReadAll(UnitSerialize(unitOptions))
 
151
        return data, errors.Trace(err)
 
152
}
 
153
 
 
154
func serializeUnit(conf common.Conf) []*unit.UnitOption {
 
155
        var unitOptions []*unit.UnitOption
 
156
 
 
157
        if conf.Desc != "" {
 
158
                unitOptions = append(unitOptions, &unit.UnitOption{
 
159
                        Section: "Unit",
 
160
                        Name:    "Description",
 
161
                        Value:   conf.Desc,
 
162
                })
 
163
        }
 
164
 
 
165
        after := []string{
 
166
                "syslog.target",
 
167
                "network.target",
 
168
                "systemd-user-sessions.service",
 
169
        }
 
170
        for _, name := range after {
 
171
                unitOptions = append(unitOptions, &unit.UnitOption{
 
172
                        Section: "Unit",
 
173
                        Name:    "After",
 
174
                        Value:   name,
 
175
                })
 
176
        }
 
177
 
 
178
        if conf.AfterStopped != "" {
 
179
                unitOptions = append(unitOptions, &unit.UnitOption{
 
180
                        Section: "Unit",
 
181
                        Name:    "After",
 
182
                        Value:   conf.AfterStopped,
 
183
                })
 
184
        }
 
185
 
 
186
        return unitOptions
 
187
}
 
188
 
 
189
func serializeService(conf common.Conf) []*unit.UnitOption {
 
190
        var unitOptions []*unit.UnitOption
 
191
 
 
192
        // TODO(ericsnow) Support "Type" (e.g. "forking")? For now we just
 
193
        // use the default, "simple".
 
194
 
 
195
        for k, v := range conf.Env {
 
196
                unitOptions = append(unitOptions, &unit.UnitOption{
 
197
                        Section: "Service",
 
198
                        Name:    "Environment",
 
199
                        Value:   fmt.Sprintf(`"%s=%s"`, k, v),
 
200
                })
 
201
        }
 
202
 
 
203
        for k, v := range conf.Limit {
 
204
                unitOptions = append(unitOptions, &unit.UnitOption{
 
205
                        Section: "Service",
 
206
                        Name:    limitMap[k],
 
207
                        Value:   strconv.Itoa(v),
 
208
                })
 
209
        }
 
210
 
 
211
        if conf.ExecStart != "" {
 
212
                unitOptions = append(unitOptions, &unit.UnitOption{
 
213
                        Section: "Service",
 
214
                        Name:    "ExecStart",
 
215
                        Value:   conf.ExecStart,
 
216
                })
 
217
        }
 
218
 
 
219
        // TODO(ericsnow) This should key off Conf.Restart, once added.
 
220
        if !conf.Transient {
 
221
                unitOptions = append(unitOptions, &unit.UnitOption{
 
222
                        Section: "Service",
 
223
                        Name:    "Restart",
 
224
                        Value:   "on-failure",
 
225
                })
 
226
        }
 
227
 
 
228
        if conf.Timeout > 0 {
 
229
                unitOptions = append(unitOptions, &unit.UnitOption{
 
230
                        Section: "Service",
 
231
                        Name:    "TimeoutSec",
 
232
                        Value:   strconv.Itoa(conf.Timeout),
 
233
                })
 
234
        }
 
235
 
 
236
        if conf.ExecStopPost != "" {
 
237
                unitOptions = append(unitOptions, &unit.UnitOption{
 
238
                        Section: "Service",
 
239
                        Name:    "ExecStopPost",
 
240
                        Value:   conf.ExecStopPost,
 
241
                })
 
242
        }
 
243
 
 
244
        return unitOptions
 
245
}
 
246
 
 
247
func serializeInstall(conf common.Conf) []*unit.UnitOption {
 
248
        var unitOptions []*unit.UnitOption
 
249
 
 
250
        unitOptions = append(unitOptions, &unit.UnitOption{
 
251
                Section: "Install",
 
252
                Name:    "WantedBy",
 
253
                Value:   "multi-user.target",
 
254
        })
 
255
 
 
256
        return unitOptions
 
257
}
 
258
 
 
259
// deserialize parses the provided data (in the systemd unit file
 
260
// format) and populates a new Conf with the result.
 
261
func deserialize(data []byte, renderer shell.Renderer) (common.Conf, error) {
 
262
        opts, err := unit.Deserialize(bytes.NewBuffer(data))
 
263
        if err != nil {
 
264
                return common.Conf{}, errors.Trace(err)
 
265
        }
 
266
        return deserializeOptions(opts, renderer)
 
267
}
 
268
 
 
269
func deserializeOptions(opts []*unit.UnitOption, renderer shell.Renderer) (common.Conf, error) {
 
270
        var conf common.Conf
 
271
 
 
272
        for _, uo := range opts {
 
273
                switch uo.Section {
 
274
                case "Unit":
 
275
                        switch uo.Name {
 
276
                        case "Description":
 
277
                                conf.Desc = uo.Value
 
278
                        case "After":
 
279
                                // Do nothing until we support it in common.Conf.
 
280
                        default:
 
281
                                return conf, errors.NotSupportedf("Unit directive %q", uo.Name)
 
282
                        }
 
283
                case "Service":
 
284
                        switch {
 
285
                        case uo.Name == "ExecStart":
 
286
                                conf.ExecStart = uo.Value
 
287
                        case uo.Name == "Environment":
 
288
                                if conf.Env == nil {
 
289
                                        conf.Env = make(map[string]string)
 
290
                                }
 
291
                                var value = uo.Value
 
292
                                if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
 
293
                                        value = value[1 : len(value)-1]
 
294
                                }
 
295
                                parts := strings.SplitN(value, "=", 2)
 
296
                                if len(parts) != 2 {
 
297
                                        return conf, errors.NotValidf("service environment value %q", uo.Value)
 
298
                                }
 
299
                                conf.Env[parts[0]] = parts[1]
 
300
                        case strings.HasPrefix(uo.Name, "Limit"):
 
301
                                if conf.Limit == nil {
 
302
                                        conf.Limit = make(map[string]int)
 
303
                                }
 
304
                                for k, v := range limitMap {
 
305
                                        if v == uo.Name {
 
306
                                                n, err := strconv.Atoi(uo.Value)
 
307
                                                if err != nil {
 
308
                                                        return conf, errors.Trace(err)
 
309
                                                }
 
310
                                                conf.Limit[k] = n
 
311
                                                break
 
312
                                        }
 
313
                                }
 
314
                        case uo.Name == "TimeoutSec":
 
315
                                timeout, err := strconv.Atoi(uo.Value)
 
316
                                if err != nil {
 
317
                                        return conf, errors.Trace(err)
 
318
                                }
 
319
                                conf.Timeout = timeout
 
320
                        case uo.Name == "Type":
 
321
                                // Do nothing until we support it in common.Conf.
 
322
                        case uo.Name == "RemainAfterExit":
 
323
                                // Do nothing until we support it in common.Conf.
 
324
                        case uo.Name == "Restart":
 
325
                                // Do nothing until we support it in common.Conf.
 
326
                        default:
 
327
                                return conf, errors.NotSupportedf("Service directive %q", uo.Name)
 
328
                        }
 
329
                case "Install":
 
330
                        switch uo.Name {
 
331
                        case "WantedBy":
 
332
                                if uo.Value != "multi-user.target" {
 
333
                                        return conf, errors.NotValidf("unit target %q", uo.Value)
 
334
                                }
 
335
                        default:
 
336
                                return conf, errors.NotSupportedf("Install directive %q", uo.Name)
 
337
                        }
 
338
                default:
 
339
                        return conf, errors.NotSupportedf("section %q", uo.Name)
 
340
                }
 
341
        }
 
342
 
 
343
        err := validate("<>", conf, renderer)
 
344
        return conf, errors.Trace(err)
 
345
}
 
346
 
 
347
// CleanShutdownService is added to machines to ensure DHCP-assigned
 
348
// IP addresses are released on shutdown, reboot, or halt. See bug
 
349
// http://pad.lv/1348663 for more info.
 
350
const CleanShutdownService = `
 
351
[Unit]
 
352
Description=Stop all network interfaces on shutdown
 
353
DefaultDependencies=false
 
354
After=final.target
 
355
 
 
356
[Service]
 
357
Type=oneshot
 
358
ExecStart=/sbin/ifdown -a -v --force
 
359
StandardOutput=tty
 
360
StandardError=tty
 
361
 
 
362
[Install]
 
363
WantedBy=final.target
 
364
`
 
365
 
 
366
// CleanShutdownServicePath is the full file path where
 
367
// CleanShutdownService is created.
 
368
const CleanShutdownServicePath = "/etc/systemd/system/juju-clean-shutdown.service"