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