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