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 }