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