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