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 `