Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
565 lines (483 sloc) 13.3 KB
// Copyright 2015 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package systemd
import (
"io/ioutil"
"os"
"path"
"reflect"
"strings"
"github.com/coreos/go-systemd/dbus"
"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/utils/shell"
"github.com/juju/juju/service/common"
)
var (
logger = loggo.GetLogger("juju.service.systemd")
renderer = shell.BashRenderer{}
cmds = commands{renderer, executable}
)
// IsRunning returns whether or not systemd is the local init system.
func IsRunning() (bool, error) {
if _, err := os.Stat("/run/systemd/system"); err == nil {
return true, nil
} else if os.IsNotExist(err) {
return false, nil
} else {
return false, errors.Trace(err)
}
}
// ListServices returns the list of installed service names.
func ListServices() ([]string, error) {
// TODO(ericsnow) conn.ListUnits misses some inactive units, so we
// would need conn.ListUnitFiles. Such a method has been requested.
// (see https://github.com/coreos/go-systemd/issues/76). In the
// meantime we use systemctl at the shell to list the services.
// Once that is addressed upstread we can just call listServices here.
names, err := Cmdline{}.ListAll()
if err != nil {
return nil, errors.Trace(err)
}
return names, nil
}
// ListCommand returns a command that will list the services on a host.
func ListCommand() string {
return cmds.listAll()
}
// Service provides visibility into and control over a systemd service.
type Service struct {
common.Service
ConfName string
UnitName string
Dirname string
Script []byte
}
// NewService returns a new value that implements Service for systemd.
func NewService(name string, conf common.Conf, dataDir string) (*Service, error) {
confName := name + ".service"
var volName string
if conf.ExecStart != "" {
volName = renderer.VolumeName(common.Unquote(strings.Fields(conf.ExecStart)[0]))
}
dirname := volName + renderer.Join(dataDir, "init", name)
service := &Service{
Service: common.Service{
Name: name,
// Conf is set in setConf.
},
ConfName: confName,
UnitName: confName,
Dirname: dirname,
}
if err := service.setConf(conf); err != nil {
return nil, errors.Trace(err)
}
return service, nil
}
// dbusAPI exposes all the systemd API methods needed by juju.
type dbusAPI interface {
Close()
ListUnits() ([]dbus.UnitStatus, error)
StartUnit(string, string, chan<- string) (int, error)
StopUnit(string, string, chan<- string) (int, error)
LinkUnitFiles([]string, bool, bool) ([]dbus.LinkUnitFileChange, error)
EnableUnitFiles([]string, bool, bool) (bool, []dbus.EnableUnitFileChange, error)
DisableUnitFiles([]string, bool) ([]dbus.DisableUnitFileChange, error)
GetUnitProperties(string) (map[string]interface{}, error)
GetUnitTypeProperties(string, string) (map[string]interface{}, error)
Reload() error
}
var newConn = func() (dbusAPI, error) {
return dbus.New()
}
var newChan = func() chan string {
return make(chan string)
}
func (s *Service) errorf(err error, msg string, args ...interface{}) error {
msg += " for application %q"
args = append(args, s.Service.Name)
if err == nil {
err = errors.Errorf(msg, args...)
} else {
err = errors.Annotatef(err, msg, args...)
}
err.(*errors.Err).SetLocation(1)
logger.Errorf("%v", err)
logger.Debugf("stack trace:\n%s", errors.ErrorStack(err))
return err
}
// Name implements service.Service.
func (s Service) Name() string {
return s.Service.Name
}
// Conf implements service.Service.
func (s Service) Conf() common.Conf {
return s.Service.Conf
}
func (s *Service) serialize() ([]byte, error) {
data, err := serialize(s.UnitName, s.Service.Conf, renderer)
if err != nil {
return nil, s.errorf(err, "failed to serialize conf")
}
return data, nil
}
func (s *Service) deserialize(data []byte) (common.Conf, error) {
conf, err := deserialize(data, renderer)
if err != nil {
return conf, s.errorf(err, "failed to deserialize conf")
}
return conf, nil
}
func (s *Service) validate(conf common.Conf) error {
if err := validate(s.Service.Name, conf, &renderer); err != nil {
return s.errorf(err, "invalid conf")
}
return nil
}
func (s *Service) normalize(conf common.Conf) (common.Conf, []byte) {
scriptPath := renderer.ScriptFilename("exec-start", s.Dirname)
return normalize(s.Service.Name, conf, scriptPath, &renderer)
}
func (s *Service) setConf(conf common.Conf) error {
if conf.IsZero() {
s.Service.Conf = conf
return nil
}
normalConf, data := s.normalize(conf)
if err := s.validate(normalConf); err != nil {
return errors.Trace(err)
}
s.Script = data
s.Service.Conf = normalConf
return nil
}
// Installed implements Service.
func (s *Service) Installed() (bool, error) {
names, err := ListServices()
if err != nil {
return false, s.errorf(err, "failed to list services")
}
for _, name := range names {
if name == s.Service.Name {
return true, nil
}
}
return false, nil
}
// Exists implements Service.
func (s *Service) Exists() (bool, error) {
if s.NoConf() {
return false, s.errorf(nil, "no conf expected")
}
same, err := s.check()
if err != nil {
return false, errors.Trace(err)
}
return same, nil
}
func (s *Service) check() (bool, error) {
conf, err := s.readConf()
if err != nil {
return false, errors.Trace(err)
}
normalConf, _ := s.normalize(s.Service.Conf)
return reflect.DeepEqual(normalConf, conf), nil
}
func (s *Service) readConf() (common.Conf, error) {
var conf common.Conf
data, err := Cmdline{}.conf(s.Service.Name, s.Dirname)
if err != nil {
return conf, s.errorf(err, "failed to read conf from systemd")
}
conf, err = s.deserialize(data)
if err != nil {
return conf, errors.Trace(err)
}
return conf, nil
}
func (s Service) newConn() (dbusAPI, error) {
conn, err := newConn()
if err != nil {
logger.Errorf("failed to connect to dbus for application %q: %v", s.Service.Name, err)
}
return conn, err
}
// Running implements Service.
func (s *Service) Running() (bool, error) {
conn, err := s.newConn()
if err != nil {
return false, errors.Trace(err)
}
defer conn.Close()
units, err := conn.ListUnits()
if err != nil {
return false, s.errorf(err, "failed to query services from dbus")
}
for _, unit := range units {
if unit.Name == s.UnitName {
running := unit.LoadState == "loaded" && unit.ActiveState == "active"
return running, nil
}
}
return false, nil
}
// Start implements Service.
func (s *Service) Start() error {
err := s.start()
if errors.IsAlreadyExists(err) {
logger.Debugf("service %q already running", s.Name())
return nil
} else if err != nil {
logger.Errorf("service %q failed to start: %v", s.Name(), err)
return err
}
logger.Debugf("service %q successfully started", s.Name())
return nil
}
func (s *Service) start() error {
installed, err := s.Installed()
if err != nil {
return errors.Trace(err)
}
if !installed {
return errors.NotFoundf("application " + s.Service.Name)
}
running, err := s.Running()
if err != nil {
return errors.Trace(err)
}
if running {
return errors.AlreadyExistsf("running service %s", s.Service.Name)
}
conn, err := s.newConn()
if err != nil {
return errors.Trace(err)
}
defer conn.Close()
statusCh := newChan()
_, err = conn.StartUnit(s.UnitName, "fail", statusCh)
if err != nil {
return s.errorf(err, "dbus start request failed")
}
if err := s.wait("start", statusCh); err != nil {
return errors.Trace(err)
}
return nil
}
func (s *Service) wait(op string, statusCh chan string) error {
status := <-statusCh
// TODO(ericsnow) Other status values *may* be okay. See:
// https://godoc.org/github.com/coreos/go-systemd/dbus#Conn.StartUnit
if status != "done" {
return s.errorf(nil, "failed to %s (API status %q)", op, status)
}
return nil
}
// Stop implements Service.
func (s *Service) Stop() error {
err := s.stop()
if errors.IsNotFound(err) {
logger.Debugf("service %q not running", s.Name())
return nil
} else if err != nil {
logger.Errorf("service %q failed to stop: %v", s.Name(), err)
return err
}
logger.Debugf("service %q successfully stopped", s.Name())
return nil
}
func (s *Service) stop() error {
running, err := s.Running()
if err != nil {
return errors.Trace(err)
}
if !running {
return errors.NotFoundf("running service %s", s.Service.Name)
}
conn, err := s.newConn()
if err != nil {
return errors.Trace(err)
}
defer conn.Close()
statusCh := newChan()
_, err = conn.StopUnit(s.UnitName, "fail", statusCh)
if err != nil {
return s.errorf(err, "dbus stop request failed")
}
if err := s.wait("stop", statusCh); err != nil {
return errors.Trace(err)
}
return err
}
// Remove implements Service.
func (s *Service) Remove() error {
err := s.remove()
if errors.IsNotFound(err) {
logger.Debugf("service %q not installed", s.Name())
return nil
} else if err != nil {
logger.Errorf("failed to remove service %q: %v", s.Name(), err)
return err
}
logger.Debugf("service %q successfully removed", s.Name())
return nil
}
func (s *Service) remove() error {
installed, err := s.Installed()
if err != nil {
return errors.Trace(err)
}
if !installed {
return errors.NotFoundf("service %s", s.Service.Name)
}
conn, err := s.newConn()
if err != nil {
return errors.Trace(err)
}
defer conn.Close()
runtime := false
_, err = conn.DisableUnitFiles([]string{s.UnitName}, runtime)
if err != nil {
return s.errorf(err, "dbus disable request failed")
}
if err := conn.Reload(); err != nil {
return s.errorf(err, "dbus post-disable daemon reload request failed")
}
if err := removeAll(s.Dirname); err != nil {
return s.errorf(err, "failed to delete juju-managed conf dir")
}
return nil
}
var removeAll = func(name string) error {
return os.RemoveAll(name)
}
// Install implements Service.
func (s *Service) Install() error {
if s.NoConf() {
return s.errorf(nil, "missing conf")
}
err := s.install()
if errors.IsAlreadyExists(err) {
logger.Debugf("service %q already installed", s.Name())
return nil
} else if err != nil {
logger.Errorf("failed to install service %q: %v", s.Name(), err)
return err
}
logger.Debugf("service %q successfully installed", s.Name())
return nil
}
func (s *Service) install() error {
installed, err := s.Installed()
if err != nil {
return errors.Trace(err)
}
if installed {
same, err := s.check()
if err != nil {
return errors.Trace(err)
}
if same {
return errors.AlreadyExistsf("service %s", s.Service.Name)
}
// An old copy is already running so stop it first.
if err := s.Stop(); err != nil {
return errors.Annotate(err, "systemd: could not stop old service")
}
if err := s.Remove(); err != nil {
return errors.Annotate(err, "systemd: could not remove old service")
}
}
filename, err := s.writeConf()
if err != nil {
return errors.Trace(err)
}
conn, err := s.newConn()
if err != nil {
return errors.Trace(err)
}
defer conn.Close()
runtime, force := false, true
_, err = conn.LinkUnitFiles([]string{filename}, runtime, force)
if err != nil {
return s.errorf(err, "dbus link request failed")
}
if err := conn.Reload(); err != nil {
return s.errorf(err, "dbus post-link daemon reload request failed")
}
_, _, err = conn.EnableUnitFiles([]string{filename}, runtime, force)
if err != nil {
return s.errorf(err, "dbus enable request failed")
}
return nil
}
func (s *Service) writeConf() (string, error) {
data, err := s.serialize()
if err != nil {
return "", errors.Trace(err)
}
if err := mkdirAll(s.Dirname); err != nil {
return "", s.errorf(err, "failed to create juju-managed service dir %q", s.Dirname)
}
filename := path.Join(s.Dirname, s.ConfName)
if s.Script != nil {
scriptPath := renderer.ScriptFilename("exec-start", s.Dirname)
if scriptPath != s.Service.Conf.ExecStart {
err := errors.Errorf("wrong script path: expected %q, got %q", scriptPath, s.Service.Conf.ExecStart)
return filename, s.errorf(err, "failed to write script at %q", scriptPath)
}
// TODO(ericsnow) Use the renderer here for the perms.
if err := createFile(scriptPath, s.Script, 0755); err != nil {
return filename, s.errorf(err, "failed to write script at %q", scriptPath)
}
}
if err := createFile(filename, data, 0644); err != nil {
return filename, s.errorf(err, "failed to write conf file %q", filename)
}
return filename, nil
}
var mkdirAll = func(dirname string) error {
return os.MkdirAll(dirname, 0755)
}
var createFile = func(filename string, data []byte, perm os.FileMode) error {
return ioutil.WriteFile(filename, data, perm)
}
// InstallCommands implements Service.
func (s *Service) InstallCommands() ([]string, error) {
if s.NoConf() {
return nil, s.errorf(nil, "missing conf")
}
name := s.Name()
dirname := s.Dirname
data, err := s.serialize()
if err != nil {
return nil, errors.Trace(err)
}
cmdList := []string{
cmds.mkdirs(dirname),
}
if s.Script != nil {
scriptName := renderer.Base(renderer.ScriptFilename("exec-start", ""))
cmdList = append(cmdList, []string{
// TODO(ericsnow) Use the renderer here.
cmds.writeFile(scriptName, dirname, s.Script),
cmds.chmod(scriptName, dirname, 0755),
}...)
}
cmdList = append(cmdList, []string{
cmds.writeConf(name, dirname, data),
cmds.link(name, dirname),
cmds.reload(),
cmds.enableLinked(name, dirname),
}...)
return cmdList, nil
}
// StartCommands implements Service.
func (s *Service) StartCommands() ([]string, error) {
name := s.Name()
cmdList := []string{
cmds.start(name),
}
return cmdList, nil
}