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