github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/cloudconfig/containerinit/container_userdata.go (about)

     1  // Copyright 2013, 2015 Canonical Ltd.
     2  // Copyright 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package containerinit
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"io/ioutil"
    11  	"path/filepath"
    12  	"strings"
    13  	"text/template"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/utils/proxy"
    18  
    19  	"github.com/juju/juju/cloudconfig"
    20  	"github.com/juju/juju/cloudconfig/cloudinit"
    21  	"github.com/juju/juju/cloudconfig/instancecfg"
    22  	"github.com/juju/juju/container"
    23  	"github.com/juju/juju/environs"
    24  	"github.com/juju/juju/network"
    25  	"github.com/juju/juju/service"
    26  	"github.com/juju/juju/service/common"
    27  )
    28  
    29  var (
    30  	logger = loggo.GetLogger("juju.cloudconfig.containerinit")
    31  )
    32  
    33  // WriteUserData generates the cloud-init user-data using the
    34  // specified machine and network config for a container, and writes
    35  // the serialized form out to a cloud-init file in the directory
    36  // specified.
    37  func WriteUserData(
    38  	instanceConfig *instancecfg.InstanceConfig,
    39  	networkConfig *container.NetworkConfig,
    40  	directory string,
    41  ) (string, error) {
    42  	userData, err := CloudInitUserData(instanceConfig, networkConfig)
    43  	if err != nil {
    44  		logger.Errorf("failed to create user data: %v", err)
    45  		return "", err
    46  	}
    47  	return WriteCloudInitFile(directory, userData)
    48  }
    49  
    50  // WriteCloudInitFile writes the data out to a cloud-init file in the
    51  // directory specified, and returns the filename.
    52  func WriteCloudInitFile(directory string, userData []byte) (string, error) {
    53  	userDataFilename := filepath.Join(directory, "cloud-init")
    54  	if err := ioutil.WriteFile(userDataFilename, userData, 0644); err != nil {
    55  		logger.Errorf("failed to write user data: %v", err)
    56  		return "", err
    57  	}
    58  	return userDataFilename, nil
    59  }
    60  
    61  // networkConfigTemplate defines how to render /etc/network/interfaces
    62  // file for a container with one or more NICs.
    63  const networkConfigTemplate = `
    64  # loopback interface
    65  auto lo
    66  iface lo inet loopback{{define "static"}}
    67  {{.InterfaceName | printf "# interface %q"}}{{if not .NoAutoStart}}
    68  auto {{.InterfaceName}}{{end}}
    69  iface {{.InterfaceName}} inet manual{{if gt (len .DNSServers) 0}}
    70      dns-nameservers{{range $dns := .DNSServers}} {{$dns.Value}}{{end}}{{end}}{{if gt (len .DNSSearch) 0}}
    71      dns-search {{.DNSSearch}}{{end}}
    72      pre-up ip address add {{.Address.Value}}/32 dev {{.InterfaceName}} &> /dev/null || true
    73      up ip route replace {{.GatewayAddress.Value}} dev {{.InterfaceName}}
    74      up ip route replace default via {{.GatewayAddress.Value}}
    75      down ip route del default via {{.GatewayAddress.Value}} &> /dev/null || true
    76      down ip route del {{.GatewayAddress.Value}} dev {{.InterfaceName}} &> /dev/null || true
    77      post-down ip address del {{.Address.Value}}/32 dev {{.InterfaceName}} &> /dev/null || true
    78  {{end}}{{define "dhcp"}}
    79  {{.InterfaceName | printf "# interface %q"}}{{if not .NoAutoStart}}
    80  auto {{.InterfaceName}}{{end}}
    81  iface {{.InterfaceName}} inet dhcp
    82  {{end}}{{range $nic := . }}{{if eq $nic.ConfigType "static"}}
    83  {{template "static" $nic}}{{else}}{{template "dhcp" $nic}}{{end}}{{end}}`
    84  
    85  // multiBridgeNetworkConfigTemplate defines how to render /etc/network/interfaces
    86  // file for a multi-NIC container.
    87  const multiBridgeNetworkConfigTemplate = `
    88  auto lo
    89  iface lo inet loopback
    90  {{range $nic := .}}{{template "single" $nic}}{{end}}
    91  {{define "single"}}{{if not .NoAutoStart}}
    92  auto {{.InterfaceName}}{{end}}
    93  iface {{.InterfaceName}} inet manual{{if .DNSServers}}
    94    dns-nameservers{{range $srv := .DNSServers}} {{$srv.Value}}{{end}}{{end}}{{if .DNSSearchDomains}}
    95    dns-search{{range $dom := .DNSSearchDomains}} {{$dom}}{{end}}{{end}}
    96    pre-up ip address add {{.CIDRAddress}} dev {{.InterfaceName}} || true
    97    up ip route replace {{.CIDR}} dev {{.InterfaceName}} || true
    98    down ip route del {{.CIDR}} dev {{.InterfaceName}} || true
    99    post-down address del {{.CIDRAddress}} dev {{.InterfaceName}} || true{{if .GatewayAddress.Value}}
   100    up ip route replace default via {{.GatewayAddress.Value}} || true
   101    down ip route del default via {{.GatewayAddress.Value}} || true{{end}}
   102  {{end}}`
   103  
   104  var networkInterfacesFile = "/etc/network/interfaces.d/00-juju.cfg"
   105  
   106  // GenerateNetworkConfig renders a network config for one or more
   107  // network interfaces, using the given non-nil networkConfig
   108  // containing a non-empty Interfaces field.
   109  func GenerateNetworkConfig(networkConfig *container.NetworkConfig) (string, error) {
   110  	if networkConfig == nil || len(networkConfig.Interfaces) == 0 {
   111  		// Don't generate networking config.
   112  		logger.Tracef("no network config to generate")
   113  		return "", nil
   114  	}
   115  	logger.Debugf("generating network config from %#v", *networkConfig)
   116  
   117  	// Copy the InterfaceInfo before modifying the original.
   118  	interfacesCopy := make([]network.InterfaceInfo, len(networkConfig.Interfaces))
   119  	copy(interfacesCopy, networkConfig.Interfaces)
   120  	for i, info := range interfacesCopy {
   121  		if info.MACAddress != "" {
   122  			info.MACAddress = ""
   123  		}
   124  		if info.InterfaceName != "eth0" {
   125  			info.GatewayAddress = network.Address{}
   126  		}
   127  		interfacesCopy[i] = info
   128  	}
   129  
   130  	// Render the config first.
   131  	tmpl, err := template.New("config").Parse(multiBridgeNetworkConfigTemplate)
   132  	if err != nil {
   133  		return "", errors.Annotate(err, "cannot parse network config template")
   134  	}
   135  
   136  	var buf bytes.Buffer
   137  	if err := tmpl.Execute(&buf, interfacesCopy); err != nil {
   138  		return "", errors.Annotate(err, "cannot render network config")
   139  	}
   140  
   141  	generatedConfig := buf.String()
   142  	logger.Debugf("generated network config from %#v\nusing%#v:\n%s", interfacesCopy, networkConfig.Interfaces, generatedConfig)
   143  
   144  	return generatedConfig, nil
   145  }
   146  
   147  // newCloudInitConfigWithNetworks creates a cloud-init config which
   148  // might include per-interface networking config if both networkConfig
   149  // is not nil and its Interfaces field is not empty.
   150  func newCloudInitConfigWithNetworks(series string, networkConfig *container.NetworkConfig) (cloudinit.CloudConfig, error) {
   151  	cloudConfig, err := cloudinit.New(series)
   152  	if err != nil {
   153  		return nil, errors.Trace(err)
   154  	}
   155  	config, err := GenerateNetworkConfig(networkConfig)
   156  	if err != nil || len(config) == 0 {
   157  		return cloudConfig, errors.Trace(err)
   158  	}
   159  
   160  	cloudConfig.AddBootTextFile(networkInterfacesFile, config, 0644)
   161  	cloudConfig.AddRunCmd("ifup -a || true")
   162  	return cloudConfig, nil
   163  }
   164  
   165  func CloudInitUserData(
   166  	instanceConfig *instancecfg.InstanceConfig,
   167  	networkConfig *container.NetworkConfig,
   168  ) ([]byte, error) {
   169  	cloudConfig, err := newCloudInitConfigWithNetworks(instanceConfig.Series, networkConfig)
   170  	if err != nil {
   171  		return nil, errors.Trace(err)
   172  	}
   173  	udata, err := cloudconfig.NewUserdataConfig(instanceConfig, cloudConfig)
   174  	if err != nil {
   175  		return nil, errors.Trace(err)
   176  	}
   177  	if err = udata.Configure(); err != nil {
   178  		return nil, errors.Trace(err)
   179  	}
   180  	// Run ifconfig to get the addresses of the internal container at least
   181  	// logged in the host.
   182  	cloudConfig.AddRunCmd("ifconfig")
   183  
   184  	if instanceConfig.MachineContainerHostname != "" {
   185  		cloudConfig.SetAttr("hostname", instanceConfig.MachineContainerHostname)
   186  	}
   187  
   188  	data, err := cloudConfig.RenderYAML()
   189  	if err != nil {
   190  		return nil, errors.Trace(err)
   191  	}
   192  	return data, nil
   193  }
   194  
   195  // TemplateUserData returns a minimal user data necessary for the template.
   196  // This should have the authorized keys, base packages, the cloud archive if
   197  // necessary,  initial apt proxy config, and it should do the apt-get
   198  // update/upgrade initially.
   199  func TemplateUserData(
   200  	series string,
   201  	authorizedKeys string,
   202  	aptProxy proxy.Settings,
   203  	aptMirror string,
   204  	enablePackageUpdates bool,
   205  	enableOSUpgrades bool,
   206  	networkConfig *container.NetworkConfig,
   207  ) ([]byte, error) {
   208  	var config cloudinit.CloudConfig
   209  	var err error
   210  	if networkConfig != nil {
   211  		config, err = newCloudInitConfigWithNetworks(series, networkConfig)
   212  		if err != nil {
   213  			return nil, errors.Trace(err)
   214  		}
   215  	} else {
   216  		config, err = cloudinit.New(series)
   217  		if err != nil {
   218  			return nil, errors.Trace(err)
   219  		}
   220  	}
   221  	cloudconfig.SetUbuntuUser(config, authorizedKeys)
   222  	config.AddScripts(
   223  		"set -xe", // ensure we run all the scripts or abort.
   224  	)
   225  	// For LTS series which need support for the cloud-tools archive,
   226  	// we need to enable apt-get update regardless of the environ
   227  	// setting, otherwise provisioning will fail.
   228  	if series == "precise" && !enablePackageUpdates {
   229  		logger.Warningf("series %q requires cloud-tools archive: enabling updates", series)
   230  		enablePackageUpdates = true
   231  	}
   232  
   233  	if enablePackageUpdates && config.RequiresCloudArchiveCloudTools() {
   234  		config.AddCloudArchiveCloudTools()
   235  	}
   236  	config.AddPackageCommands(aptProxy, aptMirror, enablePackageUpdates, enableOSUpgrades)
   237  
   238  	initSystem, err := service.VersionInitSystem(series)
   239  	if err != nil {
   240  		return nil, errors.Trace(err)
   241  	}
   242  	cmds, err := shutdownInitCommands(initSystem, series)
   243  	if err != nil {
   244  		return nil, errors.Trace(err)
   245  	}
   246  	config.AddScripts(strings.Join(cmds, "\n"))
   247  
   248  	data, err := config.RenderYAML()
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  	return data, nil
   253  }
   254  
   255  // defaultEtcNetworkInterfaces is the contents of
   256  // /etc/network/interfaces file which is left on the template LXC
   257  // container on shutdown. This is needed to allow cloned containers to
   258  // start in case no network config is provided during cloud-init, e.g.
   259  // when AUFS is used.
   260  const defaultEtcNetworkInterfaces = `
   261  # loopback interface
   262  auto lo
   263  iface lo inet loopback
   264  
   265  # primary interface
   266  auto eth0
   267  iface eth0 inet dhcp
   268  `
   269  
   270  func shutdownInitCommands(initSystem, series string) ([]string, error) {
   271  	// These files are removed just before the template shuts down.
   272  	cleanupOnShutdown := []string{
   273  		// We remove any dhclient lease files so there's no chance a
   274  		// clone to reuse a lease from the template it was cloned
   275  		// from.
   276  		"/var/lib/dhcp/dhclient*",
   277  		// Both of these sets of files below are recreated on boot and
   278  		// if we leave them in the template's rootfs boot logs coming
   279  		// from cloned containers will be appended. It's better to
   280  		// keep clean logs for diagnosing issues / debugging.
   281  		"/var/log/cloud-init*.log",
   282  	}
   283  
   284  	// Using EOC below as the template shutdown script is itself
   285  	// passed through cat > ... < EOF.
   286  	replaceNetConfCmd := fmt.Sprintf(
   287  		"/bin/cat > /etc/network/interfaces << EOC%sEOC\n  ",
   288  		defaultEtcNetworkInterfaces,
   289  	)
   290  	paths := strings.Join(cleanupOnShutdown, " ")
   291  	removeCmd := fmt.Sprintf("/bin/rm -fr %s\n  ", paths)
   292  	shutdownCmd := "/sbin/shutdown -h now"
   293  	name := "juju-template-restart"
   294  	desc := "juju shutdown job"
   295  
   296  	execStart := shutdownCmd
   297  	if environs.AddressAllocationEnabled("") { // we only care the provider is not MAAS here.
   298  		// Only do the cleanup and replacement of /e/n/i when address
   299  		// allocation feature flag is enabled.
   300  		execStart = replaceNetConfCmd + removeCmd + shutdownCmd
   301  	}
   302  
   303  	conf := common.Conf{
   304  		Desc:         desc,
   305  		Transient:    true,
   306  		AfterStopped: "cloud-final",
   307  		ExecStart:    execStart,
   308  	}
   309  	// systemd uses targets for synchronization of services
   310  	if initSystem == service.InitSystemSystemd {
   311  		conf.AfterStopped = "cloud-config.target"
   312  	}
   313  
   314  	svc, err := service.NewService(name, conf, series)
   315  	if err != nil {
   316  		return nil, errors.Trace(err)
   317  	}
   318  
   319  	cmds, err := svc.InstallCommands()
   320  	if err != nil {
   321  		return nil, errors.Trace(err)
   322  	}
   323  
   324  	startCommands, err := svc.StartCommands()
   325  	if err != nil {
   326  		return nil, errors.Trace(err)
   327  	}
   328  	cmds = append(cmds, startCommands...)
   329  
   330  	return cmds, nil
   331  }