github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/service/systemd/conf.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 "bytes" 8 "fmt" 9 "io/ioutil" 10 "strconv" 11 "strings" 12 13 "github.com/coreos/go-systemd/unit" 14 "github.com/juju/errors" 15 "github.com/juju/utils/shell" 16 17 "github.com/juju/juju/service/common" 18 "github.com/juju/juju/version" 19 ) 20 21 var limitMap = map[string]string{ 22 "as": "LimitAS", 23 "core": "LimitCORE", 24 "cpu": "LimitCPU", 25 "data": "LimitDATA", 26 "fsize": "LimitFSIZE", 27 "memlock": "LimitMEMLOCK", 28 "msgqueue": "LimitMSGQUEUE", 29 "nice": "LimitNICE", 30 "nofile": "LimitNOFILE", 31 "nproc": "LimitNPROC", 32 "rss": "LimitRSS", 33 "rtprio": "LimitRTPRIO", 34 "sigpending": "LimitSIGPENDING", 35 "stack": "LimitSTACK", 36 } 37 38 // TODO(ericsnow) Move normalize to common.Conf.Normalize. 39 40 type confRenderer interface { 41 shell.Renderer 42 shell.ScriptRenderer 43 } 44 45 func syslogUserGroup() (string, string) { 46 var user, group string 47 switch version.Current.OS { 48 case version.CentOS: 49 user, group = "root", "adm" 50 default: 51 user, group = "syslog", "syslog" 52 } 53 54 return user, group 55 } 56 57 // normalize adjusts the conf to more standardized content and 58 // returns a new Conf with that updated content. It also returns the 59 // content of any script file that should accompany the conf. 60 func normalize(name string, conf common.Conf, scriptPath string, renderer confRenderer) (common.Conf, []byte) { 61 var data []byte 62 63 var cmds []string 64 if conf.Logfile != "" { 65 filename := conf.Logfile 66 cmds = append(cmds, "# Set up logging.") 67 cmds = append(cmds, renderer.Touch(filename, nil)...) 68 // TODO(ericsnow) We should drop the assumption that the logfile 69 // is syslog. 70 user, group := syslogUserGroup() 71 cmds = append(cmds, renderer.Chown(filename, user, group)...) 72 cmds = append(cmds, renderer.Chmod(filename, 0600)...) 73 cmds = append(cmds, renderer.RedirectOutput(filename)...) 74 cmds = append(cmds, renderer.RedirectFD("out", "err")...) 75 cmds = append(cmds, 76 "", 77 "# Run the script.", 78 ) 79 // We leave conf.Logfile alone (it will be ignored during validation). 80 } 81 cmds = append(cmds, conf.ExecStart) 82 83 if conf.ExtraScript != "" { 84 cmds = append([]string{conf.ExtraScript}, cmds...) 85 conf.ExtraScript = "" 86 } 87 if !isSimpleCommand(strings.Join(cmds, "\n")) { 88 data = renderer.RenderScript(cmds) 89 conf.ExecStart = scriptPath 90 } 91 92 if len(conf.Env) == 0 { 93 conf.Env = nil 94 } 95 96 if len(conf.Limit) == 0 { 97 conf.Limit = nil 98 } 99 100 if conf.Transient { 101 // TODO(ericsnow) Handle Transient via systemd-run command? 102 conf.ExecStopPost = commands{}.disable(name) 103 } 104 105 return conf, data 106 } 107 108 func isSimpleCommand(cmd string) bool { 109 if strings.ContainsAny(cmd, "\n;|><&") { 110 return false 111 } 112 113 return true 114 } 115 116 func validate(name string, conf common.Conf, renderer shell.Renderer) error { 117 if name == "" { 118 return errors.NotValidf("missing service name") 119 } 120 121 if err := conf.Validate(renderer); err != nil { 122 return errors.Trace(err) 123 } 124 125 if conf.ExtraScript != "" { 126 return errors.NotValidf("unexpected ExtraScript") 127 } 128 129 // We ignore Desc and Logfile. 130 131 for k := range conf.Limit { 132 if _, ok := limitMap[k]; !ok { 133 return errors.NotValidf("conf.Limit key %q", k) 134 } 135 } 136 137 return nil 138 } 139 140 // serialize returns the data that should be written to disk for the 141 // provided Conf, rendered in the systemd unit file format. 142 func serialize(name string, conf common.Conf, renderer shell.Renderer) ([]byte, error) { 143 if err := validate(name, conf, renderer); err != nil { 144 return nil, errors.Trace(err) 145 } 146 147 var unitOptions []*unit.UnitOption 148 unitOptions = append(unitOptions, serializeUnit(conf)...) 149 unitOptions = append(unitOptions, serializeService(conf)...) 150 unitOptions = append(unitOptions, serializeInstall(conf)...) 151 // Don't use unit.Serialize because it has map ordering issues. 152 // Serialize copied locally, and outputs sections in alphabetical order. 153 data, err := ioutil.ReadAll(UnitSerialize(unitOptions)) 154 return data, errors.Trace(err) 155 } 156 157 func serializeUnit(conf common.Conf) []*unit.UnitOption { 158 var unitOptions []*unit.UnitOption 159 160 if conf.Desc != "" { 161 unitOptions = append(unitOptions, &unit.UnitOption{ 162 Section: "Unit", 163 Name: "Description", 164 Value: conf.Desc, 165 }) 166 } 167 168 after := []string{ 169 "syslog.target", 170 "network.target", 171 "systemd-user-sessions.service", 172 } 173 for _, name := range after { 174 unitOptions = append(unitOptions, &unit.UnitOption{ 175 Section: "Unit", 176 Name: "After", 177 Value: name, 178 }) 179 } 180 181 if conf.AfterStopped != "" { 182 unitOptions = append(unitOptions, &unit.UnitOption{ 183 Section: "Unit", 184 Name: "After", 185 Value: conf.AfterStopped, 186 }) 187 } 188 189 return unitOptions 190 } 191 192 func serializeService(conf common.Conf) []*unit.UnitOption { 193 var unitOptions []*unit.UnitOption 194 195 // TODO(ericsnow) Support "Type" (e.g. "forking")? For now we just 196 // use the default, "simple". 197 198 for k, v := range conf.Env { 199 unitOptions = append(unitOptions, &unit.UnitOption{ 200 Section: "Service", 201 Name: "Environment", 202 Value: fmt.Sprintf(`"%s=%s"`, k, v), 203 }) 204 } 205 206 for k, v := range conf.Limit { 207 unitOptions = append(unitOptions, &unit.UnitOption{ 208 Section: "Service", 209 Name: limitMap[k], 210 Value: strconv.Itoa(v), 211 }) 212 } 213 214 if conf.ExecStart != "" { 215 unitOptions = append(unitOptions, &unit.UnitOption{ 216 Section: "Service", 217 Name: "ExecStart", 218 Value: conf.ExecStart, 219 }) 220 } 221 222 // TODO(ericsnow) This should key off Conf.Restart, once added. 223 if !conf.Transient { 224 unitOptions = append(unitOptions, &unit.UnitOption{ 225 Section: "Service", 226 Name: "Restart", 227 Value: "on-failure", 228 }) 229 } 230 231 if conf.Timeout > 0 { 232 unitOptions = append(unitOptions, &unit.UnitOption{ 233 Section: "Service", 234 Name: "TimeoutSec", 235 Value: strconv.Itoa(conf.Timeout), 236 }) 237 } 238 239 if conf.ExecStopPost != "" { 240 unitOptions = append(unitOptions, &unit.UnitOption{ 241 Section: "Service", 242 Name: "ExecStopPost", 243 Value: conf.ExecStopPost, 244 }) 245 } 246 247 return unitOptions 248 } 249 250 func serializeInstall(conf common.Conf) []*unit.UnitOption { 251 var unitOptions []*unit.UnitOption 252 253 unitOptions = append(unitOptions, &unit.UnitOption{ 254 Section: "Install", 255 Name: "WantedBy", 256 Value: "multi-user.target", 257 }) 258 259 return unitOptions 260 } 261 262 // deserialize parses the provided data (in the systemd unit file 263 // format) and populates a new Conf with the result. 264 func deserialize(data []byte, renderer shell.Renderer) (common.Conf, error) { 265 opts, err := unit.Deserialize(bytes.NewBuffer(data)) 266 if err != nil { 267 return common.Conf{}, errors.Trace(err) 268 } 269 return deserializeOptions(opts, renderer) 270 } 271 272 func deserializeOptions(opts []*unit.UnitOption, renderer shell.Renderer) (common.Conf, error) { 273 var conf common.Conf 274 275 for _, uo := range opts { 276 switch uo.Section { 277 case "Unit": 278 switch uo.Name { 279 case "Description": 280 conf.Desc = uo.Value 281 case "After": 282 // Do nothing until we support it in common.Conf. 283 default: 284 return conf, errors.NotSupportedf("Unit directive %q", uo.Name) 285 } 286 case "Service": 287 switch { 288 case uo.Name == "ExecStart": 289 conf.ExecStart = uo.Value 290 case uo.Name == "Environment": 291 if conf.Env == nil { 292 conf.Env = make(map[string]string) 293 } 294 var value = uo.Value 295 if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { 296 value = value[1 : len(value)-1] 297 } 298 parts := strings.SplitN(value, "=", 2) 299 if len(parts) != 2 { 300 return conf, errors.NotValidf("service environment value %q", uo.Value) 301 } 302 conf.Env[parts[0]] = parts[1] 303 case strings.HasPrefix(uo.Name, "Limit"): 304 if conf.Limit == nil { 305 conf.Limit = make(map[string]int) 306 } 307 for k, v := range limitMap { 308 if v == uo.Name { 309 n, err := strconv.Atoi(uo.Value) 310 if err != nil { 311 return conf, errors.Trace(err) 312 } 313 conf.Limit[k] = n 314 break 315 } 316 } 317 case uo.Name == "TimeoutSec": 318 timeout, err := strconv.Atoi(uo.Value) 319 if err != nil { 320 return conf, errors.Trace(err) 321 } 322 conf.Timeout = timeout 323 case uo.Name == "Type": 324 // Do nothing until we support it in common.Conf. 325 case uo.Name == "RemainAfterExit": 326 // Do nothing until we support it in common.Conf. 327 case uo.Name == "Restart": 328 // Do nothing until we support it in common.Conf. 329 default: 330 return conf, errors.NotSupportedf("Service directive %q", uo.Name) 331 } 332 case "Install": 333 switch uo.Name { 334 case "WantedBy": 335 if uo.Value != "multi-user.target" { 336 return conf, errors.NotValidf("unit target %q", uo.Value) 337 } 338 default: 339 return conf, errors.NotSupportedf("Install directive %q", uo.Name) 340 } 341 default: 342 return conf, errors.NotSupportedf("section %q", uo.Name) 343 } 344 } 345 346 err := validate("<>", conf, renderer) 347 return conf, errors.Trace(err) 348 } 349 350 // CleanShutdownService is added to machines to ensure DHCP-assigned 351 // IP addresses are released on shutdown, reboot, or halt. See bug 352 // http://pad.lv/1348663 for more info. 353 const CleanShutdownService = ` 354 [Unit] 355 Description=Stop all network interfaces on shutdown 356 DefaultDependencies=false 357 After=final.target 358 359 [Service] 360 Type=oneshot 361 ExecStart=/sbin/ifdown -a -v --force 362 StandardOutput=tty 363 StandardError=tty 364 365 [Install] 366 WantedBy=final.target 367 ` 368 369 // CleanShutdownServicePath is the full file path where 370 // CleanShutdownService is created. 371 const CleanShutdownServicePath = "/etc/systemd/system/juju-clean-shutdown.service"