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