1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
13
"github.com/coreos/go-systemd/unit"
14
"github.com/juju/errors"
15
"github.com/juju/utils/os"
16
"github.com/juju/utils/shell"
18
"github.com/juju/juju/service/common"
21
var limitMap = map[string]string{
26
"fsize": "LimitFSIZE",
27
"memlock": "LimitMEMLOCK",
28
"msgqueue": "LimitMSGQUEUE",
30
"nofile": "LimitNOFILE",
31
"nproc": "LimitNPROC",
33
"rtprio": "LimitRTPRIO",
34
"sigpending": "LimitSIGPENDING",
35
"stack": "LimitSTACK",
38
// TODO(ericsnow) Move normalize to common.Conf.Normalize.
40
type confRenderer interface {
45
func syslogUserGroup() (string, string) {
50
return "syslog", "syslog"
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) {
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
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")...)
76
// We leave conf.Logfile alone (it will be ignored during validation).
78
cmds = append(cmds, conf.ExecStart)
80
if conf.ExtraScript != "" {
81
cmds = append([]string{conf.ExtraScript}, cmds...)
84
if !isSimpleCommand(strings.Join(cmds, "\n")) {
85
data = renderer.RenderScript(cmds)
86
conf.ExecStart = scriptPath
89
if len(conf.Env) == 0 {
93
if len(conf.Limit) == 0 {
98
// TODO(ericsnow) Handle Transient via systemd-run command?
99
conf.ExecStopPost = commands{}.disable(name)
105
func isSimpleCommand(cmd string) bool {
106
if strings.ContainsAny(cmd, "\n;|><&") {
113
func validate(name string, conf common.Conf, renderer shell.Renderer) error {
115
return errors.NotValidf("missing service name")
118
if err := conf.Validate(renderer); err != nil {
119
return errors.Trace(err)
122
if conf.ExtraScript != "" {
123
return errors.NotValidf("unexpected ExtraScript")
126
// We ignore Desc and Logfile.
128
for k := range conf.Limit {
129
if _, ok := limitMap[k]; !ok {
130
return errors.NotValidf("conf.Limit key %q", k)
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)
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)
154
func serializeUnit(conf common.Conf) []*unit.UnitOption {
155
var unitOptions []*unit.UnitOption
158
unitOptions = append(unitOptions, &unit.UnitOption{
168
"systemd-user-sessions.service",
170
for _, name := range after {
171
unitOptions = append(unitOptions, &unit.UnitOption{
178
if conf.AfterStopped != "" {
179
unitOptions = append(unitOptions, &unit.UnitOption{
182
Value: conf.AfterStopped,
189
func serializeService(conf common.Conf) []*unit.UnitOption {
190
var unitOptions []*unit.UnitOption
192
// TODO(ericsnow) Support "Type" (e.g. "forking")? For now we just
193
// use the default, "simple".
195
for k, v := range conf.Env {
196
unitOptions = append(unitOptions, &unit.UnitOption{
199
Value: fmt.Sprintf(`"%s=%s"`, k, v),
203
for k, v := range conf.Limit {
204
unitOptions = append(unitOptions, &unit.UnitOption{
207
Value: strconv.Itoa(v),
211
if conf.ExecStart != "" {
212
unitOptions = append(unitOptions, &unit.UnitOption{
215
Value: conf.ExecStart,
219
// TODO(ericsnow) This should key off Conf.Restart, once added.
221
unitOptions = append(unitOptions, &unit.UnitOption{
228
if conf.Timeout > 0 {
229
unitOptions = append(unitOptions, &unit.UnitOption{
232
Value: strconv.Itoa(conf.Timeout),
236
if conf.ExecStopPost != "" {
237
unitOptions = append(unitOptions, &unit.UnitOption{
239
Name: "ExecStopPost",
240
Value: conf.ExecStopPost,
247
func serializeInstall(conf common.Conf) []*unit.UnitOption {
248
var unitOptions []*unit.UnitOption
250
unitOptions = append(unitOptions, &unit.UnitOption{
253
Value: "multi-user.target",
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))
264
return common.Conf{}, errors.Trace(err)
266
return deserializeOptions(opts, renderer)
269
func deserializeOptions(opts []*unit.UnitOption, renderer shell.Renderer) (common.Conf, error) {
272
for _, uo := range opts {
279
// Do nothing until we support it in common.Conf.
281
return conf, errors.NotSupportedf("Unit directive %q", uo.Name)
285
case uo.Name == "ExecStart":
286
conf.ExecStart = uo.Value
287
case uo.Name == "Environment":
289
conf.Env = make(map[string]string)
292
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
293
value = value[1 : len(value)-1]
295
parts := strings.SplitN(value, "=", 2)
297
return conf, errors.NotValidf("service environment value %q", uo.Value)
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)
304
for k, v := range limitMap {
306
n, err := strconv.Atoi(uo.Value)
308
return conf, errors.Trace(err)
314
case uo.Name == "TimeoutSec":
315
timeout, err := strconv.Atoi(uo.Value)
317
return conf, errors.Trace(err)
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.
327
return conf, errors.NotSupportedf("Service directive %q", uo.Name)
332
if uo.Value != "multi-user.target" {
333
return conf, errors.NotValidf("unit target %q", uo.Value)
336
return conf, errors.NotSupportedf("Install directive %q", uo.Name)
339
return conf, errors.NotSupportedf("section %q", uo.Name)
343
err := validate("<>", conf, renderer)
344
return conf, errors.Trace(err)
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 = `
352
Description=Stop all network interfaces on shutdown
353
DefaultDependencies=false
358
ExecStart=/sbin/ifdown -a -v --force
363
WantedBy=final.target
366
// CleanShutdownServicePath is the full file path where
367
// CleanShutdownService is created.
368
const CleanShutdownServicePath = "/etc/systemd/system/juju-clean-shutdown.service"