github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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  	"net"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/utils/proxy"
    18  	"github.com/juju/utils/set"
    19  
    20  	"github.com/juju/juju/cloudconfig"
    21  	"github.com/juju/juju/cloudconfig/cloudinit"
    22  	"github.com/juju/juju/cloudconfig/instancecfg"
    23  	"github.com/juju/juju/container"
    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  var (
    62  	systemNetworkInterfacesFile = "/etc/network/interfaces"
    63  	networkInterfacesFile       = systemNetworkInterfacesFile + "-juju"
    64  )
    65  
    66  // GenerateNetworkConfig renders a network config for one or more network
    67  // interfaces, using the given non-nil networkConfig containing a non-empty
    68  // Interfaces field.
    69  func GenerateNetworkConfig(networkConfig *container.NetworkConfig) (string, error) {
    70  	if networkConfig == nil || len(networkConfig.Interfaces) == 0 {
    71  		return "", errors.Errorf("missing container network config")
    72  	}
    73  	logger.Debugf("generating network config from %#v", *networkConfig)
    74  
    75  	prepared := PrepareNetworkConfigFromInterfaces(networkConfig.Interfaces)
    76  
    77  	var output bytes.Buffer
    78  	gatewayHandled := false
    79  	for _, name := range prepared.InterfaceNames {
    80  		output.WriteString("\n")
    81  		if name == "lo" {
    82  			output.WriteString("auto ")
    83  			autoStarted := strings.Join(prepared.AutoStarted, " ")
    84  			output.WriteString(autoStarted + "\n\n")
    85  			output.WriteString("iface lo inet loopback\n")
    86  
    87  			dnsServers := strings.Join(prepared.DNSServers, " ")
    88  			if dnsServers != "" {
    89  				output.WriteString("  dns-nameservers ")
    90  				output.WriteString(dnsServers + "\n")
    91  			}
    92  
    93  			dnsSearchDomains := strings.Join(prepared.DNSSearchDomains, " ")
    94  			if dnsSearchDomains != "" {
    95  				output.WriteString("  dns-search ")
    96  				output.WriteString(dnsSearchDomains + "\n")
    97  			}
    98  			continue
    99  		}
   100  
   101  		address, hasAddress := prepared.NameToAddress[name]
   102  		if !hasAddress {
   103  			output.WriteString("iface " + name + " inet manual\n")
   104  			continue
   105  		} else if address == string(network.ConfigDHCP) {
   106  			output.WriteString("iface " + name + " inet dhcp\n")
   107  			// We're expecting to get a default gateway
   108  			// from the DHCP lease.
   109  			gatewayHandled = true
   110  			continue
   111  		}
   112  
   113  		output.WriteString("iface " + name + " inet static\n")
   114  		output.WriteString("  address " + address + "\n")
   115  		if !gatewayHandled && prepared.GatewayAddress != "" {
   116  			_, network, err := net.ParseCIDR(address)
   117  			if err != nil {
   118  				return "", errors.Annotatef(err, "invalid gateway for interface %q with address %q", name, address)
   119  			}
   120  
   121  			gatewayIP := net.ParseIP(prepared.GatewayAddress)
   122  			if network.Contains(gatewayIP) {
   123  				output.WriteString("  gateway " + prepared.GatewayAddress + "\n")
   124  				gatewayHandled = true // write it only once
   125  			}
   126  		}
   127  	}
   128  
   129  	generatedConfig := output.String()
   130  	logger.Debugf("generated network config:\n%s", generatedConfig)
   131  
   132  	if !gatewayHandled {
   133  		logger.Infof("generated network config has no gateway")
   134  	}
   135  
   136  	return generatedConfig, nil
   137  }
   138  
   139  // PreparedConfig holds all the necessary information to render a persistent
   140  // network config to a file.
   141  type PreparedConfig struct {
   142  	InterfaceNames   []string
   143  	AutoStarted      []string
   144  	DNSServers       []string
   145  	DNSSearchDomains []string
   146  	NameToAddress    map[string]string
   147  	GatewayAddress   string
   148  }
   149  
   150  // PrepareNetworkConfigFromInterfaces collects the necessary information to
   151  // render a persistent network config from the given slice of
   152  // network.InterfaceInfo. The result always includes the loopback interface.
   153  func PrepareNetworkConfigFromInterfaces(interfaces []network.InterfaceInfo) *PreparedConfig {
   154  	dnsServers := set.NewStrings()
   155  	dnsSearchDomains := set.NewStrings()
   156  	gatewayAddress := ""
   157  	namesInOrder := make([]string, 1, len(interfaces)+1)
   158  	nameToAddress := make(map[string]string)
   159  
   160  	// Always include the loopback.
   161  	namesInOrder[0] = "lo"
   162  	autoStarted := set.NewStrings("lo")
   163  
   164  	for _, info := range interfaces {
   165  		if !info.NoAutoStart {
   166  			autoStarted.Add(info.InterfaceName)
   167  		}
   168  
   169  		if cidr := info.CIDRAddress(); cidr != "" {
   170  			nameToAddress[info.InterfaceName] = cidr
   171  		} else if info.ConfigType == network.ConfigDHCP {
   172  			nameToAddress[info.InterfaceName] = string(network.ConfigDHCP)
   173  		}
   174  
   175  		for _, dns := range info.DNSServers {
   176  			dnsServers.Add(dns.Value)
   177  		}
   178  
   179  		dnsSearchDomains = dnsSearchDomains.Union(set.NewStrings(info.DNSSearchDomains...))
   180  
   181  		if gatewayAddress == "" && info.GatewayAddress.Value != "" {
   182  			gatewayAddress = info.GatewayAddress.Value
   183  		}
   184  
   185  		namesInOrder = append(namesInOrder, info.InterfaceName)
   186  	}
   187  
   188  	prepared := &PreparedConfig{
   189  		InterfaceNames:   namesInOrder,
   190  		NameToAddress:    nameToAddress,
   191  		AutoStarted:      autoStarted.SortedValues(),
   192  		DNSServers:       dnsServers.SortedValues(),
   193  		DNSSearchDomains: dnsSearchDomains.SortedValues(),
   194  		GatewayAddress:   gatewayAddress,
   195  	}
   196  
   197  	logger.Debugf("prepared network config for rendering: %+v", prepared)
   198  	return prepared
   199  }
   200  
   201  // newCloudInitConfigWithNetworks creates a cloud-init config which
   202  // might include per-interface networking config if both networkConfig
   203  // is not nil and its Interfaces field is not empty.
   204  func newCloudInitConfigWithNetworks(series string, networkConfig *container.NetworkConfig) (cloudinit.CloudConfig, error) {
   205  	cloudConfig, err := cloudinit.New(series)
   206  	if err != nil {
   207  		return nil, errors.Trace(err)
   208  	}
   209  
   210  	if networkConfig != nil {
   211  		config, err := GenerateNetworkConfig(networkConfig)
   212  		if err != nil {
   213  			return nil, errors.Trace(err)
   214  		}
   215  		cloudConfig.AddBootTextFile(networkInterfacesFile, config, 0644)
   216  		cloudConfig.AddRunCmd(raiseJujuNetworkInterfacesScript(systemNetworkInterfacesFile, networkInterfacesFile))
   217  	}
   218  
   219  	return cloudConfig, nil
   220  }
   221  
   222  func CloudInitUserData(
   223  	instanceConfig *instancecfg.InstanceConfig,
   224  	networkConfig *container.NetworkConfig,
   225  ) ([]byte, error) {
   226  	cloudConfig, err := newCloudInitConfigWithNetworks(instanceConfig.Series, networkConfig)
   227  	if err != nil {
   228  		return nil, errors.Trace(err)
   229  	}
   230  	udata, err := cloudconfig.NewUserdataConfig(instanceConfig, cloudConfig)
   231  	if err != nil {
   232  		return nil, errors.Trace(err)
   233  	}
   234  	if err = udata.Configure(); err != nil {
   235  		return nil, errors.Trace(err)
   236  	}
   237  	// Run ifconfig to get the addresses of the internal container at least
   238  	// logged in the host.
   239  	cloudConfig.AddRunCmd("ifconfig")
   240  
   241  	if instanceConfig.MachineContainerHostname != "" {
   242  		logger.Debugf("Cloud-init configured to set hostname")
   243  		cloudConfig.SetAttr("hostname", instanceConfig.MachineContainerHostname)
   244  	}
   245  
   246  	cloudConfig.SetAttr("manage_etc_hosts", true)
   247  
   248  	data, err := cloudConfig.RenderYAML()
   249  	if err != nil {
   250  		return nil, errors.Trace(err)
   251  	}
   252  	return data, nil
   253  }
   254  
   255  // TemplateUserData returns a minimal user data necessary for the template.
   256  // This should have the authorized keys, base packages, the cloud archive if
   257  // necessary,  initial apt proxy config, and it should do the apt-get
   258  // update/upgrade initially.
   259  func TemplateUserData(
   260  	series string,
   261  	authorizedKeys string,
   262  	aptProxy proxy.Settings,
   263  	aptMirror string,
   264  	enablePackageUpdates bool,
   265  	enableOSUpgrades bool,
   266  	networkConfig *container.NetworkConfig,
   267  ) ([]byte, error) {
   268  	var config cloudinit.CloudConfig
   269  	var err error
   270  	if networkConfig != nil {
   271  		config, err = newCloudInitConfigWithNetworks(series, networkConfig)
   272  		if err != nil {
   273  			return nil, errors.Trace(err)
   274  		}
   275  	} else {
   276  		config, err = cloudinit.New(series)
   277  		if err != nil {
   278  			return nil, errors.Trace(err)
   279  		}
   280  	}
   281  	cloudconfig.SetUbuntuUser(config, authorizedKeys)
   282  	config.AddScripts(
   283  		"set -xe", // ensure we run all the scripts or abort.
   284  	)
   285  	// For LTS series which need support for the cloud-tools archive,
   286  	// we need to enable apt-get update regardless of the environ
   287  	// setting, otherwise provisioning will fail.
   288  	if series == "precise" && !enablePackageUpdates {
   289  		logger.Infof("series %q requires cloud-tools archive: enabling updates", series)
   290  		enablePackageUpdates = true
   291  	}
   292  
   293  	if enablePackageUpdates && config.RequiresCloudArchiveCloudTools() {
   294  		config.AddCloudArchiveCloudTools()
   295  	}
   296  	config.AddPackageCommands(aptProxy, aptMirror, enablePackageUpdates, enableOSUpgrades)
   297  
   298  	initSystem, err := service.VersionInitSystem(series)
   299  	if err != nil {
   300  		return nil, errors.Trace(err)
   301  	}
   302  	cmds, err := shutdownInitCommands(initSystem, series)
   303  	if err != nil {
   304  		return nil, errors.Trace(err)
   305  	}
   306  	config.AddScripts(strings.Join(cmds, "\n"))
   307  
   308  	data, err := config.RenderYAML()
   309  	if err != nil {
   310  		return nil, err
   311  	}
   312  	return data, nil
   313  }
   314  
   315  func shutdownInitCommands(initSystem, series string) ([]string, error) {
   316  	shutdownCmd := "/sbin/shutdown -h now"
   317  	name := "juju-template-restart"
   318  	desc := "juju shutdown job"
   319  
   320  	execStart := shutdownCmd
   321  
   322  	conf := common.Conf{
   323  		Desc:         desc,
   324  		Transient:    true,
   325  		AfterStopped: "cloud-final",
   326  		ExecStart:    execStart,
   327  	}
   328  	// systemd uses targets for synchronization of services
   329  	if initSystem == service.InitSystemSystemd {
   330  		conf.AfterStopped = "cloud-config.target"
   331  	}
   332  
   333  	svc, err := service.NewService(name, conf, series)
   334  	if err != nil {
   335  		return nil, errors.Trace(err)
   336  	}
   337  
   338  	cmds, err := svc.InstallCommands()
   339  	if err != nil {
   340  		return nil, errors.Trace(err)
   341  	}
   342  
   343  	startCommands, err := svc.StartCommands()
   344  	if err != nil {
   345  		return nil, errors.Trace(err)
   346  	}
   347  	cmds = append(cmds, startCommands...)
   348  
   349  	return cmds, nil
   350  }
   351  
   352  // raiseJujuNetworkInterfacesScript returns a cloud-init script to
   353  // raise Juju's network interfaces supplied via cloud-init.
   354  //
   355  // Note: we sleep to mitigate against LP #1337873 and LP #1269921.
   356  func raiseJujuNetworkInterfacesScript(oldInterfacesFile, newInterfacesFile string) string {
   357  	return fmt.Sprintf(`
   358  if [ -f %[2]s ]; then
   359      ifdown -a
   360      sleep 1.5
   361      if ifup -a --interfaces=%[2]s; then
   362          cp %[1]s %[1]s-orig
   363          cp %[2]s %[1]s
   364      else
   365          ifup -a
   366      fi
   367  fi`[1:],
   368  		oldInterfacesFile, newInterfacesFile)
   369  }