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