github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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 "text/template" 15 "time" 16 17 "github.com/juju/errors" 18 "github.com/juju/utils" 19 20 "github.com/juju/juju/service/common" 21 ) 22 23 var startedRE = regexp.MustCompile(`^.* start/running, process (\d+)\n$`) 24 25 // InitDir holds the default init directory name. 26 var InitDir = "/etc/init" 27 28 var InstallStartRetryAttempts = utils.AttemptStrategy{ 29 Total: 1 * time.Second, 30 Delay: 250 * time.Millisecond, 31 } 32 33 // Service provides visibility into and control over an upstart service. 34 type Service struct { 35 Name string 36 Conf common.Conf 37 } 38 39 func NewService(name string, conf common.Conf) *Service { 40 if conf.InitDir == "" { 41 conf.InitDir = InitDir 42 } 43 return &Service{Name: name, Conf: conf} 44 } 45 46 // confPath returns the path to the service's configuration file. 47 func (s *Service) confPath() string { 48 return path.Join(s.Conf.InitDir, s.Name+".conf") 49 } 50 51 func (s *Service) UpdateConfig(conf common.Conf) { 52 s.Conf = conf 53 } 54 55 // validate returns an error if the service is not adequately defined. 56 func (s *Service) validate() error { 57 if s.Name == "" { 58 return errors.New("missing Name") 59 } 60 if s.Conf.InitDir == "" { 61 return errors.New("missing InitDir") 62 } 63 if s.Conf.Desc == "" { 64 return errors.New("missing Desc") 65 } 66 if s.Conf.Cmd == "" { 67 return errors.New("missing Cmd") 68 } 69 return nil 70 } 71 72 // render returns the upstart configuration for the service as a slice of bytes. 73 func (s *Service) render() ([]byte, error) { 74 if err := s.validate(); err != nil { 75 return nil, err 76 } 77 var buf bytes.Buffer 78 if err := confT.Execute(&buf, s.Conf); err != nil { 79 return nil, err 80 } 81 return buf.Bytes(), nil 82 } 83 84 // Installed returns whether the service configuration exists in the 85 // init directory. 86 func (s *Service) Installed() bool { 87 _, err := os.Stat(s.confPath()) 88 return err == nil 89 } 90 91 // Exists returns whether the service configuration exists in the 92 // init directory with the same content that this Service would have 93 // if installed. 94 func (s *Service) Exists() bool { 95 // In any error case, we just say it doesn't exist with this configuration. 96 // Subsequent calls into the Service will give the caller more useful errors. 97 _, same, _, err := s.existsAndSame() 98 if err != nil { 99 return false 100 } 101 return same 102 } 103 104 func (s *Service) existsAndSame() (exists, same bool, conf []byte, err error) { 105 expected, err := s.render() 106 if err != nil { 107 return false, false, nil, errors.Trace(err) 108 } 109 current, err := ioutil.ReadFile(s.confPath()) 110 if err != nil { 111 if os.IsNotExist(err) { 112 // no existing config 113 return false, false, expected, nil 114 } 115 return false, false, nil, errors.Trace(err) 116 } 117 return true, bytes.Equal(current, expected), expected, nil 118 } 119 120 // Running returns true if the Service appears to be running. 121 func (s *Service) Running() bool { 122 cmd := exec.Command("status", "--system", s.Name) 123 out, err := cmd.CombinedOutput() 124 if err != nil { 125 return false 126 } 127 return startedRE.Match(out) 128 } 129 130 // Start starts the service. 131 func (s *Service) Start() error { 132 if s.Running() { 133 return nil 134 } 135 err := runCommand("start", "--system", s.Name) 136 if err != nil { 137 // Double check to see if we were started before our command ran. 138 if s.Running() { 139 return nil 140 } 141 } 142 return err 143 } 144 145 func runCommand(args ...string) error { 146 out, err := exec.Command(args[0], args[1:]...).CombinedOutput() 147 if err == nil { 148 return nil 149 } 150 out = bytes.TrimSpace(out) 151 if len(out) > 0 { 152 return fmt.Errorf("exec %q: %v (%s)", args, err, out) 153 } 154 return fmt.Errorf("exec %q: %v", args, err) 155 } 156 157 // Stop stops the service. 158 func (s *Service) Stop() error { 159 if !s.Running() { 160 return nil 161 } 162 return runCommand("stop", "--system", s.Name) 163 } 164 165 // StopAndRemove stops the service and then deletes the service 166 // configuration from the init directory. 167 func (s *Service) StopAndRemove() error { 168 if !s.Installed() { 169 return nil 170 } 171 if err := s.Stop(); err != nil { 172 return err 173 } 174 return os.Remove(s.confPath()) 175 } 176 177 // Remove deletes the service configuration from the init directory. 178 func (s *Service) Remove() error { 179 if !s.Installed() { 180 return nil 181 } 182 return os.Remove(s.confPath()) 183 } 184 185 // Install installs and starts the service. 186 func (s *Service) Install() error { 187 exists, same, conf, err := s.existsAndSame() 188 if err != nil { 189 return errors.Trace(err) 190 } 191 if same { 192 return nil 193 } 194 if exists { 195 if err := s.StopAndRemove(); err != nil { 196 return errors.Annotate(err, "upstart: could not remove installed service") 197 } 198 199 } 200 if err := ioutil.WriteFile(s.confPath(), conf, 0644); err != nil { 201 return errors.Trace(err) 202 } 203 204 // On slower disks, upstart may take a short time to realise 205 // that there is a service there. 206 for attempt := InstallStartRetryAttempts.Start(); attempt.Next(); { 207 if err = s.Start(); err == nil { 208 break 209 } 210 } 211 return err 212 } 213 214 // InstallCommands returns shell commands to install and start the service. 215 func (s *Service) InstallCommands() ([]string, error) { 216 conf, err := s.render() 217 if err != nil { 218 return nil, err 219 } 220 return []string{ 221 fmt.Sprintf("cat >> %s << 'EOF'\n%sEOF\n", s.confPath(), conf), 222 "start " + s.Name, 223 }, nil 224 } 225 226 // BUG: %q quoting does not necessarily match libnih quoting rules 227 // (as used by upstart); this may become an issue in the future. 228 var confT = template.Must(template.New("").Parse(` 229 description "{{.Desc}}" 230 author "Juju Team <juju@lists.ubuntu.com>" 231 start on runlevel [2345] 232 stop on runlevel [!2345] 233 respawn 234 normal exit 0 235 {{range $k, $v := .Env}}env {{$k}}={{$v|printf "%q"}} 236 {{end}} 237 {{range $k, $v := .Limit}}limit {{$k}} {{$v}} 238 {{end}} 239 script 240 {{if .ExtraScript}}{{.ExtraScript}}{{end}} 241 {{if .Out}} 242 # Ensure log files are properly protected 243 touch {{.Out}} 244 chown syslog:syslog {{.Out}} 245 chmod 0600 {{.Out}} 246 {{end}} 247 exec {{.Cmd}}{{if .Out}} >> {{.Out}} 2>&1{{end}} 248 end script 249 `[1:]))