~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/service/upstart/upstart.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 2012, 2013 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
package upstart
 
5
 
 
6
import (
 
7
        "bytes"
 
8
        "fmt"
 
9
        "io/ioutil"
 
10
        "os"
 
11
        "os/exec"
 
12
        "path"
 
13
        "regexp"
 
14
        "runtime"
 
15
        "text/template"
 
16
 
 
17
        "github.com/juju/errors"
 
18
        "github.com/juju/loggo"
 
19
        "github.com/juju/utils/shell"
 
20
 
 
21
        "github.com/juju/juju/service/common"
 
22
)
 
23
 
 
24
var (
 
25
        InitDir = "/etc/init" // the default init directory name.
 
26
 
 
27
        logger      = loggo.GetLogger("juju.service.upstart")
 
28
        initctlPath = "/sbin/initctl"
 
29
        servicesRe  = regexp.MustCompile("^([a-zA-Z0-9-_:]+)\\.conf$")
 
30
        renderer    = &shell.BashRenderer{}
 
31
)
 
32
 
 
33
// IsRunning returns whether or not upstart is the local init system.
 
34
func IsRunning() (bool, error) {
 
35
        // On windows casting the error to exec.Error does not yield a os.PathError type
 
36
        // It's easyer to just return false before even trying to execute an external command
 
37
        // on windows at least
 
38
        if runtime.GOOS == "windows" {
 
39
                return false, nil
 
40
        }
 
41
 
 
42
        // TODO(ericsnow) This function should be fixed to precisely match
 
43
        // the equivalent shell script line in service/discovery.go.
 
44
 
 
45
        cmd := exec.Command(initctlPath, "--system", "list")
 
46
        _, err := cmd.CombinedOutput()
 
47
        if err == nil {
 
48
                return true, nil
 
49
        }
 
50
 
 
51
        if isCmdNotFoundErr(err) {
 
52
                return false, nil
 
53
        }
 
54
        // Note: initctl will fail if upstart is installed but not running.
 
55
        // The error message will be:
 
56
        //   Name "com.ubuntu.Upstart" does not exist
 
57
        return false, errors.Annotatef(err, "exec %q failed", initctlPath)
 
58
}
 
59
 
 
60
// isCmdNotFoundErr returns true if the provided error indicates that the
 
61
// command passed to exec.LookPath or exec.Command was not found.
 
62
func isCmdNotFoundErr(err error) bool {
 
63
        err = errors.Cause(err)
 
64
        if os.IsNotExist(err) {
 
65
                // Executable could not be found, go 1.3 and later
 
66
                return true
 
67
        }
 
68
        if err == exec.ErrNotFound {
 
69
                return true
 
70
        }
 
71
        if execErr, ok := err.(*exec.Error); ok {
 
72
                // Executable could not be found, go 1.2
 
73
                if os.IsNotExist(execErr.Err) || execErr.Err == exec.ErrNotFound {
 
74
                        return true
 
75
                }
 
76
        }
 
77
        return false
 
78
}
 
79
 
 
80
// ListServices returns the name of all installed services on the
 
81
// local host.
 
82
func ListServices() ([]string, error) {
 
83
        fis, err := ioutil.ReadDir(InitDir)
 
84
        if err != nil {
 
85
                return nil, errors.Trace(err)
 
86
        }
 
87
 
 
88
        var services []string
 
89
        for _, fi := range fis {
 
90
                if groups := servicesRe.FindStringSubmatch(fi.Name()); len(groups) > 0 {
 
91
                        services = append(services, groups[1])
 
92
                }
 
93
        }
 
94
        return services, nil
 
95
}
 
96
 
 
97
// ListCommand returns a command that will list the services on a host.
 
98
func ListCommand() string {
 
99
        // TODO(ericsnow) Do "ls /etc/init/*.conf" instead?
 
100
        return `sudo initctl list | awk '{print $1}' | sort | uniq`
 
101
}
 
102
 
 
103
var startedRE = regexp.MustCompile(`^.* start/running(?:, process (\d+))?\n$`)
 
104
 
 
105
// Service provides visibility into and control over an upstart service.
 
106
type Service struct {
 
107
        common.Service
 
108
}
 
109
 
 
110
func NewService(name string, conf common.Conf) *Service {
 
111
        return &Service{
 
112
                Service: common.Service{
 
113
                        Name: name,
 
114
                        Conf: conf,
 
115
                },
 
116
        }
 
117
}
 
118
 
 
119
// Name implements service.Service.
 
120
func (s Service) Name() string {
 
121
        return s.Service.Name
 
122
}
 
123
 
 
124
// Conf implements service.Service.
 
125
func (s Service) Conf() common.Conf {
 
126
        return s.Service.Conf
 
127
}
 
128
 
 
129
// confPath returns the path to the service's configuration file.
 
130
func (s *Service) confPath() string {
 
131
        return path.Join(InitDir, s.Service.Name+".conf")
 
132
}
 
133
 
 
134
// Validate returns an error if the service is not adequately defined.
 
135
func (s *Service) Validate() error {
 
136
        if err := s.Service.Validate(renderer); err != nil {
 
137
                return errors.Trace(err)
 
138
        }
 
139
 
 
140
        if s.Service.Conf.Transient {
 
141
                if len(s.Service.Conf.Env) > 0 {
 
142
                        return errors.NotSupportedf("Conf.Env (when transient)")
 
143
                }
 
144
                if len(s.Service.Conf.Limit) > 0 {
 
145
                        return errors.NotSupportedf("Conf.Limit (when transient)")
 
146
                }
 
147
                if s.Service.Conf.Logfile != "" {
 
148
                        return errors.NotSupportedf("Conf.Logfile (when transient)")
 
149
                }
 
150
                if s.Service.Conf.ExtraScript != "" {
 
151
                        return errors.NotSupportedf("Conf.ExtraScript (when transient)")
 
152
                }
 
153
        } else {
 
154
                if s.Service.Conf.AfterStopped != "" {
 
155
                        return errors.NotSupportedf("Conf.AfterStopped (when not transient)")
 
156
                }
 
157
                if s.Service.Conf.ExecStopPost != "" {
 
158
                        return errors.NotSupportedf("Conf.ExecStopPost (when not transient)")
 
159
                }
 
160
        }
 
161
 
 
162
        return nil
 
163
}
 
164
 
 
165
// render returns the upstart configuration for the application as a slice of bytes.
 
166
func (s *Service) render() ([]byte, error) {
 
167
        if err := s.Validate(); err != nil {
 
168
                return nil, err
 
169
        }
 
170
        conf := s.Conf()
 
171
        if conf.Transient {
 
172
                conf.ExecStopPost = "rm " + s.confPath()
 
173
        }
 
174
        return Serialize(s.Name(), conf)
 
175
}
 
176
 
 
177
// Installed returns whether the service configuration exists in the
 
178
// init directory.
 
179
func (s *Service) Installed() (bool, error) {
 
180
        _, err := os.Stat(s.confPath())
 
181
        if os.IsNotExist(err) {
 
182
                return false, nil
 
183
        }
 
184
        if err != nil {
 
185
                return false, errors.Trace(err)
 
186
        }
 
187
        return true, nil
 
188
}
 
189
 
 
190
// Exists returns whether the service configuration exists in the
 
191
// init directory with the same content that this Service would have
 
192
// if installed.
 
193
func (s *Service) Exists() (bool, error) {
 
194
        // In any error case, we just say it doesn't exist with this configuration.
 
195
        // Subsequent calls into the Service will give the caller more useful errors.
 
196
        _, same, _, err := s.existsAndSame()
 
197
        if err != nil {
 
198
                return false, errors.Trace(err)
 
199
        }
 
200
        return same, nil
 
201
}
 
202
 
 
203
func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) {
 
204
        expected, err := s.render()
 
205
        if err != nil {
 
206
                return false, false, nil, errors.Trace(err)
 
207
        }
 
208
        current, err := ioutil.ReadFile(s.confPath())
 
209
        if err != nil {
 
210
                if os.IsNotExist(err) {
 
211
                        // no existing config
 
212
                        return false, false, expected, nil
 
213
                }
 
214
                return false, false, nil, errors.Trace(err)
 
215
        }
 
216
        return true, bytes.Equal(current, expected), expected, nil
 
217
}
 
218
 
 
219
// Running returns true if the Service appears to be running.
 
220
func (s *Service) Running() (bool, error) {
 
221
        cmd := exec.Command("status", "--system", s.Service.Name)
 
222
        out, err := cmd.CombinedOutput()
 
223
        logger.Tracef("Running \"status --system %s\": %q", s.Service.Name, out)
 
224
        if err == nil {
 
225
                return startedRE.Match(out), nil
 
226
        }
 
227
        if err.Error() != "exit status 1" {
 
228
                return false, errors.Trace(err)
 
229
        }
 
230
        return false, nil
 
231
}
 
232
 
 
233
// Start starts the service.
 
234
func (s *Service) Start() error {
 
235
        running, err := s.Running()
 
236
        if err != nil {
 
237
                return errors.Trace(err)
 
238
        }
 
239
        if running {
 
240
                return nil
 
241
        }
 
242
        err = runCommand("start", "--system", s.Service.Name)
 
243
        if err != nil {
 
244
                // Double check to see if we were started before our command ran.
 
245
                // If this fails then we simply trust it's okay.
 
246
                if running, _ := s.Running(); running {
 
247
                        return nil
 
248
                }
 
249
        }
 
250
        return err
 
251
}
 
252
 
 
253
func runCommand(args ...string) error {
 
254
        out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
 
255
        if err == nil {
 
256
                return nil
 
257
        }
 
258
        out = bytes.TrimSpace(out)
 
259
        if len(out) > 0 {
 
260
                return fmt.Errorf("exec %q: %v (%s)", args, err, out)
 
261
        }
 
262
        return fmt.Errorf("exec %q: %v", args, err)
 
263
}
 
264
 
 
265
// Stop stops the service.
 
266
func (s *Service) Stop() error {
 
267
        running, err := s.Running()
 
268
        if err != nil {
 
269
                return errors.Trace(err)
 
270
        }
 
271
        if !running {
 
272
                return nil
 
273
        }
 
274
        return runCommand("stop", "--system", s.Service.Name)
 
275
}
 
276
 
 
277
// Restart restarts the service.
 
278
func (s *Service) Restart() error {
 
279
        return runCommand("restart", s.Service.Name)
 
280
}
 
281
 
 
282
// Remove deletes the service configuration from the init directory.
 
283
func (s *Service) Remove() error {
 
284
        installed, err := s.Installed()
 
285
        if err != nil {
 
286
                return errors.Trace(err)
 
287
        }
 
288
        if !installed {
 
289
                return nil
 
290
        }
 
291
        return os.Remove(s.confPath())
 
292
}
 
293
 
 
294
// Install installs and starts the service.
 
295
func (s *Service) Install() error {
 
296
        exists, same, conf, err := s.existsAndSame()
 
297
        if err != nil {
 
298
                return errors.Trace(err)
 
299
        }
 
300
        if same {
 
301
                return nil
 
302
        }
 
303
        if exists {
 
304
                if err := s.Stop(); err != nil {
 
305
                        return errors.Annotate(err, "upstart: could not stop installed service")
 
306
                }
 
307
                if err := s.Remove(); err != nil {
 
308
                        return errors.Annotate(err, "upstart: could not remove installed service")
 
309
                }
 
310
        }
 
311
        if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil {
 
312
                return errors.Trace(err)
 
313
        }
 
314
 
 
315
        return nil
 
316
}
 
317
 
 
318
// InstallCommands returns shell commands to install the service.
 
319
func (s *Service) InstallCommands() ([]string, error) {
 
320
        conf, err := s.render()
 
321
        if err != nil {
 
322
                return nil, err
 
323
        }
 
324
        cmd := fmt.Sprintf("cat > %s << 'EOF'\n%sEOF\n", s.confPath(), conf)
 
325
        return []string{cmd}, nil
 
326
}
 
327
 
 
328
// StartCommands returns shell commands to start the service.
 
329
func (s *Service) StartCommands() ([]string, error) {
 
330
        // TODO(ericsnow) Add clarification about why transient services are not started.
 
331
        if s.Service.Conf.Transient {
 
332
                return nil, nil
 
333
        }
 
334
        return []string{"start " + s.Service.Name}, nil
 
335
}
 
336
 
 
337
// Serialize renders the conf as raw bytes.
 
338
func Serialize(name string, conf common.Conf) ([]byte, error) {
 
339
        var buf bytes.Buffer
 
340
        if conf.Transient {
 
341
                if err := transientConfT.Execute(&buf, conf); err != nil {
 
342
                        return nil, err
 
343
                }
 
344
        } else {
 
345
                if err := confT.Execute(&buf, conf); err != nil {
 
346
                        return nil, err
 
347
                }
 
348
        }
 
349
        return buf.Bytes(), nil
 
350
}
 
351
 
 
352
// TODO(ericsnow) Use a different solution than templates?
 
353
 
 
354
// BUG: %q quoting does not necessarily match libnih quoting rules
 
355
// (as used by upstart); this may become an issue in the future.
 
356
var confT = template.Must(template.New("").Parse(`
 
357
description "{{.Desc}}"
 
358
author "Juju Team <juju@lists.ubuntu.com>"
 
359
start on runlevel [2345]
 
360
stop on runlevel [!2345]
 
361
respawn
 
362
normal exit 0
 
363
{{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}}
 
364
{{end}}
 
365
{{range $k, $v := .Limit}}limit {{$k}} {{$v}} {{$v}}
 
366
{{end}}
 
367
script
 
368
{{if .ExtraScript}}{{.ExtraScript}}{{end}}
 
369
{{if .Logfile}}
 
370
  # Ensure log files are properly protected
 
371
  touch {{.Logfile}}
 
372
  chown syslog:syslog {{.Logfile}}
 
373
  chmod 0600 {{.Logfile}}
 
374
{{end}}
 
375
  exec {{.ExecStart}}{{if .Logfile}} >> {{.Logfile}} 2>&1{{end}}
 
376
end script
 
377
`[1:]))
 
378
 
 
379
var transientConfT = template.Must(template.New("").Parse(`
 
380
description "{{.Desc}}"
 
381
author "Juju Team <juju@lists.ubuntu.com>"
 
382
start on stopped {{.AfterStopped}}
 
383
 
 
384
script
 
385
  {{.ExecStart}}
 
386
end script
 
387
{{if .ExecStopPost}}
 
388
post-stop script
 
389
  {{.ExecStopPost}}
 
390
end script
 
391
{{end}}
 
392
`[1:]))
 
393
 
 
394
// CleanShutdownJob is added to machines to ensure DHCP-assigned IP
 
395
// addresses are released on shutdown, reboot, or halt. See bug
 
396
// http://pad.lv/1348663 for more info.
 
397
const CleanShutdownJob = `
 
398
author "Juju Team <juju@lists.ubuntu.com>"
 
399
description "Stop all network interfaces on shutdown"
 
400
start on runlevel [016]
 
401
task
 
402
console output
 
403
 
 
404
exec /sbin/ifdown -a -v --force
 
405
`
 
406
 
 
407
// CleanShutdownJobPath is the full file path where CleanShutdownJob
 
408
// is created.
 
409
const CleanShutdownJobPath = "/etc/init/juju-clean-shutdown.conf"