github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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  	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 service 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"