github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/service/upstart/upstart.go (about)

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