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