github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/service/upstart/upstart.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package upstart 5 6 import ( 7 "bytes" 8 "fmt" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path" 13 "regexp" 14 "runtime" 15 "text/template" 16 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/utils/shell" 20 21 "github.com/juju/juju/service/common" 22 ) 23 24 var ( 25 InitDir = "/etc/init" // the default init directory name. 26 27 logger = loggo.GetLogger("juju.service.upstart") 28 initctlPath = "/sbin/initctl" 29 servicesRe = regexp.MustCompile("^([a-zA-Z0-9-_:]+)\\.conf$") 30 renderer = &shell.BashRenderer{} 31 ) 32 33 // IsRunning returns whether or not upstart is the local init system. 34 func IsRunning() (bool, error) { 35 // On windows casting the error to exec.Error does not yield a os.PathError type 36 // It's easyer to just return false before even trying to execute an external command 37 // on windows at least 38 if runtime.GOOS == "windows" { 39 return false, nil 40 } 41 42 // TODO(ericsnow) This function should be fixed to precisely match 43 // the equivalent shell script line in service/discovery.go. 44 45 cmd := exec.Command(initctlPath, "--system", "list") 46 _, err := cmd.CombinedOutput() 47 if err == nil { 48 return true, nil 49 } 50 51 if isCmdNotFoundErr(err) { 52 return false, nil 53 } 54 // Note: initctl will fail if upstart is installed but not running. 55 // The error message will be: 56 // Name "com.ubuntu.Upstart" does not exist 57 return false, errors.Annotatef(err, "exec %q failed", initctlPath) 58 } 59 60 // isCmdNotFoundErr returns true if the provided error indicates that the 61 // command passed to exec.LookPath or exec.Command was not found. 62 func isCmdNotFoundErr(err error) bool { 63 err = errors.Cause(err) 64 if os.IsNotExist(err) { 65 // Executable could not be found, go 1.3 and later 66 return true 67 } 68 if err == exec.ErrNotFound { 69 return true 70 } 71 if execErr, ok := err.(*exec.Error); ok { 72 // Executable could not be found, go 1.2 73 if os.IsNotExist(execErr.Err) || execErr.Err == exec.ErrNotFound { 74 return true 75 } 76 } 77 return false 78 } 79 80 // ListServices returns the name of all installed services on the 81 // local host. 82 func ListServices() ([]string, error) { 83 fis, err := ioutil.ReadDir(InitDir) 84 if err != nil { 85 return nil, errors.Trace(err) 86 } 87 88 var services []string 89 for _, fi := range fis { 90 if groups := servicesRe.FindStringSubmatch(fi.Name()); len(groups) > 0 { 91 services = append(services, groups[1]) 92 } 93 } 94 return services, nil 95 } 96 97 // ListCommand returns a command that will list the services on a host. 98 func ListCommand() string { 99 // TODO(ericsnow) Do "ls /etc/init/*.conf" instead? 100 return `sudo initctl list | awk '{print $1}' | sort | uniq` 101 } 102 103 var startedRE = regexp.MustCompile(`^.* start/running(?:, process (\d+))?\n$`) 104 105 // Service provides visibility into and control over an upstart service. 106 type Service struct { 107 common.Service 108 } 109 110 func NewService(name string, conf common.Conf) *Service { 111 return &Service{ 112 Service: common.Service{ 113 Name: name, 114 Conf: conf, 115 }, 116 } 117 } 118 119 // Name implements service.Service. 120 func (s Service) Name() string { 121 return s.Service.Name 122 } 123 124 // Conf implements service.Service. 125 func (s Service) Conf() common.Conf { 126 return s.Service.Conf 127 } 128 129 // confPath returns the path to the service's configuration file. 130 func (s *Service) confPath() string { 131 return path.Join(InitDir, s.Service.Name+".conf") 132 } 133 134 // Validate returns an error if the service is not adequately defined. 135 func (s *Service) Validate() error { 136 if err := s.Service.Validate(renderer); err != nil { 137 return errors.Trace(err) 138 } 139 140 if s.Service.Conf.Transient { 141 if len(s.Service.Conf.Env) > 0 { 142 return errors.NotSupportedf("Conf.Env (when transient)") 143 } 144 if len(s.Service.Conf.Limit) > 0 { 145 return errors.NotSupportedf("Conf.Limit (when transient)") 146 } 147 if s.Service.Conf.Logfile != "" { 148 return errors.NotSupportedf("Conf.Logfile (when transient)") 149 } 150 if s.Service.Conf.ExtraScript != "" { 151 return errors.NotSupportedf("Conf.ExtraScript (when transient)") 152 } 153 } else { 154 if s.Service.Conf.AfterStopped != "" { 155 return errors.NotSupportedf("Conf.AfterStopped (when not transient)") 156 } 157 if s.Service.Conf.ExecStopPost != "" { 158 return errors.NotSupportedf("Conf.ExecStopPost (when not transient)") 159 } 160 } 161 162 return nil 163 } 164 165 // render returns the upstart configuration for the service as a slice of bytes. 166 func (s *Service) render() ([]byte, error) { 167 if err := s.Validate(); err != nil { 168 return nil, err 169 } 170 conf := s.Conf() 171 if conf.Transient { 172 conf.ExecStopPost = "rm " + s.confPath() 173 } 174 return Serialize(s.Name(), conf) 175 } 176 177 // Installed returns whether the service configuration exists in the 178 // init directory. 179 func (s *Service) Installed() (bool, error) { 180 _, err := os.Stat(s.confPath()) 181 if os.IsNotExist(err) { 182 return false, nil 183 } 184 if err != nil { 185 return false, errors.Trace(err) 186 } 187 return true, nil 188 } 189 190 // Exists returns whether the service configuration exists in the 191 // init directory with the same content that this Service would have 192 // if installed. 193 func (s *Service) Exists() (bool, error) { 194 // In any error case, we just say it doesn't exist with this configuration. 195 // Subsequent calls into the Service will give the caller more useful errors. 196 _, same, _, err := s.existsAndSame() 197 if err != nil { 198 return false, errors.Trace(err) 199 } 200 return same, nil 201 } 202 203 func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) { 204 expected, err := s.render() 205 if err != nil { 206 return false, false, nil, errors.Trace(err) 207 } 208 current, err := ioutil.ReadFile(s.confPath()) 209 if err != nil { 210 if os.IsNotExist(err) { 211 // no existing config 212 return false, false, expected, nil 213 } 214 return false, false, nil, errors.Trace(err) 215 } 216 return true, bytes.Equal(current, expected), expected, nil 217 } 218 219 // Running returns true if the Service appears to be running. 220 func (s *Service) Running() (bool, error) { 221 cmd := exec.Command("status", "--system", s.Service.Name) 222 out, err := cmd.CombinedOutput() 223 logger.Tracef("Running \"status --system %s\": %q", s.Service.Name, out) 224 if err == nil { 225 return startedRE.Match(out), nil 226 } 227 if err.Error() != "exit status 1" { 228 return false, errors.Trace(err) 229 } 230 return false, nil 231 } 232 233 // Start starts the service. 234 func (s *Service) Start() error { 235 running, err := s.Running() 236 if err != nil { 237 return errors.Trace(err) 238 } 239 if running { 240 return nil 241 } 242 err = runCommand("start", "--system", s.Service.Name) 243 if err != nil { 244 // Double check to see if we were started before our command ran. 245 // If this fails then we simply trust it's okay. 246 if running, _ := s.Running(); running { 247 return nil 248 } 249 } 250 return err 251 } 252 253 func runCommand(args ...string) error { 254 out, err := exec.Command(args[0], args[1:]...).CombinedOutput() 255 if err == nil { 256 return nil 257 } 258 out = bytes.TrimSpace(out) 259 if len(out) > 0 { 260 return fmt.Errorf("exec %q: %v (%s)", args, err, out) 261 } 262 return fmt.Errorf("exec %q: %v", args, err) 263 } 264 265 // Stop stops the service. 266 func (s *Service) Stop() error { 267 running, err := s.Running() 268 if err != nil { 269 return errors.Trace(err) 270 } 271 if !running { 272 return nil 273 } 274 return runCommand("stop", "--system", s.Service.Name) 275 } 276 277 // Restart restarts the service. 278 func (s *Service) Restart() error { 279 return runCommand("restart", s.Service.Name) 280 } 281 282 // Remove deletes the service configuration from the init directory. 283 func (s *Service) Remove() error { 284 installed, err := s.Installed() 285 if err != nil { 286 return errors.Trace(err) 287 } 288 if !installed { 289 return nil 290 } 291 return os.Remove(s.confPath()) 292 } 293 294 // Install installs and starts the service. 295 func (s *Service) Install() error { 296 exists, same, conf, err := s.existsAndSame() 297 if err != nil { 298 return errors.Trace(err) 299 } 300 if same { 301 return nil 302 } 303 if exists { 304 if err := s.Stop(); err != nil { 305 return errors.Annotate(err, "upstart: could not stop installed service") 306 } 307 if err := s.Remove(); err != nil { 308 return errors.Annotate(err, "upstart: could not remove installed service") 309 } 310 } 311 if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil { 312 return errors.Trace(err) 313 } 314 315 return nil 316 } 317 318 // InstallCommands returns shell commands to install the service. 319 func (s *Service) InstallCommands() ([]string, error) { 320 conf, err := s.render() 321 if err != nil { 322 return nil, err 323 } 324 cmd := fmt.Sprintf("cat > %s << 'EOF'\n%sEOF\n", s.confPath(), conf) 325 return []string{cmd}, nil 326 } 327 328 // StartCommands returns shell commands to start the service. 329 func (s *Service) StartCommands() ([]string, error) { 330 // TODO(ericsnow) Add clarification about why transient services are not started. 331 if s.Service.Conf.Transient { 332 return nil, nil 333 } 334 return []string{"start " + s.Service.Name}, nil 335 } 336 337 // Serialize renders the conf as raw bytes. 338 func Serialize(name string, conf common.Conf) ([]byte, error) { 339 var buf bytes.Buffer 340 if conf.Transient { 341 if err := transientConfT.Execute(&buf, conf); err != nil { 342 return nil, err 343 } 344 } else { 345 if err := confT.Execute(&buf, conf); err != nil { 346 return nil, err 347 } 348 } 349 return buf.Bytes(), nil 350 } 351 352 // TODO(ericsnow) Use a different solution than templates? 353 354 // BUG: %q quoting does not necessarily match libnih quoting rules 355 // (as used by upstart); this may become an issue in the future. 356 var confT = template.Must(template.New("").Parse(` 357 description "{{.Desc}}" 358 author "Juju Team <juju@lists.ubuntu.com>" 359 start on runlevel [2345] 360 stop on runlevel [!2345] 361 respawn 362 normal exit 0 363 {{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}} 364 {{end}} 365 {{range $k, $v := .Limit}}limit {{$k}} {{$v}} {{$v}} 366 {{end}} 367 script 368 {{if .ExtraScript}}{{.ExtraScript}}{{end}} 369 {{if .Logfile}} 370 # Ensure log files are properly protected 371 touch {{.Logfile}} 372 chown syslog:syslog {{.Logfile}} 373 chmod 0600 {{.Logfile}} 374 {{end}} 375 exec {{.ExecStart}}{{if .Logfile}} >> {{.Logfile}} 2>&1{{end}} 376 end script 377 `[1:])) 378 379 var transientConfT = template.Must(template.New("").Parse(` 380 description "{{.Desc}}" 381 author "Juju Team <juju@lists.ubuntu.com>" 382 start on stopped {{.AfterStopped}} 383 384 script 385 {{.ExecStart}} 386 end script 387 {{if .ExecStopPost}} 388 post-stop script 389 {{.ExecStopPost}} 390 end script 391 {{end}} 392 `[1:])) 393 394 // CleanShutdownJob is added to machines to ensure DHCP-assigned IP 395 // addresses are released on shutdown, reboot, or halt. See bug 396 // http://pad.lv/1348663 for more info. 397 const CleanShutdownJob = ` 398 author "Juju Team <juju@lists.ubuntu.com>" 399 description "Stop all network interfaces on shutdown" 400 start on runlevel [016] 401 task 402 console output 403 404 exec /sbin/ifdown -a -v --force 405 ` 406 407 // CleanShutdownJobPath is the full file path where CleanShutdownJob 408 // is created. 409 const CleanShutdownJobPath = "/etc/init/juju-clean-shutdown.conf"