Permalink
Cannot retrieve contributors at this time
Fetching contributors…
| // Copyright 2012, 2013 Canonical Ltd. | |
| // Licensed under the AGPLv3, see LICENCE file for details. | |
| package upstart | |
| import ( | |
| "bytes" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "os/exec" | |
| "path" | |
| "regexp" | |
| "runtime" | |
| "text/template" | |
| "github.com/juju/errors" | |
| "github.com/juju/loggo" | |
| "github.com/juju/utils/shell" | |
| "github.com/juju/juju/service/common" | |
| ) | |
| var ( | |
| InitDir = "/etc/init" // the default init directory name. | |
| logger = loggo.GetLogger("juju.service.upstart") | |
| initctlPath = "/sbin/initctl" | |
| servicesRe = regexp.MustCompile("^([a-zA-Z0-9-_:]+)\\.conf$") | |
| renderer = &shell.BashRenderer{} | |
| ) | |
| // IsRunning returns whether or not upstart is the local init system. | |
| func IsRunning() (bool, error) { | |
| // On windows casting the error to exec.Error does not yield a os.PathError type | |
| // It's easyer to just return false before even trying to execute an external command | |
| // on windows at least | |
| if runtime.GOOS == "windows" { | |
| return false, nil | |
| } | |
| // TODO(ericsnow) This function should be fixed to precisely match | |
| // the equivalent shell script line in service/discovery.go. | |
| cmd := exec.Command(initctlPath, "--system", "list") | |
| _, err := cmd.CombinedOutput() | |
| if err == nil { | |
| return true, nil | |
| } | |
| if isCmdNotFoundErr(err) { | |
| return false, nil | |
| } | |
| // Note: initctl will fail if upstart is installed but not running. | |
| // The error message will be: | |
| // Name "com.ubuntu.Upstart" does not exist | |
| return false, errors.Annotatef(err, "exec %q failed", initctlPath) | |
| } | |
| // isCmdNotFoundErr returns true if the provided error indicates that the | |
| // command passed to exec.LookPath or exec.Command was not found. | |
| func isCmdNotFoundErr(err error) bool { | |
| err = errors.Cause(err) | |
| if os.IsNotExist(err) { | |
| // Executable could not be found, go 1.3 and later | |
| return true | |
| } | |
| if err == exec.ErrNotFound { | |
| return true | |
| } | |
| if execErr, ok := err.(*exec.Error); ok { | |
| // Executable could not be found, go 1.2 | |
| if os.IsNotExist(execErr.Err) || execErr.Err == exec.ErrNotFound { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| // ListServices returns the name of all installed services on the | |
| // local host. | |
| func ListServices() ([]string, error) { | |
| fis, err := ioutil.ReadDir(InitDir) | |
| if err != nil { | |
| return nil, errors.Trace(err) | |
| } | |
| var services []string | |
| for _, fi := range fis { | |
| if groups := servicesRe.FindStringSubmatch(fi.Name()); len(groups) > 0 { | |
| services = append(services, groups[1]) | |
| } | |
| } | |
| return services, nil | |
| } | |
| // ListCommand returns a command that will list the services on a host. | |
| func ListCommand() string { | |
| // TODO(ericsnow) Do "ls /etc/init/*.conf" instead? | |
| return `sudo initctl list | awk '{print $1}' | sort | uniq` | |
| } | |
| var startedRE = regexp.MustCompile(`^.* start/running(?:, process (\d+))?\n$`) | |
| // Service provides visibility into and control over an upstart service. | |
| type Service struct { | |
| common.Service | |
| } | |
| func NewService(name string, conf common.Conf) *Service { | |
| return &Service{ | |
| Service: common.Service{ | |
| Name: name, | |
| Conf: conf, | |
| }, | |
| } | |
| } | |
| // Name implements service.Service. | |
| func (s Service) Name() string { | |
| return s.Service.Name | |
| } | |
| // Conf implements service.Service. | |
| func (s Service) Conf() common.Conf { | |
| return s.Service.Conf | |
| } | |
| // confPath returns the path to the service's configuration file. | |
| func (s *Service) confPath() string { | |
| return path.Join(InitDir, s.Service.Name+".conf") | |
| } | |
| // Validate returns an error if the service is not adequately defined. | |
| func (s *Service) Validate() error { | |
| if err := s.Service.Validate(renderer); err != nil { | |
| return errors.Trace(err) | |
| } | |
| if s.Service.Conf.Transient { | |
| if len(s.Service.Conf.Env) > 0 { | |
| return errors.NotSupportedf("Conf.Env (when transient)") | |
| } | |
| if len(s.Service.Conf.Limit) > 0 { | |
| return errors.NotSupportedf("Conf.Limit (when transient)") | |
| } | |
| if s.Service.Conf.Logfile != "" { | |
| return errors.NotSupportedf("Conf.Logfile (when transient)") | |
| } | |
| if s.Service.Conf.ExtraScript != "" { | |
| return errors.NotSupportedf("Conf.ExtraScript (when transient)") | |
| } | |
| } else { | |
| if s.Service.Conf.AfterStopped != "" { | |
| return errors.NotSupportedf("Conf.AfterStopped (when not transient)") | |
| } | |
| if s.Service.Conf.ExecStopPost != "" { | |
| return errors.NotSupportedf("Conf.ExecStopPost (when not transient)") | |
| } | |
| } | |
| return nil | |
| } | |
| // render returns the upstart configuration for the application as a slice of bytes. | |
| func (s *Service) render() ([]byte, error) { | |
| if err := s.Validate(); err != nil { | |
| return nil, err | |
| } | |
| conf := s.Conf() | |
| if conf.Transient { | |
| conf.ExecStopPost = "rm " + s.confPath() | |
| } | |
| return Serialize(s.Name(), conf) | |
| } | |
| // Installed returns whether the service configuration exists in the | |
| // init directory. | |
| func (s *Service) Installed() (bool, error) { | |
| _, err := os.Stat(s.confPath()) | |
| if os.IsNotExist(err) { | |
| return false, nil | |
| } | |
| if err != nil { | |
| return false, errors.Trace(err) | |
| } | |
| return true, nil | |
| } | |
| // Exists returns whether the service configuration exists in the | |
| // init directory with the same content that this Service would have | |
| // if installed. | |
| func (s *Service) Exists() (bool, error) { | |
| // In any error case, we just say it doesn't exist with this configuration. | |
| // Subsequent calls into the Service will give the caller more useful errors. | |
| _, same, _, err := s.existsAndSame() | |
| if err != nil { | |
| return false, errors.Trace(err) | |
| } | |
| return same, nil | |
| } | |
| func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) { | |
| expected, err := s.render() | |
| if err != nil { | |
| return false, false, nil, errors.Trace(err) | |
| } | |
| current, err := ioutil.ReadFile(s.confPath()) | |
| if err != nil { | |
| if os.IsNotExist(err) { | |
| // no existing config | |
| return false, false, expected, nil | |
| } | |
| return false, false, nil, errors.Trace(err) | |
| } | |
| return true, bytes.Equal(current, expected), expected, nil | |
| } | |
| // Running returns true if the Service appears to be running. | |
| func (s *Service) Running() (bool, error) { | |
| cmd := exec.Command("status", "--system", s.Service.Name) | |
| out, err := cmd.CombinedOutput() | |
| logger.Tracef("Running \"status --system %s\": %q", s.Service.Name, out) | |
| if err == nil { | |
| return startedRE.Match(out), nil | |
| } | |
| if err.Error() != "exit status 1" { | |
| return false, errors.Trace(err) | |
| } | |
| return false, nil | |
| } | |
| // Start starts the service. | |
| func (s *Service) Start() error { | |
| running, err := s.Running() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if running { | |
| return nil | |
| } | |
| err = runCommand("start", "--system", s.Service.Name) | |
| if err != nil { | |
| // Double check to see if we were started before our command ran. | |
| // If this fails then we simply trust it's okay. | |
| if running, _ := s.Running(); running { | |
| return nil | |
| } | |
| } | |
| return err | |
| } | |
| func runCommand(args ...string) error { | |
| out, err := exec.Command(args[0], args[1:]...).CombinedOutput() | |
| if err == nil { | |
| return nil | |
| } | |
| out = bytes.TrimSpace(out) | |
| if len(out) > 0 { | |
| return fmt.Errorf("exec %q: %v (%s)", args, err, out) | |
| } | |
| return fmt.Errorf("exec %q: %v", args, err) | |
| } | |
| // Stop stops the service. | |
| func (s *Service) Stop() error { | |
| running, err := s.Running() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if !running { | |
| return nil | |
| } | |
| return runCommand("stop", "--system", s.Service.Name) | |
| } | |
| // Restart restarts the service. | |
| func (s *Service) Restart() error { | |
| return runCommand("restart", s.Service.Name) | |
| } | |
| // Remove deletes the service configuration from the init directory. | |
| func (s *Service) Remove() error { | |
| installed, err := s.Installed() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if !installed { | |
| return nil | |
| } | |
| return os.Remove(s.confPath()) | |
| } | |
| // Install installs and starts the service. | |
| func (s *Service) Install() error { | |
| exists, same, conf, err := s.existsAndSame() | |
| if err != nil { | |
| return errors.Trace(err) | |
| } | |
| if same { | |
| return nil | |
| } | |
| if exists { | |
| if err := s.Stop(); err != nil { | |
| return errors.Annotate(err, "upstart: could not stop installed service") | |
| } | |
| if err := s.Remove(); err != nil { | |
| return errors.Annotate(err, "upstart: could not remove installed service") | |
| } | |
| } | |
| if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil { | |
| return errors.Trace(err) | |
| } | |
| return nil | |
| } | |
| // InstallCommands returns shell commands to install the service. | |
| func (s *Service) InstallCommands() ([]string, error) { | |
| conf, err := s.render() | |
| if err != nil { | |
| return nil, err | |
| } | |
| cmd := fmt.Sprintf("cat > %s << 'EOF'\n%sEOF\n", s.confPath(), conf) | |
| return []string{cmd}, nil | |
| } | |
| // StartCommands returns shell commands to start the service. | |
| func (s *Service) StartCommands() ([]string, error) { | |
| // TODO(ericsnow) Add clarification about why transient services are not started. | |
| if s.Service.Conf.Transient { | |
| return nil, nil | |
| } | |
| return []string{"start " + s.Service.Name}, nil | |
| } | |
| // Serialize renders the conf as raw bytes. | |
| func Serialize(name string, conf common.Conf) ([]byte, error) { | |
| var buf bytes.Buffer | |
| if conf.Transient { | |
| if err := transientConfT.Execute(&buf, conf); err != nil { | |
| return nil, err | |
| } | |
| } else { | |
| if err := confT.Execute(&buf, conf); err != nil { | |
| return nil, err | |
| } | |
| } | |
| return buf.Bytes(), nil | |
| } | |
| // TODO(ericsnow) Use a different solution than templates? | |
| // BUG: %q quoting does not necessarily match libnih quoting rules | |
| // (as used by upstart); this may become an issue in the future. | |
| var confT = template.Must(template.New("").Parse(` | |
| description "{{.Desc}}" | |
| author "Juju Team <juju@lists.ubuntu.com>" | |
| start on runlevel [2345] | |
| stop on runlevel [!2345] | |
| respawn | |
| normal exit 0 | |
| {{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}} | |
| {{end}} | |
| {{range $k, $v := .Limit}}limit {{$k}} {{$v}} {{$v}} | |
| {{end}} | |
| script | |
| {{if .ExtraScript}}{{.ExtraScript}}{{end}} | |
| {{if .Logfile}} | |
| # Ensure log files are properly protected | |
| touch {{.Logfile}} | |
| chown syslog:syslog {{.Logfile}} | |
| chmod 0600 {{.Logfile}} | |
| {{end}} | |
| exec {{.ExecStart}}{{if .Logfile}} >> {{.Logfile}} 2>&1{{end}} | |
| end script | |
| `[1:])) | |
| var transientConfT = template.Must(template.New("").Parse(` | |
| description "{{.Desc}}" | |
| author "Juju Team <juju@lists.ubuntu.com>" | |
| start on stopped {{.AfterStopped}} | |
| script | |
| {{.ExecStart}} | |
| end script | |
| {{if .ExecStopPost}} | |
| post-stop script | |
| {{.ExecStopPost}} | |
| end script | |
| {{end}} | |
| `[1:])) | |
| // CleanShutdownJob is added to machines to ensure DHCP-assigned IP | |
| // addresses are released on shutdown, reboot, or halt. See bug | |
| // http://pad.lv/1348663 for more info. | |
| const CleanShutdownJob = ` | |
| author "Juju Team <juju@lists.ubuntu.com>" | |
| description "Stop all network interfaces on shutdown" | |
| start on runlevel [016] | |
| task | |
| console output | |
| exec /sbin/ifdown -a -v --force | |
| ` | |
| // CleanShutdownJobPath is the full file path where CleanShutdownJob | |
| // is created. | |
| const CleanShutdownJobPath = "/etc/init/juju-clean-shutdown.conf" |