github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "path" 8 "reflect" 9 "strings" 10 11 "github.com/coreos/go-systemd/v22/dbus" 12 "github.com/juju/errors" 13 "github.com/juju/loggo" 14 "github.com/juju/utils/v3/shell" 15 16 "github.com/juju/juju/core/paths" 17 "github.com/juju/juju/service/common" 18 ) 19 20 const ( 21 LibSystemdDir = "/lib/systemd/system" 22 EtcSystemdDir = "/etc/systemd/system" 23 EtcSystemdMultiUserDir = EtcSystemdDir + "/multi-user.target.wants" 24 ) 25 26 var ( 27 logger = loggo.GetLogger("juju.service.systemd") 28 29 renderer = shell.BashRenderer{} 30 cmds = commands{renderer, executable} 31 ) 32 33 // ListServices returns the list of installed service names. 34 func ListServices() ([]string, error) { 35 // TODO(ericsnow) conn.ListUnits misses some inactive units, so we 36 // would need conn.ListUnitFiles. Such a method has been requested. 37 // (see https://github.com/coreos/go-systemd/issues/76). In the 38 // meantime we use systemctl at the shell to list the services. 39 // Once that is addressed upstream we can just call listServices here. 40 names, err := Cmdline{}.ListAll() 41 if err != nil { 42 return nil, errors.Trace(err) 43 } 44 return names, nil 45 } 46 47 // ListCommand returns a command that will list the services on a host. 48 func ListCommand() string { 49 return cmds.listAll() 50 } 51 52 // Type alias for a DBusAPI factory method. 53 type DBusAPIFactory = func() (DBusAPI, error) 54 55 // Service provides visibility into and control over a systemd service. 56 type Service struct { 57 common.Service 58 59 ConfName string 60 UnitName string 61 DirName string 62 FallBackDirName string 63 Script []byte 64 65 fileOps FileSystemOps 66 newDBus DBusAPIFactory 67 } 68 69 // NewServiceWithDefaults returns a new systemd service reference populated 70 // with sensible defaults. 71 func NewServiceWithDefaults(name string, conf common.Conf) (*Service, error) { 72 svc, err := NewService( 73 name, conf, EtcSystemdDir, NewDBusAPI, fileSystemOps{}, renderer.Join(paths.NixDataDir, "init")) 74 return svc, errors.Trace(err) 75 } 76 77 // NewService returns a new reference to an object that implements the Service 78 // interface for systemd. 79 func NewService( 80 name string, conf common.Conf, dataDir string, newDBus DBusAPIFactory, fileOps FileSystemOps, fallBackDirName string, 81 ) (*Service, error) { 82 confName := name + ".service" 83 84 service := &Service{ 85 Service: common.Service{ 86 Name: name, 87 // Conf is set in setConf. 88 }, 89 ConfName: confName, 90 UnitName: confName, 91 DirName: renderer.Join(dataDir), 92 FallBackDirName: fallBackDirName, 93 fileOps: fileOps, 94 newDBus: newDBus, 95 } 96 97 if err := service.setConf(conf); err != nil { 98 return nil, errors.Trace(err) 99 } 100 101 return service, nil 102 } 103 104 var NewDBusAPI = func() (DBusAPI, error) { 105 return dbus.New() 106 } 107 108 var newChan = func() chan string { 109 return make(chan string) 110 } 111 112 func (s *Service) errorf(err error, msg string, args ...interface{}) error { 113 msg += " for application %q" 114 args = append(args, s.Service.Name) 115 if err == nil { 116 err = errors.Errorf(msg, args...) 117 } else { 118 err = errors.Annotatef(err, msg, args...) 119 } 120 err.(*errors.Err).SetLocation(1) 121 logger.Errorf("%v", err) 122 logger.Debugf("stack trace:\n%s", errors.ErrorStack(err)) 123 return err 124 } 125 126 // Name implements service.Service. 127 func (s Service) Name() string { 128 return s.Service.Name 129 } 130 131 // Conf implements service.Service. 132 func (s Service) Conf() common.Conf { 133 return s.Service.Conf 134 } 135 136 func (s *Service) serialize() ([]byte, error) { 137 data, err := serialize(s.UnitName, s.Service.Conf, renderer) 138 if err != nil { 139 return nil, s.errorf(err, "failed to serialize conf") 140 } 141 return data, nil 142 } 143 144 func (s *Service) deserialize(data []byte) (common.Conf, error) { 145 conf, err := deserialize(data, renderer) 146 if err != nil { 147 return conf, s.errorf(err, "failed to deserialize conf") 148 } 149 return conf, nil 150 } 151 152 func (s *Service) validate(conf common.Conf) error { 153 if err := validate(s.Service.Name, conf, &renderer); err != nil { 154 return s.errorf(err, "invalid conf") 155 } 156 return nil 157 } 158 159 func (s *Service) normalize(conf common.Conf) (common.Conf, []byte) { 160 scriptPath := renderer.ScriptFilename(s.execStartFileName(), s.DirName) 161 return normalize(s.Service.Name, conf, scriptPath, &renderer) 162 } 163 164 func (s *Service) setConf(conf common.Conf) error { 165 if conf.IsZero() { 166 s.Service.Conf = conf 167 return nil 168 } 169 170 normalConf, data := s.normalize(conf) 171 if err := s.validate(normalConf); err != nil { 172 return errors.Trace(err) 173 } 174 175 s.Script = data 176 s.Service.Conf = normalConf 177 return nil 178 } 179 180 // Installed implements Service. 181 func (s *Service) Installed() (bool, error) { 182 names, err := ListServices() 183 if err != nil { 184 return false, s.errorf(err, "failed to list services") 185 } 186 for _, name := range names { 187 if name == s.Service.Name { 188 return true, nil 189 } 190 } 191 return false, nil 192 } 193 194 // Exists implements Service. 195 func (s *Service) Exists() (bool, error) { 196 if s.NoConf() { 197 return false, s.errorf(nil, "no conf expected") 198 } 199 200 same, err := s.check() 201 if err != nil { 202 return false, errors.Trace(err) 203 } 204 return same, nil 205 } 206 207 func (s *Service) check() (bool, error) { 208 conf, err := s.readConf() 209 if err != nil { 210 return false, errors.Trace(err) 211 } 212 normalConf, _ := s.normalize(s.Service.Conf) 213 return reflect.DeepEqual(normalConf, conf), nil 214 } 215 216 func (s *Service) readConf() (common.Conf, error) { 217 var conf common.Conf 218 219 data, err := Cmdline{}.conf(s.Service.Name, s.DirName) 220 if err != nil && !strings.Contains(err.Error(), "No such file or directory") { 221 return conf, s.errorf(err, "failed to read conf from systemd") 222 } else if err != nil && strings.Contains(err.Error(), "No such file or directory") { 223 // give another try to check if db service exists in /var/lib/juju/init. 224 // this check can be useful for installing mongoDB during upgrade. 225 _, err = Cmdline{}.conf(s.Service.Name, renderer.Join(s.FallBackDirName, s.Service.Name)) 226 if err != nil { 227 return conf, s.errorf(err, "failed to read conf from systemd") 228 } 229 // FIXME: (stickupkid) - I think this is wrong, as we never use the retry. 230 return common.Conf{}, nil 231 } 232 233 conf, err = s.deserialize(data) 234 if err != nil { 235 return conf, errors.Trace(err) 236 } 237 return conf, nil 238 } 239 240 func (s *Service) newConn() (DBusAPI, error) { 241 conn, err := s.newDBus() 242 if err != nil { 243 logger.Errorf("failed to connect to dbus for application %q: %v", s.Service.Name, err) 244 } 245 return conn, err 246 } 247 248 // Running implements Service. 249 func (s *Service) Running() (bool, error) { 250 conn, err := s.newConn() 251 if err != nil { 252 return false, errors.Trace(err) 253 } 254 defer conn.Close() 255 256 units, err := conn.ListUnits() 257 if err != nil { 258 return false, s.errorf(err, "failed to query services from dbus") 259 } 260 261 for _, unit := range units { 262 if unit.Name == s.UnitName { 263 running := unit.LoadState == "loaded" && unit.ActiveState == "active" 264 return running, nil 265 } 266 } 267 return false, nil 268 } 269 270 // Start implements Service. 271 func (s *Service) Start() error { 272 err := s.start() 273 if errors.IsAlreadyExists(err) { 274 logger.Debugf("service %q already running", s.Name()) 275 return nil 276 } else if err != nil { 277 logger.Errorf("service %q failed to start: %v", s.Name(), err) 278 return err 279 } 280 logger.Debugf("service %q successfully started", s.Name()) 281 return nil 282 } 283 284 func (s *Service) start() error { 285 installed, err := s.Installed() 286 if err != nil { 287 return errors.Trace(err) 288 } 289 if !installed { 290 return errors.NotFoundf("application " + s.Service.Name) 291 } 292 running, err := s.Running() 293 if err != nil { 294 return errors.Trace(err) 295 } 296 if running { 297 return errors.AlreadyExistsf("running service %s", s.Service.Name) 298 } 299 300 conn, err := s.newConn() 301 if err != nil { 302 return errors.Trace(err) 303 } 304 defer conn.Close() 305 306 statusCh := newChan() 307 _, err = conn.StartUnit(s.UnitName, "fail", statusCh) 308 if err != nil { 309 return s.errorf(err, "dbus start request failed") 310 } 311 312 if err := s.wait("start", statusCh); err != nil { 313 return errors.Trace(err) 314 } 315 316 return nil 317 } 318 319 func (s *Service) wait(op string, statusCh chan string) error { 320 status := <-statusCh 321 322 // TODO(ericsnow) Other status values *may* be okay. See: 323 // https://godoc.org/github.com/coreos/go-systemd/dbus#Conn.StartUnit 324 if status != "done" { 325 return s.errorf(nil, "failed to %s (API status %q)", op, status) 326 } 327 return nil 328 } 329 330 // Stop implements Service. 331 func (s *Service) Stop() error { 332 err := s.stop() 333 if errors.IsNotFound(err) { 334 logger.Debugf("service %q not running", s.Name()) 335 return nil 336 } else if err != nil { 337 logger.Errorf("service %q failed to stop: %v", s.Name(), err) 338 return err 339 } 340 logger.Debugf("service %q successfully stopped", s.Name()) 341 return nil 342 } 343 344 func (s *Service) stop() error { 345 running, err := s.Running() 346 if err != nil { 347 return errors.Trace(err) 348 } 349 if !running { 350 return errors.NotFoundf("running service %s", s.Service.Name) 351 } 352 353 conn, err := s.newConn() 354 if err != nil { 355 return errors.Trace(err) 356 } 357 defer conn.Close() 358 359 statusCh := newChan() 360 _, err = conn.StopUnit(s.UnitName, "fail", statusCh) 361 if err != nil { 362 return s.errorf(err, "dbus stop request failed") 363 } 364 365 if err := s.wait("stop", statusCh); err != nil { 366 return errors.Trace(err) 367 } 368 369 return err 370 } 371 372 // Remove implements Service. 373 func (s *Service) Remove() error { 374 err := s.remove() 375 if errors.IsNotFound(err) { 376 logger.Debugf("service %q not installed", s.Name()) 377 return nil 378 } else if err != nil { 379 logger.Errorf("failed to remove service %q: %v", s.Name(), err) 380 return err 381 } 382 logger.Debugf("service %q successfully removed", s.Name()) 383 return nil 384 } 385 386 func (s *Service) remove() error { 387 installed, err := s.Installed() 388 if err != nil { 389 return errors.Trace(err) 390 } 391 if !installed { 392 return errors.NotFoundf("service %s", s.Service.Name) 393 } 394 395 conn, err := s.newConn() 396 if err != nil { 397 return errors.Trace(err) 398 } 399 defer conn.Close() 400 401 _, err = conn.DisableUnitFiles([]string{s.UnitName}, false) 402 if err != nil { 403 return s.errorf(err, "dbus disable request failed") 404 } 405 406 if err := conn.Reload(); err != nil { 407 return s.errorf(err, "dbus post-disable daemon reload request failed") 408 } 409 410 // Remove the service unit file and the exec-start script. 411 if err := s.fileOps.Remove(path.Join(s.DirName, s.ConfName)); err != nil { 412 return s.errorf(err, "failed to delete service unit file") 413 } 414 if err := s.fileOps.Remove(renderer.ScriptFilename(s.execStartFileName(), s.DirName)); err != nil { 415 return s.errorf(err, "failed to delete service exec-start script") 416 } 417 418 return nil 419 } 420 421 // Install implements Service. 422 func (s *Service) Install() error { 423 if s.NoConf() { 424 return s.errorf(nil, "missing conf") 425 } 426 427 err := s.install() 428 if errors.IsAlreadyExists(err) { 429 logger.Debugf("service %q already installed", s.Name()) 430 return nil 431 } else if err != nil { 432 logger.Errorf("failed to install service %q: %v", s.Name(), err) 433 return err 434 } 435 logger.Debugf("service %q successfully installed", s.Name()) 436 return nil 437 } 438 439 func (s *Service) install() error { 440 installed, err := s.Installed() 441 if err != nil { 442 return errors.Trace(err) 443 } 444 if installed { 445 same, err := s.check() 446 if err != nil { 447 return errors.Trace(err) 448 } 449 if same { 450 return errors.AlreadyExistsf("service %s", s.Service.Name) 451 } 452 // An old copy is already running so stop it first. 453 if err := s.Stop(); err != nil { 454 return errors.Annotate(err, "systemd: could not stop old service") 455 } 456 if err := s.Remove(); err != nil { 457 return errors.Annotate(err, "systemd: could not remove old service") 458 } 459 } 460 461 return s.WriteService() 462 } 463 464 func (s *Service) writeConf() (string, error) { 465 data, err := s.serialize() 466 if err != nil { 467 return "", errors.Trace(err) 468 } 469 470 filename := path.Join(s.DirName, s.ConfName) 471 472 if s.Script != nil { 473 scriptPath := renderer.ScriptFilename(s.execStartFileName(), s.DirName) 474 if scriptPath != s.Service.Conf.ExecStart { 475 err := errors.Errorf("wrong script path: expected %q, got %q", scriptPath, s.Service.Conf.ExecStart) 476 return filename, s.errorf(err, "failed to write script at %q", scriptPath) 477 } 478 // TODO(ericsnow) Use the renderer here for the perms. 479 if err := s.fileOps.WriteFile(scriptPath, s.Script, 0755); err != nil { 480 return filename, s.errorf(err, "failed to write script at %q", scriptPath) 481 } 482 } 483 484 if err := s.fileOps.WriteFile(filename, data, 0644); err != nil { 485 return filename, s.errorf(err, "failed to write conf file %q", filename) 486 } 487 488 return filename, nil 489 } 490 491 // InstallCommands implements Service. 492 func (s *Service) InstallCommands() ([]string, error) { 493 if s.NoConf() { 494 return nil, s.errorf(nil, "missing conf") 495 } 496 497 name := s.Name() 498 dirname := s.DirName 499 500 data, err := s.serialize() 501 if err != nil { 502 return nil, errors.Trace(err) 503 } 504 505 var cmdList []string 506 if s.Script != nil { 507 scriptName := renderer.Base(renderer.ScriptFilename(s.execStartFileName(), "")) 508 cmdList = append(cmdList, []string{ 509 // TODO(ericsnow) Use the renderer here. 510 cmds.writeFile(scriptName, dirname, s.Script), 511 cmds.chmod(scriptName, dirname, 0755), 512 }...) 513 } 514 cmdList = append(cmdList, []string{ 515 cmds.writeConf(name, dirname, data), 516 cmds.link(name, dirname), 517 cmds.reload(), 518 cmds.enableLinked(name, dirname), 519 }...) 520 return cmdList, nil 521 } 522 523 // StartCommands implements Service. 524 func (s *Service) StartCommands() ([]string, error) { 525 name := s.Name() 526 cmdList := []string{ 527 cmds.start(name), 528 } 529 return cmdList, nil 530 } 531 532 // WriteService (UpgradableService) writes a systemd unit file for the service 533 // and ensures that it is linked and enabled by systemd. 534 func (s *Service) WriteService() error { 535 filename, err := s.writeConf() 536 if err != nil { 537 return errors.Trace(err) 538 } 539 540 // If systemd is not the running init system, 541 // then do not attempt to use it for linking unit files. 542 if !IsRunning() { 543 return nil 544 } 545 546 conn, err := s.newConn() 547 if err != nil { 548 return errors.Trace(err) 549 } 550 defer conn.Close() 551 552 const runtime, force = false, true 553 if _, err = conn.LinkUnitFiles([]string{filename}, runtime, force); err != nil { 554 return s.errorf(err, "dbus link request failed") 555 } 556 557 err = conn.Reload() 558 if err != nil { 559 return s.errorf(err, "dbus post-link daemon reload request failed") 560 } 561 562 if _, _, err = conn.EnableUnitFiles([]string{filename}, runtime, force); err != nil { 563 return s.errorf(err, "dbus enable request failed") 564 565 } 566 return nil 567 } 568 569 // scriptFileName returns the name of the file that will contain a start script 570 // indicated by the service unit file `ExecStart` property. 571 // This is required for non-trivial start-up logic, as bash-isms are not 572 // supported. 573 // See: https://www.freedesktop.org/software/systemd/man/systemd.service.html#Command%20lines 574 func (s *Service) execStartFileName() string { 575 return s.Name() + "-exec-start" 576 } 577 578 // SysdReload reloads Service daemon. 579 func SysdReload() error { 580 err := Cmdline{}.reload() 581 if err != nil { 582 logger.Errorf("services not reloaded %v\n", err) 583 return err 584 } 585 return nil 586 }