github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/service/systemd/service.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package systemd
     5  
     6  import (
     7  	"io/ioutil"
     8  	"os"
     9  	"path"
    10  	"reflect"
    11  	"strings"
    12  
    13  	"github.com/coreos/go-systemd/dbus"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/utils/shell"
    17  
    18  	"github.com/juju/juju/service/common"
    19  )
    20  
    21  var (
    22  	logger = loggo.GetLogger("juju.service.systemd")
    23  
    24  	renderer = shell.BashRenderer{}
    25  	cmds     = commands{renderer, executable}
    26  )
    27  
    28  // IsRunning returns whether or not systemd is the local init system.
    29  func IsRunning() (bool, error) {
    30  	if _, err := os.Stat("/run/systemd/system"); err == nil {
    31  		return true, nil
    32  	} else if os.IsNotExist(err) {
    33  		return false, nil
    34  	} else {
    35  		return false, errors.Trace(err)
    36  	}
    37  }
    38  
    39  // ListServices returns the list of installed service names.
    40  func ListServices() ([]string, error) {
    41  	// TODO(ericsnow) conn.ListUnits misses some inactive units, so we
    42  	// would need conn.ListUnitFiles. Such a method has been requested.
    43  	// (see https://github.com/coreos/go-systemd/issues/76). In the
    44  	// meantime we use systemctl at the shell to list the services.
    45  	// Once that is addressed upstread we can just call listServices here.
    46  	names, err := Cmdline{}.ListAll()
    47  	if err != nil {
    48  		return nil, errors.Trace(err)
    49  	}
    50  	return names, nil
    51  }
    52  
    53  // ListCommand returns a command that will list the services on a host.
    54  func ListCommand() string {
    55  	return cmds.listAll()
    56  }
    57  
    58  // Service provides visibility into and control over a systemd service.
    59  type Service struct {
    60  	common.Service
    61  
    62  	ConfName string
    63  	UnitName string
    64  	Dirname  string
    65  	Script   []byte
    66  }
    67  
    68  // NewService returns a new value that implements Service for systemd.
    69  func NewService(name string, conf common.Conf, dataDir string) (*Service, error) {
    70  	confName := name + ".service"
    71  	var volName string
    72  	if conf.ExecStart != "" {
    73  		volName = renderer.VolumeName(common.Unquote(strings.Fields(conf.ExecStart)[0]))
    74  	}
    75  	dirname := volName + renderer.Join(dataDir, "init", name)
    76  
    77  	service := &Service{
    78  		Service: common.Service{
    79  			Name: name,
    80  			// Conf is set in setConf.
    81  		},
    82  		ConfName: confName,
    83  		UnitName: confName,
    84  		Dirname:  dirname,
    85  	}
    86  
    87  	if err := service.setConf(conf); err != nil {
    88  		return nil, errors.Trace(err)
    89  	}
    90  
    91  	return service, nil
    92  }
    93  
    94  // dbusAPI exposes all the systemd API methods needed by juju.
    95  type dbusAPI interface {
    96  	Close()
    97  	ListUnits() ([]dbus.UnitStatus, error)
    98  	StartUnit(string, string, chan<- string) (int, error)
    99  	StopUnit(string, string, chan<- string) (int, error)
   100  	LinkUnitFiles([]string, bool, bool) ([]dbus.LinkUnitFileChange, error)
   101  	EnableUnitFiles([]string, bool, bool) (bool, []dbus.EnableUnitFileChange, error)
   102  	DisableUnitFiles([]string, bool) ([]dbus.DisableUnitFileChange, error)
   103  	GetUnitProperties(string) (map[string]interface{}, error)
   104  	GetUnitTypeProperties(string, string) (map[string]interface{}, error)
   105  	Reload() error
   106  }
   107  
   108  var newConn = func() (dbusAPI, error) {
   109  	return dbus.New()
   110  }
   111  
   112  var newChan = func() chan string {
   113  	return make(chan string)
   114  }
   115  
   116  func (s *Service) errorf(err error, msg string, args ...interface{}) error {
   117  	msg += " for application %q"
   118  	args = append(args, s.Service.Name)
   119  	if err == nil {
   120  		err = errors.Errorf(msg, args...)
   121  	} else {
   122  		err = errors.Annotatef(err, msg, args...)
   123  	}
   124  	err.(*errors.Err).SetLocation(1)
   125  	logger.Errorf("%v", err)
   126  	logger.Debugf("stack trace:\n%s", errors.ErrorStack(err))
   127  	return err
   128  }
   129  
   130  // Name implements service.Service.
   131  func (s Service) Name() string {
   132  	return s.Service.Name
   133  }
   134  
   135  // Conf implements service.Service.
   136  func (s Service) Conf() common.Conf {
   137  	return s.Service.Conf
   138  }
   139  
   140  func (s *Service) serialize() ([]byte, error) {
   141  	data, err := serialize(s.UnitName, s.Service.Conf, renderer)
   142  	if err != nil {
   143  		return nil, s.errorf(err, "failed to serialize conf")
   144  	}
   145  	return data, nil
   146  }
   147  
   148  func (s *Service) deserialize(data []byte) (common.Conf, error) {
   149  	conf, err := deserialize(data, renderer)
   150  	if err != nil {
   151  		return conf, s.errorf(err, "failed to deserialize conf")
   152  	}
   153  	return conf, nil
   154  }
   155  
   156  func (s *Service) validate(conf common.Conf) error {
   157  	if err := validate(s.Service.Name, conf, &renderer); err != nil {
   158  		return s.errorf(err, "invalid conf")
   159  	}
   160  	return nil
   161  }
   162  
   163  func (s *Service) normalize(conf common.Conf) (common.Conf, []byte) {
   164  	scriptPath := renderer.ScriptFilename("exec-start", s.Dirname)
   165  	return normalize(s.Service.Name, conf, scriptPath, &renderer)
   166  }
   167  
   168  func (s *Service) setConf(conf common.Conf) error {
   169  	if conf.IsZero() {
   170  		s.Service.Conf = conf
   171  		return nil
   172  	}
   173  
   174  	normalConf, data := s.normalize(conf)
   175  	if err := s.validate(normalConf); err != nil {
   176  		return errors.Trace(err)
   177  	}
   178  
   179  	s.Script = data
   180  	s.Service.Conf = normalConf
   181  	return nil
   182  }
   183  
   184  // Installed implements Service.
   185  func (s *Service) Installed() (bool, error) {
   186  	names, err := ListServices()
   187  	if err != nil {
   188  		return false, s.errorf(err, "failed to list services")
   189  	}
   190  	for _, name := range names {
   191  		if name == s.Service.Name {
   192  			return true, nil
   193  		}
   194  	}
   195  	return false, nil
   196  }
   197  
   198  // Exists implements Service.
   199  func (s *Service) Exists() (bool, error) {
   200  	if s.NoConf() {
   201  		return false, s.errorf(nil, "no conf expected")
   202  	}
   203  
   204  	same, err := s.check()
   205  	if err != nil {
   206  		return false, errors.Trace(err)
   207  	}
   208  	return same, nil
   209  }
   210  
   211  func (s *Service) check() (bool, error) {
   212  	conf, err := s.readConf()
   213  	if err != nil {
   214  		return false, errors.Trace(err)
   215  	}
   216  	normalConf, _ := s.normalize(s.Service.Conf)
   217  	return reflect.DeepEqual(normalConf, conf), nil
   218  }
   219  
   220  func (s *Service) readConf() (common.Conf, error) {
   221  	var conf common.Conf
   222  
   223  	data, err := Cmdline{}.conf(s.Service.Name, s.Dirname)
   224  	if err != nil {
   225  		return conf, s.errorf(err, "failed to read conf from systemd")
   226  	}
   227  
   228  	conf, err = s.deserialize(data)
   229  	if err != nil {
   230  		return conf, errors.Trace(err)
   231  	}
   232  	return conf, nil
   233  }
   234  
   235  func (s Service) newConn() (dbusAPI, error) {
   236  	conn, err := newConn()
   237  	if err != nil {
   238  		logger.Errorf("failed to connect to dbus for application %q: %v", s.Service.Name, err)
   239  	}
   240  	return conn, err
   241  }
   242  
   243  // Running implements Service.
   244  func (s *Service) Running() (bool, error) {
   245  	conn, err := s.newConn()
   246  	if err != nil {
   247  		return false, errors.Trace(err)
   248  	}
   249  	defer conn.Close()
   250  
   251  	units, err := conn.ListUnits()
   252  	if err != nil {
   253  		return false, s.errorf(err, "failed to query services from dbus")
   254  	}
   255  
   256  	for _, unit := range units {
   257  		if unit.Name == s.UnitName {
   258  			running := unit.LoadState == "loaded" && unit.ActiveState == "active"
   259  			return running, nil
   260  		}
   261  	}
   262  	return false, nil
   263  }
   264  
   265  // Start implements Service.
   266  func (s *Service) Start() error {
   267  	err := s.start()
   268  	if errors.IsAlreadyExists(err) {
   269  		logger.Debugf("service %q already running", s.Name())
   270  		return nil
   271  	} else if err != nil {
   272  		logger.Errorf("service %q failed to start: %v", s.Name(), err)
   273  		return err
   274  	}
   275  	logger.Debugf("service %q successfully started", s.Name())
   276  	return nil
   277  }
   278  
   279  func (s *Service) start() error {
   280  	installed, err := s.Installed()
   281  	if err != nil {
   282  		return errors.Trace(err)
   283  	}
   284  	if !installed {
   285  		return errors.NotFoundf("application " + s.Service.Name)
   286  	}
   287  	running, err := s.Running()
   288  	if err != nil {
   289  		return errors.Trace(err)
   290  	}
   291  	if running {
   292  		return errors.AlreadyExistsf("running service %s", s.Service.Name)
   293  	}
   294  
   295  	conn, err := s.newConn()
   296  	if err != nil {
   297  		return errors.Trace(err)
   298  	}
   299  	defer conn.Close()
   300  
   301  	statusCh := newChan()
   302  	_, err = conn.StartUnit(s.UnitName, "fail", statusCh)
   303  	if err != nil {
   304  		return s.errorf(err, "dbus start request failed")
   305  	}
   306  
   307  	if err := s.wait("start", statusCh); err != nil {
   308  		return errors.Trace(err)
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  func (s *Service) wait(op string, statusCh chan string) error {
   315  	status := <-statusCh
   316  
   317  	// TODO(ericsnow) Other status values *may* be okay. See:
   318  	//  https://godoc.org/github.com/coreos/go-systemd/dbus#Conn.StartUnit
   319  	if status != "done" {
   320  		return s.errorf(nil, "failed to %s (API status %q)", op, status)
   321  	}
   322  	return nil
   323  }
   324  
   325  // Stop implements Service.
   326  func (s *Service) Stop() error {
   327  	err := s.stop()
   328  	if errors.IsNotFound(err) {
   329  		logger.Debugf("service %q not running", s.Name())
   330  		return nil
   331  	} else if err != nil {
   332  		logger.Errorf("service %q failed to stop: %v", s.Name(), err)
   333  		return err
   334  	}
   335  	logger.Debugf("service %q successfully stopped", s.Name())
   336  	return nil
   337  }
   338  
   339  func (s *Service) stop() error {
   340  	running, err := s.Running()
   341  	if err != nil {
   342  		return errors.Trace(err)
   343  	}
   344  	if !running {
   345  		return errors.NotFoundf("running service %s", s.Service.Name)
   346  	}
   347  
   348  	conn, err := s.newConn()
   349  	if err != nil {
   350  		return errors.Trace(err)
   351  	}
   352  	defer conn.Close()
   353  
   354  	statusCh := newChan()
   355  	_, err = conn.StopUnit(s.UnitName, "fail", statusCh)
   356  	if err != nil {
   357  		return s.errorf(err, "dbus stop request failed")
   358  	}
   359  
   360  	if err := s.wait("stop", statusCh); err != nil {
   361  		return errors.Trace(err)
   362  	}
   363  
   364  	return err
   365  }
   366  
   367  // Remove implements Service.
   368  func (s *Service) Remove() error {
   369  	err := s.remove()
   370  	if errors.IsNotFound(err) {
   371  		logger.Debugf("service %q not installed", s.Name())
   372  		return nil
   373  	} else if err != nil {
   374  		logger.Errorf("failed to remove service %q: %v", s.Name(), err)
   375  		return err
   376  	}
   377  	logger.Debugf("service %q successfully removed", s.Name())
   378  	return nil
   379  }
   380  
   381  func (s *Service) remove() error {
   382  	installed, err := s.Installed()
   383  	if err != nil {
   384  		return errors.Trace(err)
   385  	}
   386  	if !installed {
   387  		return errors.NotFoundf("service %s", s.Service.Name)
   388  	}
   389  
   390  	conn, err := s.newConn()
   391  	if err != nil {
   392  		return errors.Trace(err)
   393  	}
   394  	defer conn.Close()
   395  
   396  	runtime := false
   397  	_, err = conn.DisableUnitFiles([]string{s.UnitName}, runtime)
   398  	if err != nil {
   399  		return s.errorf(err, "dbus disable request failed")
   400  	}
   401  
   402  	if err := conn.Reload(); err != nil {
   403  		return s.errorf(err, "dbus post-disable daemon reload request failed")
   404  	}
   405  
   406  	if err := removeAll(s.Dirname); err != nil {
   407  		return s.errorf(err, "failed to delete juju-managed conf dir")
   408  	}
   409  
   410  	return nil
   411  }
   412  
   413  var removeAll = func(name string) error {
   414  	return os.RemoveAll(name)
   415  }
   416  
   417  // Install implements Service.
   418  func (s *Service) Install() error {
   419  	if s.NoConf() {
   420  		return s.errorf(nil, "missing conf")
   421  	}
   422  
   423  	err := s.install()
   424  	if errors.IsAlreadyExists(err) {
   425  		logger.Debugf("service %q already installed", s.Name())
   426  		return nil
   427  	} else if err != nil {
   428  		logger.Errorf("failed to install service %q: %v", s.Name(), err)
   429  		return err
   430  	}
   431  	logger.Debugf("service %q successfully installed", s.Name())
   432  	return nil
   433  }
   434  
   435  func (s *Service) install() error {
   436  	installed, err := s.Installed()
   437  	if err != nil {
   438  		return errors.Trace(err)
   439  	}
   440  	if installed {
   441  		same, err := s.check()
   442  		if err != nil {
   443  			return errors.Trace(err)
   444  		}
   445  		if same {
   446  			return errors.AlreadyExistsf("service %s", s.Service.Name)
   447  		}
   448  		// An old copy is already running so stop it first.
   449  		if err := s.Stop(); err != nil {
   450  			return errors.Annotate(err, "systemd: could not stop old service")
   451  		}
   452  		if err := s.Remove(); err != nil {
   453  			return errors.Annotate(err, "systemd: could not remove old service")
   454  		}
   455  	}
   456  
   457  	filename, err := s.writeConf()
   458  	if err != nil {
   459  		return errors.Trace(err)
   460  	}
   461  
   462  	conn, err := s.newConn()
   463  	if err != nil {
   464  		return errors.Trace(err)
   465  	}
   466  	defer conn.Close()
   467  
   468  	runtime, force := false, true
   469  	_, err = conn.LinkUnitFiles([]string{filename}, runtime, force)
   470  	if err != nil {
   471  		return s.errorf(err, "dbus link request failed")
   472  	}
   473  
   474  	if err := conn.Reload(); err != nil {
   475  		return s.errorf(err, "dbus post-link daemon reload request failed")
   476  	}
   477  
   478  	_, _, err = conn.EnableUnitFiles([]string{filename}, runtime, force)
   479  	if err != nil {
   480  		return s.errorf(err, "dbus enable request failed")
   481  	}
   482  	return nil
   483  }
   484  
   485  func (s *Service) writeConf() (string, error) {
   486  	data, err := s.serialize()
   487  	if err != nil {
   488  		return "", errors.Trace(err)
   489  	}
   490  
   491  	if err := mkdirAll(s.Dirname); err != nil {
   492  		return "", s.errorf(err, "failed to create juju-managed service dir %q", s.Dirname)
   493  	}
   494  	filename := path.Join(s.Dirname, s.ConfName)
   495  
   496  	if s.Script != nil {
   497  		scriptPath := renderer.ScriptFilename("exec-start", s.Dirname)
   498  		if scriptPath != s.Service.Conf.ExecStart {
   499  			err := errors.Errorf("wrong script path: expected %q, got %q", scriptPath, s.Service.Conf.ExecStart)
   500  			return filename, s.errorf(err, "failed to write script at %q", scriptPath)
   501  		}
   502  		// TODO(ericsnow) Use the renderer here for the perms.
   503  		if err := createFile(scriptPath, s.Script, 0755); err != nil {
   504  			return filename, s.errorf(err, "failed to write script at %q", scriptPath)
   505  		}
   506  	}
   507  
   508  	if err := createFile(filename, data, 0644); err != nil {
   509  		return filename, s.errorf(err, "failed to write conf file %q", filename)
   510  	}
   511  
   512  	return filename, nil
   513  }
   514  
   515  var mkdirAll = func(dirname string) error {
   516  	return os.MkdirAll(dirname, 0755)
   517  }
   518  
   519  var createFile = func(filename string, data []byte, perm os.FileMode) error {
   520  	return ioutil.WriteFile(filename, data, perm)
   521  }
   522  
   523  // InstallCommands implements Service.
   524  func (s *Service) InstallCommands() ([]string, error) {
   525  	if s.NoConf() {
   526  		return nil, s.errorf(nil, "missing conf")
   527  	}
   528  
   529  	name := s.Name()
   530  	dirname := s.Dirname
   531  
   532  	data, err := s.serialize()
   533  	if err != nil {
   534  		return nil, errors.Trace(err)
   535  	}
   536  
   537  	cmdList := []string{
   538  		cmds.mkdirs(dirname),
   539  	}
   540  	if s.Script != nil {
   541  		scriptName := renderer.Base(renderer.ScriptFilename("exec-start", ""))
   542  		cmdList = append(cmdList, []string{
   543  			// TODO(ericsnow) Use the renderer here.
   544  			cmds.writeFile(scriptName, dirname, s.Script),
   545  			cmds.chmod(scriptName, dirname, 0755),
   546  		}...)
   547  	}
   548  	cmdList = append(cmdList, []string{
   549  		cmds.writeConf(name, dirname, data),
   550  		cmds.link(name, dirname),
   551  		cmds.reload(),
   552  		cmds.enableLinked(name, dirname),
   553  	}...)
   554  	return cmdList, nil
   555  }
   556  
   557  // StartCommands implements Service.
   558  func (s *Service) StartCommands() ([]string, error) {
   559  	name := s.Name()
   560  	cmdList := []string{
   561  		cmds.start(name),
   562  	}
   563  	return cmdList, nil
   564  }