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"