github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/service/snap/snap.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // Package snap is a minimal service.Service implementation, derived from the on service/upstart package.
     5  package snap
     6  
     7  import (
     8  	"fmt"
     9  	"os/exec"
    10  	"regexp"
    11  	"runtime"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/utils"
    17  	"github.com/juju/utils/set"
    18  	"github.com/juju/utils/shell"
    19  
    20  	"github.com/juju/juju/service/common"
    21  )
    22  
    23  const (
    24  	// Command is a path to the snap binary, or to one that can be detected by os.Exec
    25  	Command = "snap"
    26  
    27  	defaultConfinementPolicy = "jailmode"
    28  	defaultChannel           = "stable"
    29  )
    30  
    31  var (
    32  	logger = loggo.GetLogger("juju.service.snap")
    33  
    34  	// snapNameRe is derived from https://github.com/snapcore/snapcraft/blob/a2ef08109d86259a0748446f41bce5205d00a922/schema/snapcraft.yaml#L81-106
    35  	// but does not test for "--"
    36  	snapNameRe = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,39}[^-]$")
    37  
    38  	// ConfinementPolicies represents the legal flags for installing a snap
    39  	ConfinementPolicies = set.NewStrings("devmode", "classic", "jailmode")
    40  
    41  	// Channels represents the legal channels for installing a snap
    42  	Channels = set.NewStrings("edge", "beta", "candidate", "stable")
    43  )
    44  
    45  // BackgroundService represents the a service that snaps define.
    46  // For example, the multipass snap includes the libvirt-bin and multipassd background services.
    47  type BackgroundService struct {
    48  	// name is the name of the service, without the snap name.
    49  	// For example , for the`juju-db.daemon` service, use the name `daemon`.
    50  	Name string
    51  
    52  	// enableAtStartup determines whether services provided
    53  	// by the snap should be started with the `--enable` flag
    54  	EnableAtStartup bool
    55  }
    56  
    57  // Validate checks that the construction parameters of
    58  // backgroundService are valid. Successful validation
    59  // returns nil.
    60  func (backgroundService *BackgroundService) Validate() error {
    61  	name := backgroundService.Name
    62  	if name == "" {
    63  		return errors.NotValidf("backgroundService.Name must be non-empty -")
    64  	}
    65  
    66  	if !snapNameRe.MatchString(name) {
    67  		return errors.NotValidf("backgroundService.Name (%s) fails validation check -", name)
    68  	}
    69  
    70  	return nil
    71  }
    72  
    73  // App is a wrapper around a single snap
    74  type App struct {
    75  	Name               string
    76  	ConfinementPolicy  string
    77  	Channel            string
    78  	BackgroundServices []BackgroundService
    79  	Prerequisites      []App
    80  }
    81  
    82  func (a *App) Validate() error {
    83  	var validationErrors = []error{}
    84  
    85  	if !Channels.Contains(a.Channel) {
    86  		err := errors.NotValidf("%v is not a supported Channel (supported: %v)", a.Channel, Channels)
    87  		validationErrors = append(validationErrors, err)
    88  	}
    89  
    90  	if !ConfinementPolicies.Contains(a.ConfinementPolicy) {
    91  		err := errors.NotValidf("%v is not a supported ConfinementPolicy of running snaps (supported: %v)", a.ConfinementPolicy, ConfinementPolicies)
    92  		validationErrors = append(validationErrors, err)
    93  	}
    94  
    95  	if !snapNameRe.MatchString(a.Name) {
    96  		err := errors.NotValidf("app.Name")
    97  		if err != nil {
    98  			logger.Errorf("error detected in app.Name: %#v", a)
    99  			validationErrors = append(validationErrors, err)
   100  		}
   101  	}
   102  
   103  	for _, backgroundService := range a.BackgroundServices {
   104  		err := backgroundService.Validate()
   105  		if err != nil {
   106  			validationErrors = append(validationErrors, err)
   107  		}
   108  	}
   109  
   110  	for _, prerequisite := range a.Prerequisites {
   111  		err := prerequisite.Validate()
   112  		if err != nil {
   113  			validationErrors = append(validationErrors, err)
   114  		}
   115  	}
   116  
   117  	if len(validationErrors) == 0 {
   118  		return nil
   119  	}
   120  
   121  	return errors.NotValidf("%v - snap.App", validationErrors)
   122  }
   123  
   124  // StartCommands returns a list if shell commands that should be executed (in order)
   125  // to start App and its background services. executeable is a path to the snap
   126  // executable. If the app has prerequisite applications defined, then take care to call
   127  // StartCommands on those apps also.
   128  func (a *App) StartCommands(executable string) []string {
   129  	if len(a.BackgroundServices) == 0 {
   130  		return []string{fmt.Sprintf("%s start %s", executable, a.Name)}
   131  	}
   132  
   133  	commands := make([]string, 0, len(a.BackgroundServices))
   134  	for _, backgroundService := range a.BackgroundServices {
   135  		enableFlag := ""
   136  		if backgroundService.EnableAtStartup {
   137  			enableFlag = " --enable "
   138  		}
   139  
   140  		command := fmt.Sprintf("%s start %s %s.%s", executable, enableFlag, a.Name, backgroundService.Name)
   141  		commands = append(commands, command)
   142  	}
   143  	return commands
   144  }
   145  
   146  // IsRunning indicates whether Snap is currently running on the system.
   147  // When the snap command (normally installed to /usr/bin/snap) cannot be
   148  // detected, IsRunning returns (false, nil). Other errors result in (false, err).
   149  func IsRunning() (bool, error) {
   150  	if runtime.GOOS == "windows" {
   151  		return false, nil
   152  	}
   153  
   154  	cmd := exec.Command(Command, "version")
   155  	out, err := cmd.CombinedOutput()
   156  	logger.Debugf("snap version output: %#v", string(out[:]))
   157  	if err == nil {
   158  		return true, nil
   159  	}
   160  	if common.IsCmdNotFoundErr(err) {
   161  		return false, nil
   162  	}
   163  
   164  	return false, errors.Annotatef(err, "exec %q failed", Command)
   165  }
   166  
   167  // SetSnapConfig sets a snap's key to value.
   168  func SetSnapConfig(snap string, key string, value string) error {
   169  	if key == "" {
   170  		return errors.NotValidf("key must not be empty")
   171  	}
   172  
   173  	cmd := exec.Command(Command, "set", snap, fmt.Sprintf("%s=%s", key, value))
   174  	_, err := cmd.Output()
   175  	if err != nil {
   176  		return errors.Annotate(err, fmt.Sprintf("setting snap %s config %s to %s", snap, key, value))
   177  	}
   178  
   179  	return nil
   180  }
   181  
   182  // ListCommand returns a command that will be interpreted by a shell
   183  // to produce a list of currently-installed services that are managed by snap.
   184  func ListCommand() string {
   185  	// filters the output from `snap list` to only be a newline-delimited list of snaps
   186  	return Command + " services | tail +2 | cut -d ' ' -f1 | sort -u"
   187  }
   188  
   189  // ListServices returns a list of services that are being managed by snap.
   190  func ListServices() ([]string, error) {
   191  	fullCommand := strings.Fields(ListCommand())
   192  	services, err := utils.RunCommand(fullCommand[0], fullCommand[1:]...)
   193  	if err != nil {
   194  		return []string{}, errors.Trace(err)
   195  	}
   196  	return strings.Split(services, "\n"), nil
   197  }
   198  
   199  // Service is a type for services that are being managed by snapd as snaps.
   200  type Service struct {
   201  	scriptRenderer shell.Renderer
   202  	executable     string
   203  	app            App
   204  	conf           common.Conf
   205  }
   206  
   207  // NewService returns a new Service defined by `conf`, with
   208  // the name `name`. If no BackgroundServices are provided, manage all of the snap's background services together.
   209  func NewService(name string, conf common.Conf, snapPath string, Channel string, ConfinementPolicy string, backgroundServices []BackgroundService, prerequisites []App) (Service, error) {
   210  	app := App{
   211  		Name:               name,
   212  		ConfinementPolicy:  ConfinementPolicy,
   213  		Channel:            Channel,
   214  		BackgroundServices: backgroundServices,
   215  		Prerequisites:      prerequisites,
   216  	}
   217  	err := app.Validate()
   218  	if err != nil {
   219  		return Service{}, errors.Trace(err)
   220  	}
   221  
   222  	svc := Service{
   223  		scriptRenderer: &shell.BashRenderer{},
   224  		executable:     snapPath,
   225  		app:            app,
   226  		conf:           conf,
   227  	}
   228  
   229  	return svc, nil
   230  }
   231  
   232  func NewApp(name string) App {
   233  	return App{
   234  		Name:              name,
   235  		ConfinementPolicy: defaultConfinementPolicy,
   236  		Channel:           defaultChannel,
   237  	}
   238  }
   239  
   240  // NewServiceFromName returns a service that manages all of a snap's
   241  // services as if they were a single service. NewServiceFromName uses
   242  // the name parameter to fetch and install a snap with a matching name, then uses
   243  // default policies for the installation. To install a snap with --classic confinement,
   244  // or via --edge, --candidate or --beta, then create the Service via another method.
   245  func NewServiceFromName(name string, conf common.Conf) (Service, error) {
   246  	Prerequisites := []App{}
   247  	BackgroundServices := []BackgroundService{}
   248  	Channel := defaultChannel
   249  	ConfinementPolicy := defaultConfinementPolicy
   250  
   251  	return NewService(name, conf, Command, Channel, ConfinementPolicy, BackgroundServices, Prerequisites)
   252  
   253  }
   254  
   255  // Validate validates that snap.Service has been correctly configured.
   256  // Validate returns nil when successful and an error when successful.
   257  func (s Service) Validate() error {
   258  	var validationErrors = []error{}
   259  
   260  	err := s.app.Validate()
   261  	if err != nil {
   262  		validationErrors = append(validationErrors, err)
   263  	}
   264  
   265  	for _, prerequisite := range s.app.Prerequisites {
   266  		err = prerequisite.Validate()
   267  		if err != nil {
   268  			validationErrors = append(validationErrors, err)
   269  		}
   270  	}
   271  
   272  	if len(validationErrors) == 0 {
   273  		return nil
   274  	}
   275  
   276  	return errors.Errorf("snap.Service validation failed %v", validationErrors)
   277  }
   278  
   279  // Name returns the service's name. It should match snap's naming conventions,
   280  // e.g. <snap> for all services provided by <snap> and `<snap>.<app>` for a specific service
   281  // under the snap's control.For example, the `juju-db` snap provides a `daemon` service.
   282  // Its name is `juju-db.daemon`.
   283  //
   284  // Name is part of the service.Service interface
   285  func (s Service) Name() string {
   286  	return s.app.Name
   287  }
   288  
   289  // Conf returns the service's configuration.
   290  //
   291  // Conf is part of the service.Service interface.
   292  func (s Service) Conf() common.Conf {
   293  	return s.conf
   294  }
   295  
   296  // Running returns (true, nil) when snap indicates that service is currently active.
   297  func (s Service) Running() (bool, error) {
   298  	_, _, running, err := s.status()
   299  	if err != nil {
   300  		return false, errors.Trace(err)
   301  	}
   302  	return running, nil
   303  }
   304  
   305  // Exists is not implemented for snaps.
   306  //
   307  // Exists is part of the service.Service interface.
   308  func (s Service) Exists() (bool, error) {
   309  	return s.Installed()
   310  }
   311  
   312  // Install installs the snap and its background services.
   313  //
   314  // Install is part of the service.Service interface.
   315  func (s Service) Install() error {
   316  	commands, err := s.InstallCommands()
   317  	if err != nil {
   318  		return errors.Trace(err)
   319  	}
   320  	for _, cmd := range commands {
   321  		if cmd == "" {
   322  			continue
   323  		}
   324  		logger.Infof("command: %v", cmd)
   325  		cmdParts := strings.Fields(cmd)
   326  		executable := cmdParts[0]
   327  		args := cmdParts[1:]
   328  		out, err := utils.RunCommand(executable, args...)
   329  		if err != nil {
   330  			return errors.Annotatef(err, "output: %v", out)
   331  		}
   332  
   333  	}
   334  	return nil
   335  }
   336  
   337  // Installed returns true if the service has been successfully installed.
   338  //
   339  // Installed is part of the service.Service interface.
   340  func (s Service) Installed() (bool, error) {
   341  	installed, _, _, err := s.status()
   342  	if err != nil {
   343  		return false, errors.Trace(err)
   344  	}
   345  	return installed, nil
   346  }
   347  
   348  // InstallCommands returns a slice of shell commands that is
   349  // executed independently, in serial, by a shell. When the
   350  // final command returns with a 0 exit code, the installation
   351  // will be deemed to have been successful.
   352  //
   353  // InstallCommands is part of the service.Service interface
   354  func (s Service) InstallCommands() ([]string, error) {
   355  	commands := make([]string, 0, 1+len(s.app.Prerequisites))
   356  
   357  	for _, prerequisite := range s.app.Prerequisites {
   358  		command := fmt.Sprintf("%v install --%v --%v %v",
   359  			s.executable,
   360  			prerequisite.Channel,
   361  			prerequisite.ConfinementPolicy,
   362  			prerequisite.Name,
   363  		)
   364  		logger.Infof("preparing command: %v", command)
   365  		commands = append(commands, command)
   366  	}
   367  
   368  	command := fmt.Sprintf("%v install --%v --%v %v",
   369  		s.executable,
   370  		s.app.Channel,
   371  		s.app.ConfinementPolicy,
   372  		s.app.Name,
   373  	)
   374  	logger.Infof("preparing command: %v", command)
   375  	commands = append(commands, command)
   376  	return commands, nil
   377  }
   378  
   379  // StartCommands returns a slice of strings. that are
   380  // shell commands to be executed by a shell which start the service.
   381  func (s Service) StartCommands() ([]string, error) {
   382  	commands := make([]string, 0, 1+len(s.app.Prerequisites))
   383  	for _, prerequisite := range s.app.Prerequisites {
   384  		commands = append(commands, prerequisite.StartCommands(s.executable)...)
   385  	}
   386  	commands = append(commands, s.app.StartCommands(s.executable)...)
   387  	return commands, nil
   388  }
   389  
   390  // status returns an interpreted output from the `snap services` command.
   391  // For example, this output from `snap services juju-db.daemon`
   392  //
   393  //     Service                                Startup  Current
   394  //     juju-db.daemon                         enabled  inactive
   395  //
   396  // returns this output from status
   397  //
   398  //     (true, true, false, nil)
   399  func (s *Service) status() (isInstalled, enabledAtStartup, isCurrentlyActive bool, err error) {
   400  	out, err := s.runCommand("services", s.Name())
   401  	if err != nil {
   402  		return false, false, false, errors.Trace(err)
   403  	}
   404  	for _, line := range strings.Split(out, "\n") {
   405  		if !strings.HasPrefix(line, s.Name()) {
   406  			continue
   407  		}
   408  
   409  		fields := strings.Fields(line)
   410  		return true, fields[1] == "enabled", fields[2] == "active", nil
   411  	}
   412  
   413  	return false, false, false, nil
   414  }
   415  
   416  // Start starts the service, returning nil when successful.
   417  // If the service is already running, Start does not restart it.
   418  //
   419  // Start is part of the service.ServiceActions interface
   420  func (s Service) Start() error {
   421  	running, err := s.Running()
   422  	if err != nil {
   423  		return errors.Trace(err)
   424  	}
   425  	if running {
   426  		return nil
   427  	}
   428  
   429  	commands, err := s.StartCommands()
   430  	if err != nil {
   431  		return errors.Trace(err)
   432  	}
   433  	for _, command := range commands {
   434  		commandParts := strings.Fields(command)
   435  		out, err := utils.RunCommand(commandParts[0], commandParts[1:]...)
   436  		if err != nil {
   437  			if strings.Contains(out, "has no services") {
   438  				continue
   439  			}
   440  			return errors.Annotatef(err, "%v -> %v", command, out)
   441  		}
   442  	}
   443  
   444  	return nil
   445  }
   446  
   447  // Stop stops a running service. Returns nil when the underlying
   448  // call to `snap stop <service-name>` exits with error code 0.
   449  //
   450  // Stop is part of the service.ServiceActions interface.
   451  func (s Service) Stop() error {
   452  	running, err := s.Running()
   453  	if err != nil {
   454  		return errors.Trace(err)
   455  	}
   456  	if !running {
   457  		return nil
   458  	}
   459  
   460  	args := []string{"stop", s.Name()}
   461  	return s.execThenExpect(args, "Stopped.")
   462  }
   463  
   464  // Remove uninstalls a service, . Returns nil when the underlying
   465  // call to `snap remove <service-name>` exits with error code 0.
   466  //
   467  // Remove is part of the service.ServiceActions interface.
   468  func (s Service) Remove() error {
   469  	err := s.Stop()
   470  	if err != nil {
   471  		return errors.Trace(err)
   472  	}
   473  
   474  	args := []string{"remove", s.Name()}
   475  	return s.execThenExpect(args, s.Name()+" removed")
   476  }
   477  
   478  // Restart restarts the service, or starts if it's not currently
   479  // running.
   480  //
   481  // Restart is part of the service.RestartableService interface
   482  func (s *Service) Restart() error {
   483  	args := []string{"restart", s.Name()}
   484  	return s.execThenExpect(args, "Restarted.")
   485  }
   486  
   487  // execThenExpect calls `snap <commandArgs>...` and then checks
   488  // stdout against expectation and snap's exit code. When there's a
   489  // mismatch or non-0 exit code, execThenExpect returns an error.
   490  func (s *Service) execThenExpect(commandArgs []string, expectation string) error {
   491  	out, err := s.runCommand(commandArgs...)
   492  	if err != nil {
   493  		return errors.Trace(err)
   494  	}
   495  	if !strings.Contains(out, expectation) {
   496  		return errors.Annotatef(err, `expected "%s", got "%s"`, expectation, out)
   497  	}
   498  	return nil
   499  }
   500  
   501  func (s *Service) runCommand(args ...string) (string, error) {
   502  	return utils.RunCommand(s.executable, args...)
   503  }