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