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