github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cloudconfig/cloudinit/network_ubuntu.go (about)

     1  // Copyright 2013, 2015, 2018 Canonical Ltd.
     2  // Copyright 2015 Cloudbase Solutions SRL
     3  // Licensed under the AGPLv3, see LICENCE file for details.
     4  
     5  package cloudinit
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"net"
    11  	"strings"
    12  
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  
    17  	"github.com/juju/juju/network"
    18  	"github.com/juju/juju/network/netplan"
    19  )
    20  
    21  var logger = loggo.GetLogger("juju.cloudconfig.cloudinit")
    22  
    23  var (
    24  	systemNetworkInterfacesFile = "/etc/network/interfaces"
    25  	networkInterfacesFile       = systemNetworkInterfacesFile + "-juju"
    26  	jujuNetplanFile             = "/etc/netplan/99-juju.yaml"
    27  )
    28  
    29  // GenerateENITemplate renders an e/n/i template config for one or more network
    30  // interfaces, using the given non-empty interfaces list.
    31  func GenerateENITemplate(interfaces []network.InterfaceInfo) (string, error) {
    32  	if len(interfaces) == 0 {
    33  		return "", errors.Errorf("missing container network config")
    34  	}
    35  	logger.Debugf("generating /e/n/i template from %#v", interfaces)
    36  
    37  	prepared := PrepareNetworkConfigFromInterfaces(interfaces)
    38  
    39  	var output bytes.Buffer
    40  	gateway4Handled := false
    41  	gateway6Handled := false
    42  	hasV4Interface := false
    43  	hasV6Interface := false
    44  	for _, name := range prepared.InterfaceNames {
    45  		output.WriteString("\n")
    46  		if name == "lo" {
    47  			output.WriteString("auto ")
    48  			autoStarted := strings.Join(prepared.AutoStarted, " ")
    49  			output.WriteString(autoStarted + "\n\n")
    50  			output.WriteString("iface lo inet loopback\n")
    51  
    52  			dnsServers := strings.Join(prepared.DNSServers, " ")
    53  			if dnsServers != "" {
    54  				output.WriteString("  dns-nameservers ")
    55  				output.WriteString(dnsServers + "\n")
    56  			}
    57  
    58  			dnsSearchDomains := strings.Join(prepared.DNSSearchDomains, " ")
    59  			if dnsSearchDomains != "" {
    60  				output.WriteString("  dns-search ")
    61  				output.WriteString(dnsSearchDomains + "\n")
    62  			}
    63  			continue
    64  		}
    65  
    66  		address, hasAddress := prepared.NameToAddress[name]
    67  		if !hasAddress {
    68  			output.WriteString("iface " + name + " inet manual\n")
    69  			continue
    70  		} else if address == string(network.ConfigDHCP) {
    71  			output.WriteString("iface " + name + " inet dhcp\n")
    72  			// We're expecting to get a default gateway
    73  			// from the DHCP lease.
    74  			gateway4Handled = true
    75  			continue
    76  		}
    77  
    78  		_, network, err := net.ParseCIDR(address)
    79  		if err != nil {
    80  			return "", errors.Annotatef(err, "invalid address for interface %q: %q", name, address)
    81  		}
    82  
    83  		isIpv4 := network.IP.To4() != nil
    84  
    85  		if isIpv4 {
    86  			output.WriteString("iface " + name + " inet static\n")
    87  			hasV4Interface = true
    88  		} else {
    89  			output.WriteString("iface " + name + " inet6 static\n")
    90  			hasV6Interface = true
    91  		}
    92  		output.WriteString("  address " + address + "\n")
    93  
    94  		if isIpv4 {
    95  			if !gateway4Handled && prepared.Gateway4Address != "" {
    96  				gatewayIP := net.ParseIP(prepared.Gateway4Address)
    97  				if network.Contains(gatewayIP) {
    98  					output.WriteString("  gateway " + prepared.Gateway4Address + "\n")
    99  					gateway4Handled = true // write it only once
   100  				}
   101  			}
   102  		} else {
   103  			if !gateway6Handled && prepared.Gateway6Address != "" {
   104  				gatewayIP := net.ParseIP(prepared.Gateway6Address)
   105  				if network.Contains(gatewayIP) {
   106  					output.WriteString("  gateway " + prepared.Gateway6Address + "\n")
   107  					gateway4Handled = true // write it only once
   108  				}
   109  			}
   110  		}
   111  
   112  		if mtu, ok := prepared.NameToMTU[name]; ok {
   113  			output.WriteString(fmt.Sprintf("  mtu %d\n", mtu))
   114  		}
   115  
   116  		for _, route := range prepared.NameToRoutes[name] {
   117  			output.WriteString(fmt.Sprintf("  post-up ip route add %s via %s metric %d\n",
   118  				route.DestinationCIDR, route.GatewayIP, route.Metric))
   119  			output.WriteString(fmt.Sprintf("  pre-down ip route del %s via %s metric %d\n",
   120  				route.DestinationCIDR, route.GatewayIP, route.Metric))
   121  		}
   122  	}
   123  
   124  	generatedConfig := output.String()
   125  	logger.Debugf("generated network config:\n%s", generatedConfig)
   126  
   127  	if hasV4Interface && !gateway4Handled {
   128  		logger.Infof("generated network config has no ipv4 gateway")
   129  	}
   130  
   131  	if hasV6Interface && !gateway6Handled {
   132  		logger.Infof("generated network config has no ipv6 gateway")
   133  	}
   134  
   135  	return generatedConfig, nil
   136  }
   137  
   138  // GenerateNetplan renders a netplan file for one or more network
   139  // interfaces, using the given non-empty list of interfaces.
   140  func GenerateNetplan(interfaces []network.InterfaceInfo) (string, error) {
   141  	if len(interfaces) == 0 {
   142  		return "", errors.Errorf("missing container network config")
   143  	}
   144  	logger.Debugf("generating netplan from %#v", interfaces)
   145  	var netPlan netplan.Netplan
   146  	netPlan.Network.Ethernets = make(map[string]netplan.Ethernet)
   147  	netPlan.Network.Version = 2
   148  	for _, info := range interfaces {
   149  		var iface netplan.Ethernet
   150  		if cidr := info.CIDRAddress(); cidr != "" {
   151  			iface.Addresses = append(iface.Addresses, cidr)
   152  		} else if info.ConfigType == network.ConfigDHCP {
   153  			t := true
   154  			iface.DHCP4 = &t
   155  		}
   156  
   157  		for _, dns := range info.DNSServers {
   158  			iface.Nameservers.Addresses = append(iface.Nameservers.Addresses, dns.Value)
   159  		}
   160  		iface.Nameservers.Search = append(iface.Nameservers.Search, info.DNSSearchDomains...)
   161  
   162  		if info.GatewayAddress.Value != "" {
   163  			switch {
   164  			case info.GatewayAddress.Type == network.IPv4Address:
   165  				iface.Gateway4 = info.GatewayAddress.Value
   166  			case info.GatewayAddress.Type == network.IPv6Address:
   167  				iface.Gateway6 = info.GatewayAddress.Value
   168  			}
   169  		}
   170  
   171  		if info.MTU != 0 && info.MTU != 1500 {
   172  			iface.MTU = info.MTU
   173  		}
   174  		if info.MACAddress != "" {
   175  			iface.Match = map[string]string{"macaddress": info.MACAddress}
   176  		} else {
   177  			iface.Match = map[string]string{"name": info.InterfaceName}
   178  		}
   179  		for _, route := range info.Routes {
   180  			route := netplan.Route{
   181  				To:     route.DestinationCIDR,
   182  				Via:    route.GatewayIP,
   183  				Metric: &route.Metric,
   184  			}
   185  			iface.Routes = append(iface.Routes, route)
   186  		}
   187  		netPlan.Network.Ethernets[info.InterfaceName] = iface
   188  	}
   189  	out, err := netplan.Marshal(&netPlan)
   190  	if err != nil {
   191  		return "", errors.Trace(err)
   192  	}
   193  	return string(out), nil
   194  }
   195  
   196  // PreparedConfig holds all the necessary information to render a persistent
   197  // network config to a file.
   198  type PreparedConfig struct {
   199  	InterfaceNames   []string
   200  	AutoStarted      []string
   201  	DNSServers       []string
   202  	DNSSearchDomains []string
   203  	NameToAddress    map[string]string
   204  	NameToRoutes     map[string][]network.Route
   205  	NameToMTU        map[string]int
   206  	Gateway4Address  string
   207  	Gateway6Address  string
   208  }
   209  
   210  // PrepareNetworkConfigFromInterfaces collects the necessary information to
   211  // render a persistent network config from the given slice of
   212  // network.InterfaceInfo. The result always includes the loopback interface.
   213  func PrepareNetworkConfigFromInterfaces(interfaces []network.InterfaceInfo) *PreparedConfig {
   214  	dnsServers := set.NewStrings()
   215  	dnsSearchDomains := set.NewStrings()
   216  	gateway4Address := ""
   217  	gateway6Address := ""
   218  	namesInOrder := make([]string, 1, len(interfaces)+1)
   219  	nameToAddress := make(map[string]string)
   220  	nameToRoutes := make(map[string][]network.Route)
   221  	nameToMTU := make(map[string]int)
   222  
   223  	// Always include the loopback.
   224  	namesInOrder[0] = "lo"
   225  	autoStarted := set.NewStrings("lo")
   226  
   227  	// We need to check if we have a host-provided default GW and use it.
   228  	// Otherwise we'll use the first device with a gateway address,
   229  	// it'll be filled in the second loop.
   230  	for _, info := range interfaces {
   231  		if info.IsDefaultGateway {
   232  			switch info.GatewayAddress.Type {
   233  			case network.IPv4Address:
   234  				gateway4Address = info.GatewayAddress.Value
   235  			case network.IPv6Address:
   236  				gateway6Address = info.GatewayAddress.Value
   237  			}
   238  		}
   239  	}
   240  
   241  	for _, info := range interfaces {
   242  		ifaceName := strings.Replace(info.MACAddress, ":", "_", -1)
   243  		// prepend eth because .format of python wont like a tag starting with numbers.
   244  		ifaceName = fmt.Sprintf("{eth%s}", ifaceName)
   245  
   246  		if !info.NoAutoStart {
   247  			autoStarted.Add(ifaceName)
   248  		}
   249  
   250  		if cidr := info.CIDRAddress(); cidr != "" {
   251  			nameToAddress[ifaceName] = cidr
   252  		} else if info.ConfigType == network.ConfigDHCP {
   253  			nameToAddress[ifaceName] = string(network.ConfigDHCP)
   254  		}
   255  		nameToRoutes[ifaceName] = info.Routes
   256  
   257  		for _, dns := range info.DNSServers {
   258  			dnsServers.Add(dns.Value)
   259  		}
   260  
   261  		dnsSearchDomains = dnsSearchDomains.Union(set.NewStrings(info.DNSSearchDomains...))
   262  
   263  		if info.GatewayAddress.Value != "" {
   264  			switch {
   265  			case gateway4Address == "" && info.GatewayAddress.Type == network.IPv4Address:
   266  				gateway4Address = info.GatewayAddress.Value
   267  
   268  			case gateway6Address == "" && info.GatewayAddress.Type == network.IPv6Address:
   269  				gateway6Address = info.GatewayAddress.Value
   270  			}
   271  		}
   272  
   273  		if info.MTU != 0 && info.MTU != 1500 {
   274  			nameToMTU[ifaceName] = info.MTU
   275  		}
   276  
   277  		namesInOrder = append(namesInOrder, ifaceName)
   278  	}
   279  
   280  	prepared := &PreparedConfig{
   281  		InterfaceNames:   namesInOrder,
   282  		NameToAddress:    nameToAddress,
   283  		NameToRoutes:     nameToRoutes,
   284  		NameToMTU:        nameToMTU,
   285  		AutoStarted:      autoStarted.SortedValues(),
   286  		DNSServers:       dnsServers.SortedValues(),
   287  		DNSSearchDomains: dnsSearchDomains.SortedValues(),
   288  		Gateway4Address:  gateway4Address,
   289  		Gateway6Address:  gateway6Address,
   290  	}
   291  
   292  	logger.Debugf("prepared network config for rendering: %+v", prepared)
   293  	return prepared
   294  }
   295  
   296  // AddNetworkConfig adds configuration scripts for specified interfaces
   297  // to cloudconfig - using boot textfiles and boot commands. It currently
   298  // supports e/n/i and netplan.
   299  func (cfg *ubuntuCloudConfig) AddNetworkConfig(interfaces []network.InterfaceInfo) error {
   300  	if len(interfaces) != 0 {
   301  		eni, err := GenerateENITemplate(interfaces)
   302  		if err != nil {
   303  			return errors.Trace(err)
   304  		}
   305  		netPlan, err := GenerateNetplan(interfaces)
   306  		if err != nil {
   307  			return errors.Trace(err)
   308  		}
   309  		cfg.AddBootTextFile(jujuNetplanFile, netPlan, 0644)
   310  		cfg.AddBootTextFile(systemNetworkInterfacesFile+".templ", eni, 0644)
   311  		cfg.AddBootTextFile(systemNetworkInterfacesFile+".py", NetworkInterfacesScript, 0744)
   312  		cfg.AddBootCmd(populateNetworkInterfaces(systemNetworkInterfacesFile))
   313  	}
   314  	return nil
   315  }
   316  
   317  // Note: we sleep to mitigate against LP #1337873 and LP #1269921.
   318  // Note2: wait with anything that's hard to revert for as long as possible,
   319  // we've seen weird failure modes and IMHO it's impossible to avoid them all,
   320  // but we could do as much as we can to 1. avoid them 2. make the machine boot
   321  // if we mess up
   322  func populateNetworkInterfaces(networkFile string) string {
   323  	s := `
   324  if [ ! -f /sbin/ifup ]; then
   325      echo "No /sbin/ifup, applying netplan configuration."
   326      netplan generate
   327      netplan apply
   328  else
   329    if [ -f /usr/bin/python ]; then
   330        python %[1]s.py --interfaces-file %[1]s --output-file %[1]s.out
   331    else
   332        python3 %[1]s.py --interfaces-file %[1]s --output-file %[1]s.out
   333    fi
   334    ifdown -a
   335    sleep 1.5
   336    mv %[1]s.out %[1]s
   337    ifup -a
   338  fi
   339  `
   340  	return fmt.Sprintf(s, networkFile)
   341  }
   342  
   343  const NetworkInterfacesScript = `from __future__ import print_function, unicode_literals
   344  import subprocess, re, argparse, os, time, shutil
   345  from string import Formatter
   346  
   347  INTERFACES_FILE="/etc/network/interfaces"
   348  IP_LINE = re.compile(r"^\d+: (.*?):")
   349  IP_HWADDR = re.compile(r".*link/ether ((\w{2}|:){11})")
   350  COMMAND = "ip -oneline link"
   351  RETRIES = 3
   352  WAIT = 5
   353  
   354  # Python3 vs Python2
   355  try:
   356      strdecode = str.decode
   357  except AttributeError:
   358      strdecode = str
   359  
   360  def ip_parse(ip_output):
   361      """parses the output of the ip command
   362      and returns a hwaddr->nic-name dict"""
   363      devices = dict()
   364      print("Parsing ip command output %s" % ip_output)
   365      for ip_line in ip_output:
   366          ip_line_str = strdecode(ip_line, "utf-8")
   367          match = IP_LINE.match(ip_line_str)
   368          if match is None:
   369              continue
   370          nic_name = match.group(1).split('@')[0]
   371          match = IP_HWADDR.match(ip_line_str)
   372          if match is None:
   373              continue
   374          nic_hwaddr = match.group(1)
   375          devices[nic_hwaddr] = nic_name
   376      print("Found the following devices: %s" % str(devices))
   377      return devices
   378  
   379  def replace_ethernets(interfaces_file, output_file, devices, fail_on_missing):
   380      """check if the contents of interfaces_file contain template
   381      keys corresponding to hwaddresses and replace them with
   382      the proper device name"""
   383      with open(interfaces_file + ".templ", "r") as templ_file:
   384          interfaces = templ_file.read()
   385  
   386      formatter = Formatter()
   387      hwaddrs = [v[1] for v in formatter.parse(interfaces) if v[1]]
   388      print("Found the following hwaddrs: %s" % str(hwaddrs))
   389      device_replacements = dict()
   390      for hwaddr in hwaddrs:
   391          hwaddr_clean = hwaddr[3:].replace("_", ":")
   392          if devices.get(hwaddr_clean, None):
   393              device_replacements[hwaddr] = devices[hwaddr_clean]
   394          else:
   395              if fail_on_missing:
   396                  print("Can't find device with MAC %s, will retry" % hwaddr_clean)
   397                  return False
   398              else:
   399                  print("WARNING: Can't find device with MAC %s when expected" % hwaddr_clean)
   400                  device_replacements[hwaddr] = hwaddr
   401      formatted = interfaces.format(**device_replacements)
   402      print("Used the values in: %s\nto fix the interfaces file:\n%s\ninto\n%s" %
   403             (str(device_replacements), str(interfaces), str(formatted)))
   404  
   405      with open(output_file, "w") as intf_out_file:
   406          intf_out_file.write(formatted)
   407  
   408      if not os.path.exists(interfaces_file + ".bak"):
   409          try:
   410              shutil.copyfile(interfaces_file, interfaces_file + ".bak")
   411          except OSError: #silently ignore if the file is missing
   412              pass
   413      return True
   414  
   415  def main():
   416      parser = argparse.ArgumentParser()
   417      parser.add_argument("--interfaces-file", dest="intf_file", default=INTERFACES_FILE)
   418      parser.add_argument("--output-file", dest="out_file", default=INTERFACES_FILE+".out")
   419      parser.add_argument("--command", default=COMMAND)
   420      parser.add_argument("--retries", default=RETRIES)
   421      parser.add_argument("--wait", default=WAIT)
   422      args = parser.parse_args()
   423      retries = int(args.retries)
   424      for tries in range(retries):
   425          ip_output = ip_parse(subprocess.check_output(args.command.split()).splitlines())
   426          if replace_ethernets(args.intf_file, args.out_file, ip_output, (tries != retries - 1)):
   427               break
   428          else:
   429               time.sleep(float(args.wait))
   430  
   431  if __name__ == "__main__":
   432      main()
   433  `
   434  
   435  const CloudInitNetworkConfigDisabled = `config: "disabled"
   436  `