github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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"