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