1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
17
"github.com/juju/errors"
18
"github.com/juju/loggo"
19
"github.com/juju/utils/shell"
21
"github.com/juju/juju/service/common"
25
InitDir = "/etc/init" // the default init directory name.
27
logger = loggo.GetLogger("juju.service.upstart")
28
initctlPath = "/sbin/initctl"
29
servicesRe = regexp.MustCompile("^([a-zA-Z0-9-_:]+)\\.conf$")
30
renderer = &shell.BashRenderer{}
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" {
42
// TODO(ericsnow) This function should be fixed to precisely match
43
// the equivalent shell script line in service/discovery.go.
45
cmd := exec.Command(initctlPath, "--system", "list")
46
_, err := cmd.CombinedOutput()
51
if isCmdNotFoundErr(err) {
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)
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
68
if err == exec.ErrNotFound {
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 {
80
// ListServices returns the name of all installed services on the
82
func ListServices() ([]string, error) {
83
fis, err := ioutil.ReadDir(InitDir)
85
return nil, errors.Trace(err)
89
for _, fi := range fis {
90
if groups := servicesRe.FindStringSubmatch(fi.Name()); len(groups) > 0 {
91
services = append(services, groups[1])
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`
103
var startedRE = regexp.MustCompile(`^.* start/running(?:, process (\d+))?\n$`)
105
// Service provides visibility into and control over an upstart service.
106
type Service struct {
110
func NewService(name string, conf common.Conf) *Service {
112
Service: common.Service{
119
// Name implements service.Service.
120
func (s Service) Name() string {
121
return s.Service.Name
124
// Conf implements service.Service.
125
func (s Service) Conf() common.Conf {
126
return s.Service.Conf
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")
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)
140
if s.Service.Conf.Transient {
141
if len(s.Service.Conf.Env) > 0 {
142
return errors.NotSupportedf("Conf.Env (when transient)")
144
if len(s.Service.Conf.Limit) > 0 {
145
return errors.NotSupportedf("Conf.Limit (when transient)")
147
if s.Service.Conf.Logfile != "" {
148
return errors.NotSupportedf("Conf.Logfile (when transient)")
150
if s.Service.Conf.ExtraScript != "" {
151
return errors.NotSupportedf("Conf.ExtraScript (when transient)")
154
if s.Service.Conf.AfterStopped != "" {
155
return errors.NotSupportedf("Conf.AfterStopped (when not transient)")
157
if s.Service.Conf.ExecStopPost != "" {
158
return errors.NotSupportedf("Conf.ExecStopPost (when not transient)")
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 {
172
conf.ExecStopPost = "rm " + s.confPath()
174
return Serialize(s.Name(), conf)
177
// Installed returns whether the service configuration exists in the
179
func (s *Service) Installed() (bool, error) {
180
_, err := os.Stat(s.confPath())
181
if os.IsNotExist(err) {
185
return false, errors.Trace(err)
190
// Exists returns whether the service configuration exists in the
191
// init directory with the same content that this Service would have
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()
198
return false, errors.Trace(err)
203
func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) {
204
expected, err := s.render()
206
return false, false, nil, errors.Trace(err)
208
current, err := ioutil.ReadFile(s.confPath())
210
if os.IsNotExist(err) {
211
// no existing config
212
return false, false, expected, nil
214
return false, false, nil, errors.Trace(err)
216
return true, bytes.Equal(current, expected), expected, nil
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)
225
return startedRE.Match(out), nil
227
if err.Error() != "exit status 1" {
228
return false, errors.Trace(err)
233
// Start starts the service.
234
func (s *Service) Start() error {
235
running, err := s.Running()
237
return errors.Trace(err)
242
err = runCommand("start", "--system", s.Service.Name)
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 {
253
func runCommand(args ...string) error {
254
out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
258
out = bytes.TrimSpace(out)
260
return fmt.Errorf("exec %q: %v (%s)", args, err, out)
262
return fmt.Errorf("exec %q: %v", args, err)
265
// Stop stops the service.
266
func (s *Service) Stop() error {
267
running, err := s.Running()
269
return errors.Trace(err)
274
return runCommand("stop", "--system", s.Service.Name)
277
// Restart restarts the service.
278
func (s *Service) Restart() error {
279
return runCommand("restart", s.Service.Name)
282
// Remove deletes the service configuration from the init directory.
283
func (s *Service) Remove() error {
284
installed, err := s.Installed()
286
return errors.Trace(err)
291
return os.Remove(s.confPath())
294
// Install installs and starts the service.
295
func (s *Service) Install() error {
296
exists, same, conf, err := s.existsAndSame()
298
return errors.Trace(err)
304
if err := s.Stop(); err != nil {
305
return errors.Annotate(err, "upstart: could not stop installed service")
307
if err := s.Remove(); err != nil {
308
return errors.Annotate(err, "upstart: could not remove installed service")
311
if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil {
312
return errors.Trace(err)
318
// InstallCommands returns shell commands to install the service.
319
func (s *Service) InstallCommands() ([]string, error) {
320
conf, err := s.render()
324
cmd := fmt.Sprintf("cat > %s << 'EOF'\n%sEOF\n", s.confPath(), conf)
325
return []string{cmd}, nil
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 {
334
return []string{"start " + s.Service.Name}, nil
337
// Serialize renders the conf as raw bytes.
338
func Serialize(name string, conf common.Conf) ([]byte, error) {
341
if err := transientConfT.Execute(&buf, conf); err != nil {
345
if err := confT.Execute(&buf, conf); err != nil {
349
return buf.Bytes(), nil
352
// TODO(ericsnow) Use a different solution than templates?
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]
363
{{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}}
365
{{range $k, $v := .Limit}}limit {{$k}} {{$v}} {{$v}}
368
{{if .ExtraScript}}{{.ExtraScript}}{{end}}
370
# Ensure log files are properly protected
372
chown syslog:syslog {{.Logfile}}
373
chmod 0600 {{.Logfile}}
375
exec {{.ExecStart}}{{if .Logfile}} >> {{.Logfile}} 2>&1{{end}}
379
var transientConfT = template.Must(template.New("").Parse(`
380
description "{{.Desc}}"
381
author "Juju Team <juju@lists.ubuntu.com>"
382
start on stopped {{.AfterStopped}}
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]
404
exec /sbin/ifdown -a -v --force
407
// CleanShutdownJobPath is the full file path where CleanShutdownJob
409
const CleanShutdownJobPath = "/etc/init/juju-clean-shutdown.conf"