github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/service/snap/snap.go (about) 1 // Copyright 2019 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package snap 5 6 import ( 7 "fmt" 8 "io" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "regexp" 13 "strings" 14 "time" 15 16 "github.com/juju/clock" 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/retry" 20 "github.com/juju/utils/v3" 21 "github.com/juju/utils/v3/shell" 22 23 "github.com/juju/juju/service/common" 24 "github.com/juju/juju/service/systemd" 25 ) 26 27 const ( 28 // Command is a path to the snap binary, or to one that can be detected by os.Exec 29 Command = "snap" 30 ) 31 32 var ( 33 logger = loggo.GetLogger("juju.service.snap") 34 35 // snapNameRe is derived from https://github.com/snapcore/snapcraft/blob/a2ef08109d86259a0748446f41bce5205d00a922/schema/snapcraft.yaml#L81-106 36 // but does not test for "--" 37 snapNameRe = regexp.MustCompile("^[a-z0-9][a-z0-9-]{0,39}[^-]$") 38 ) 39 40 // Runnable expects to be able to run a given command with a series of arguments 41 // and return the output and/or error from that executing command. 42 type Runnable interface { 43 Execute(name string, args ...string) (string, error) 44 } 45 46 // BackgroundService represents the a service that snaps define. 47 // For example, the multipass snap includes the libvirt-bin and multipassd background services. 48 type BackgroundService struct { 49 // name is the name of the service, without the snap name. 50 // For example , for the`juju-db.daemon` service, use the name `daemon`. 51 Name string 52 53 // enableAtStartup determines whether services provided 54 // by the snap should be started with the `--enable` flag 55 EnableAtStartup bool 56 } 57 58 // Validate checks that the construction parameters of 59 // backgroundService are valid. Successful validation 60 // returns nil. 61 func (backgroundService *BackgroundService) Validate() error { 62 name := backgroundService.Name 63 if name == "" { 64 return errors.NotValidf("empty background service name") 65 } 66 67 if !snapNameRe.MatchString(name) { 68 return errors.NotValidf("background service name %q", name) 69 } 70 71 return nil 72 } 73 74 // SetSnapConfig sets a snap's key to value. 75 func SetSnapConfig(snap string, key string, value string) error { 76 if key == "" { 77 return errors.NotValidf("key must not be empty") 78 } 79 80 cmd := exec.Command(Command, "set", snap, fmt.Sprintf("%s=%s", key, value)) 81 _, err := cmd.Output() 82 if err != nil { 83 return errors.Annotate(err, fmt.Sprintf("setting snap %s config %s to %s", snap, key, value)) 84 } 85 86 return nil 87 } 88 89 // Installable represents an installable snap. 90 type Installable interface { 91 // Name returns the name of the application 92 Name() string 93 94 // Install returns a way to install one application with all it's settings. 95 Install() []string 96 97 // Validate will validate a given application for any potential issues. 98 Validate() error 99 100 // StartCommands returns a list if shell commands that should be executed 101 // (in order) to start App and its background services. 102 StartCommands(executable string) []string 103 104 // Prerequisites defines a list of all the Prerequisites required before the 105 // application also needs to be installed. 106 Prerequisites() []Installable 107 108 // BackgroundServices returns a list of background services that are 109 // required to be installed for the main application to run. 110 BackgroundServices() []BackgroundService 111 } 112 113 // Service is a type for services that are being managed by snapd as snaps. 114 type Service struct { 115 runnable Runnable 116 clock clock.Clock 117 name string 118 scriptRenderer shell.Renderer 119 executable string 120 app Installable 121 conf common.Conf 122 configDir string 123 } 124 125 // NewService returns a new Service defined by `conf`, with the name `serviceName`. 126 // The Service abstracts service(s) provided by a snap. 127 // 128 // `serviceName` defaults to `snapName`. These two parameters are distinct to allow 129 // for a file path to provided as a `mainSnap`, implying that a local snap will be 130 // installed by snapd. 131 // 132 // If no BackgroundServices are provided, Service will wrap all of the snap's 133 // background services. 134 func NewService(mainSnap, serviceName string, conf common.Conf, snapPath, configDir, channel string, confinementPolicy ConfinementPolicy, backgroundServices []BackgroundService, prerequisites []Installable) (Service, error) { 135 if serviceName == "" { 136 serviceName = mainSnap 137 } 138 if mainSnap == "" { 139 return Service{}, errors.New("mainSnap must be provided") 140 } 141 app := &App{ 142 name: mainSnap, 143 confinementPolicy: confinementPolicy, 144 channel: channel, 145 backgroundServices: backgroundServices, 146 prerequisites: prerequisites, 147 } 148 err := app.Validate() 149 if err != nil { 150 return Service{}, errors.Trace(err) 151 } 152 153 return Service{ 154 runnable: defaultRunner{}, 155 clock: clock.WallClock, 156 name: serviceName, 157 scriptRenderer: &shell.BashRenderer{}, 158 executable: snapPath, 159 app: app, 160 conf: conf, 161 configDir: configDir, 162 }, nil 163 } 164 165 // Validate validates that snap.Service has been correctly configured. 166 // Validate returns nil when successful and an error when successful. 167 func (s Service) Validate() error { 168 if err := s.app.Validate(); err != nil { 169 return errors.Trace(err) 170 } 171 172 for _, prerequisite := range s.app.Prerequisites() { 173 if err := prerequisite.Validate(); err != nil { 174 return errors.Trace(err) 175 } 176 } 177 178 return nil 179 } 180 181 // Name returns the service's name. It should match snap's naming conventions, 182 // e.g. <snap> for all services provided by <snap> and `<snap>.<app>` for a specific service 183 // under the snap's control.For example, the `juju-db` snap provides a `daemon` service. 184 // Its name is `juju-db.daemon`. 185 func (s Service) Name() string { 186 if s.name != "" { 187 return s.name 188 } 189 return s.app.Name() 190 } 191 192 // Running returns (true, nil) when snap indicates that service is currently active. 193 func (s Service) Running() (bool, error) { 194 _, _, running, err := s.status() 195 if err != nil { 196 return false, errors.Trace(err) 197 } 198 return running, nil 199 } 200 201 // Exists is not implemented for snaps. 202 func (s Service) Exists() (bool, error) { 203 return false, errors.NotImplementedf("snap service Exists") 204 } 205 206 // Install installs the snap and its background services. 207 func (s Service) Install() error { 208 for _, app := range s.app.Prerequisites() { 209 logger.Infof("command: %v", app) 210 211 out, err := s.runCommandWithRetry(app.Install()...) 212 if err != nil { 213 return errors.Annotatef(err, "output: %v", out) 214 } 215 } 216 217 out, err := s.runCommandWithRetry(s.app.Install()...) 218 if err != nil { 219 return errors.Annotatef(err, "output: %v", out) 220 } 221 return nil 222 } 223 224 // Installed returns true if the service has been successfully installed. 225 func (s Service) Installed() (bool, error) { 226 installed, _, _, err := s.status() 227 if err != nil { 228 return false, errors.Trace(err) 229 } 230 return installed, nil 231 } 232 233 // ConfigOverride writes a systemd override to enable the 234 // specified limits to be used by the snap. 235 func (s Service) ConfigOverride() error { 236 if len(s.conf.Limit) == 0 { 237 return nil 238 } 239 240 unitOptions := systemd.ServiceLimits(s.conf) 241 data, err := io.ReadAll(systemd.UnitSerialize(unitOptions)) 242 if err != nil { 243 return errors.Trace(err) 244 } 245 246 for _, backgroundService := range s.app.BackgroundServices() { 247 overridesDir := fmt.Sprintf("%s/snap.%s.%s.service.d", s.configDir, s.name, backgroundService.Name) 248 if err := os.MkdirAll(overridesDir, 0755); err != nil { 249 return errors.Trace(err) 250 } 251 if err := os.WriteFile(filepath.Join(overridesDir, "overrides.conf"), data, 0644); err != nil { 252 return errors.Trace(err) 253 } 254 } 255 return nil 256 } 257 258 // StartCommands returns a slice of strings. that are 259 // shell commands to be executed by a shell which start the service. 260 func (s Service) StartCommands() ([]string, error) { 261 deps := s.app.Prerequisites() 262 commands := make([]string, 0, 1+len(deps)) 263 for _, prerequisite := range deps { 264 commands = append(commands, prerequisite.StartCommands(s.executable)...) 265 } 266 return append(commands, s.app.StartCommands(s.executable)...), nil 267 } 268 269 // status returns an interpreted output from the `snap services` command. 270 // For example, this output from `snap services juju-db.daemon` 271 // 272 // Service Startup Current 273 // juju-db.daemon enabled inactive 274 // 275 // returns this output from status 276 // 277 // (true, true, false, nil) 278 func (s *Service) status() (isInstalled, enabledAtStartup, isCurrentlyActive bool, err error) { 279 out, err := s.runCommand("services", s.Name()) 280 if err != nil { 281 return false, false, false, errors.Trace(err) 282 } 283 for _, line := range strings.Split(out, "\n") { 284 if !strings.HasPrefix(line, s.Name()) { 285 continue 286 } 287 288 fields := strings.Fields(line) 289 return true, fields[1] == "enabled", fields[2] == "active", nil 290 } 291 292 return false, false, false, nil 293 } 294 295 // Start starts the service, returning nil when successful. 296 // If the service is already running, Start does not restart it. 297 func (s Service) Start() error { 298 running, err := s.Running() 299 if err != nil { 300 return errors.Trace(err) 301 } 302 if running { 303 return nil 304 } 305 306 commands, err := s.StartCommands() 307 if err != nil { 308 return errors.Trace(err) 309 } 310 for _, command := range commands { 311 commandParts := strings.Fields(command) 312 out, err := utils.RunCommand(commandParts[0], commandParts[1:]...) 313 if err != nil { 314 if strings.Contains(out, "has no services") { 315 continue 316 } 317 return errors.Annotatef(err, "%v -> %v", command, out) 318 } 319 } 320 321 return nil 322 } 323 324 // Stop stops a running service. Returns nil when the underlying 325 // call to `snap stop <service-name>` exits with error code 0. 326 func (s Service) Stop() error { 327 running, err := s.Running() 328 if err != nil { 329 return errors.Trace(err) 330 } 331 if !running { 332 return nil 333 } 334 335 args := []string{"stop", s.Name()} 336 return s.execThenExpect(args, "Stopped.") 337 } 338 339 // Restart restarts the service, or starts if it's not currently 340 // running. 341 // 342 // Restart is part of the service.RestartableService interface 343 func (s Service) Restart() error { 344 args := []string{"restart", s.Name()} 345 return s.execThenExpect(args, "Restarted.") 346 } 347 348 // execThenExpect calls `snap <commandArgs>...` and then checks 349 // stdout against expectation and snap's exit code. When there's a 350 // mismatch or non-0 exit code, execThenExpect returns an error. 351 func (s Service) execThenExpect(commandArgs []string, expectation string) error { 352 out, err := s.runCommand(commandArgs...) 353 if err != nil { 354 return errors.Trace(err) 355 } 356 if !strings.Contains(out, expectation) { 357 return errors.Annotatef(err, `expected "%s", got "%s"`, expectation, out) 358 } 359 return nil 360 } 361 362 func (s Service) runCommand(args ...string) (string, error) { 363 return s.runnable.Execute(s.executable, args...) 364 } 365 366 func (s Service) runCommandWithRetry(args ...string) (res string, err error) { 367 if resErr := retry.Call(retry.CallArgs{ 368 Clock: s.clock, 369 Func: func() error { 370 res, err = s.runCommand(args...) 371 return errors.Trace(err) 372 }, 373 Delay: 5 * time.Second, 374 Attempts: 2, 375 }); resErr != nil { 376 return "", errors.Trace(resErr) 377 } 378 379 // Named args are set via the retry. 380 return 381 } 382 383 type defaultRunner struct{} 384 385 func (defaultRunner) Execute(name string, args ...string) (string, error) { 386 return utils.RunCommand(name, args...) 387 }