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