github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"text/template"
    15  	"time"
    16  
    17  	"github.com/juju/errors"
    18  	"github.com/juju/utils"
    19  
    20  	"github.com/juju/juju/service/common"
    21  )
    22  
    23  var startedRE = regexp.MustCompile(`^.* start/running, process (\d+)\n$`)
    24  
    25  // InitDir holds the default init directory name.
    26  var InitDir = "/etc/init"
    27  
    28  var InstallStartRetryAttempts = utils.AttemptStrategy{
    29  	Total: 1 * time.Second,
    30  	Delay: 250 * time.Millisecond,
    31  }
    32  
    33  // Service provides visibility into and control over an upstart service.
    34  type Service struct {
    35  	Name string
    36  	Conf common.Conf
    37  }
    38  
    39  func NewService(name string, conf common.Conf) *Service {
    40  	if conf.InitDir == "" {
    41  		conf.InitDir = InitDir
    42  	}
    43  	return &Service{Name: name, Conf: conf}
    44  }
    45  
    46  // confPath returns the path to the service's configuration file.
    47  func (s *Service) confPath() string {
    48  	return path.Join(s.Conf.InitDir, s.Name+".conf")
    49  }
    50  
    51  func (s *Service) UpdateConfig(conf common.Conf) {
    52  	s.Conf = conf
    53  }
    54  
    55  // validate returns an error if the service is not adequately defined.
    56  func (s *Service) validate() error {
    57  	if s.Name == "" {
    58  		return errors.New("missing Name")
    59  	}
    60  	if s.Conf.InitDir == "" {
    61  		return errors.New("missing InitDir")
    62  	}
    63  	if s.Conf.Desc == "" {
    64  		return errors.New("missing Desc")
    65  	}
    66  	if s.Conf.Cmd == "" {
    67  		return errors.New("missing Cmd")
    68  	}
    69  	return nil
    70  }
    71  
    72  // render returns the upstart configuration for the service as a slice of bytes.
    73  func (s *Service) render() ([]byte, error) {
    74  	if err := s.validate(); err != nil {
    75  		return nil, err
    76  	}
    77  	var buf bytes.Buffer
    78  	if err := confT.Execute(&buf, s.Conf); err != nil {
    79  		return nil, err
    80  	}
    81  	return buf.Bytes(), nil
    82  }
    83  
    84  // Installed returns whether the service configuration exists in the
    85  // init directory.
    86  func (s *Service) Installed() bool {
    87  	_, err := os.Stat(s.confPath())
    88  	return err == nil
    89  }
    90  
    91  // Exists returns whether the service configuration exists in the
    92  // init directory with the same content that this Service would have
    93  // if installed.
    94  func (s *Service) Exists() bool {
    95  	// In any error case, we just say it doesn't exist with this configuration.
    96  	// Subsequent calls into the Service will give the caller more useful errors.
    97  	_, same, _, err := s.existsAndSame()
    98  	if err != nil {
    99  		return false
   100  	}
   101  	return same
   102  }
   103  
   104  func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) {
   105  	expected, err := s.render()
   106  	if err != nil {
   107  		return false, false, nil, errors.Trace(err)
   108  	}
   109  	current, err := ioutil.ReadFile(s.confPath())
   110  	if err != nil {
   111  		if os.IsNotExist(err) {
   112  			// no existing config
   113  			return false, false, expected, nil
   114  		}
   115  		return false, false, nil, errors.Trace(err)
   116  	}
   117  	return true, bytes.Equal(current, expected), expected, nil
   118  }
   119  
   120  // Running returns true if the Service appears to be running.
   121  func (s *Service) Running() bool {
   122  	cmd := exec.Command("status", "--system", s.Name)
   123  	out, err := cmd.CombinedOutput()
   124  	if err != nil {
   125  		return false
   126  	}
   127  	return startedRE.Match(out)
   128  }
   129  
   130  // Start starts the service.
   131  func (s *Service) Start() error {
   132  	if s.Running() {
   133  		return nil
   134  	}
   135  	err := runCommand("start", "--system", s.Name)
   136  	if err != nil {
   137  		// Double check to see if we were started before our command ran.
   138  		if s.Running() {
   139  			return nil
   140  		}
   141  	}
   142  	return err
   143  }
   144  
   145  func runCommand(args ...string) error {
   146  	out, err := exec.Command(args[0], args[1:]...).CombinedOutput()
   147  	if err == nil {
   148  		return nil
   149  	}
   150  	out = bytes.TrimSpace(out)
   151  	if len(out) > 0 {
   152  		return fmt.Errorf("exec %q: %v (%s)", args, err, out)
   153  	}
   154  	return fmt.Errorf("exec %q: %v", args, err)
   155  }
   156  
   157  // Stop stops the service.
   158  func (s *Service) Stop() error {
   159  	if !s.Running() {
   160  		return nil
   161  	}
   162  	return runCommand("stop", "--system", s.Name)
   163  }
   164  
   165  // StopAndRemove stops the service and then deletes the service
   166  // configuration from the init directory.
   167  func (s *Service) StopAndRemove() error {
   168  	if !s.Installed() {
   169  		return nil
   170  	}
   171  	if err := s.Stop(); err != nil {
   172  		return err
   173  	}
   174  	return os.Remove(s.confPath())
   175  }
   176  
   177  // Remove deletes the service configuration from the init directory.
   178  func (s *Service) Remove() error {
   179  	if !s.Installed() {
   180  		return nil
   181  	}
   182  	return os.Remove(s.confPath())
   183  }
   184  
   185  // Install installs and starts the service.
   186  func (s *Service) Install() error {
   187  	exists, same, conf, err := s.existsAndSame()
   188  	if err != nil {
   189  		return errors.Trace(err)
   190  	}
   191  	if same {
   192  		return nil
   193  	}
   194  	if exists {
   195  		if err := s.StopAndRemove(); err != nil {
   196  			return errors.Annotate(err, "upstart: could not remove installed service")
   197  		}
   198  
   199  	}
   200  	if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil {
   201  		return errors.Trace(err)
   202  	}
   203  
   204  	// On slower disks, upstart may take a short time to realise
   205  	// that there is a service there.
   206  	for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); {
   207  		if err = s.Start(); err == nil {
   208  			break
   209  		}
   210  	}
   211  	return err
   212  }
   213  
   214  // InstallCommands returns shell commands to install and start the service.
   215  func (s *Service) InstallCommands() ([]string, error) {
   216  	conf, err := s.render()
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	return []string{
   221  		fmt.Sprintf("cat >> %s << 'EOF'\n%sEOF\n", s.confPath(), conf),
   222  		"start " + s.Name,
   223  	}, nil
   224  }
   225  
   226  // BUG: %q quoting does not necessarily match libnih quoting rules
   227  // (as used by upstart); this may become an issue in the future.
   228  var confT = template.Must(template.New("").Parse(`
   229  description "{{.Desc}}"
   230  author "Juju Team <juju@lists.ubuntu.com>"
   231  start on runlevel [2345]
   232  stop on runlevel [!2345]
   233  respawn
   234  normal exit 0
   235  {{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}}
   236  {{end}}
   237  {{range $k, $v := .Limit}}limit {{$k}} {{$v}}
   238  {{end}}
   239  script
   240  {{if .ExtraScript}}{{.ExtraScript}}{{end}}
   241  {{if .Out}}
   242    # Ensure log files are properly protected
   243    touch {{.Out}}
   244    chown syslog:syslog {{.Out}}
   245    chmod 0600 {{.Out}}
   246  {{end}}
   247    exec {{.Cmd}}{{if .Out}} >> {{.Out}} 2>&1{{end}}
   248  end script
   249  `[1:]))