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 `