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"