github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/container/lxd/network.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "os" 10 "sort" 11 "strconv" 12 "strings" 13 14 "github.com/juju/errors" 15 "github.com/lxc/lxd/shared/api" 16 17 "github.com/juju/juju/network" 18 ) 19 20 const ( 21 nic = "nic" 22 nicTypeBridged = "bridged" 23 nicTypeMACVLAN = "macvlan" 24 ) 25 26 // device is a type alias for profile devices. 27 type device = map[string]string 28 29 // LocalBridgeName returns the name of the local LXD network bridge. 30 func (s *Server) LocalBridgeName() string { 31 return s.localBridgeName 32 } 33 34 // EnableHTTPSListener configures LXD to listen for HTTPS requests, rather than 35 // only via a Unix socket. Attempts to listen on all protocols, but falls back 36 // to IPv4 only if IPv6 has been disabled with in kernel. 37 // Returns an error if updating the server configuration fails. 38 func (s *Server) EnableHTTPSListener() error { 39 // Make sure the LXD service is configured to listen to local https 40 // requests, rather than only via the Unix socket. 41 // TODO: jam 2016-02-25 This tells LXD to listen on all addresses, 42 // which does expose the LXD to outside requests. It would 43 // probably be better to only tell LXD to listen for requests on 44 // the loopback and LXC bridges that we are using. 45 if err := s.UpdateServerConfig(map[string]string{ 46 "core.https_address": "[::]", 47 }); err != nil { 48 cause := errors.Cause(err) 49 if strings.HasSuffix(cause.Error(), errIPV6NotSupported) { 50 // Fall back to IPv4 only. 51 return errors.Trace(s.UpdateServerConfig(map[string]string{ 52 "core.https_address": "0.0.0.0", 53 })) 54 } 55 return errors.Trace(err) 56 } 57 return nil 58 } 59 60 // EnsureIPv4 retrieves the network for the input name and checks its IPv4 61 // configuration. If none is detected, it is set to "auto". 62 // The boolean return indicates if modification was necessary. 63 func (s *Server) EnsureIPv4(netName string) (bool, error) { 64 var modified bool 65 66 net, eTag, err := s.GetNetwork(netName) 67 if err != nil { 68 return false, errors.Trace(err) 69 } 70 71 cfg, ok := net.Config["ipv4.address"] 72 if !ok || cfg == "none" { 73 if net.Config == nil { 74 net.Config = make(device, 2) 75 } 76 net.Config["ipv4.address"] = "auto" 77 net.Config["ipv4.nat"] = "true" 78 79 if err := s.UpdateNetwork(netName, net.Writable(), eTag); err != nil { 80 return false, errors.Trace(err) 81 } 82 modified = true 83 } 84 85 return modified, nil 86 } 87 88 // GetNICsFromProfile returns all NIC devices in the profile with the input 89 // name. All returned devices have a MAC address; generated if required. 90 func (s *Server) GetNICsFromProfile(profName string) (map[string]device, error) { 91 profile, _, err := s.GetProfile(lxdDefaultProfileName) 92 if err != nil { 93 return nil, errors.Trace(err) 94 } 95 96 nics := getProfileNICs(profile) 97 for name := range nics { 98 if nics[name]["hwaddr"] == "" { 99 nics[name]["hwaddr"] = network.GenerateVirtualMACAddress() 100 } 101 } 102 return nics, nil 103 } 104 105 // VerifyNetworkDevice attempts to ensure that there is a network usable by LXD 106 // and that there is a NIC device with said network as its parent. 107 // If there are no NIC devices, and this server is *not* in cluster mode, 108 // an attempt is made to create an new device in the input profile, 109 // with the default LXD bridge as its parent. 110 func (s *Server) VerifyNetworkDevice(profile *api.Profile, eTag string) error { 111 nics := getProfileNICs(profile) 112 113 if len(nics) == 0 { 114 if s.networkAPISupport && !s.clustered { 115 return errors.Annotate(s.ensureDefaultNetworking(profile, eTag), "ensuring default bridge config") 116 } 117 return errors.Errorf("profile %q does not have any devices configured with type %q", profile.Name, nic) 118 } 119 120 if s.networkAPISupport { 121 return errors.Annotatef(s.verifyNICsWithAPI(nics), "profile %q", profile.Name) 122 } 123 124 return errors.Annotatef(s.verifyNICsWithConfigFile(nics, ioutil.ReadFile), "profile %q", profile.Name) 125 } 126 127 // ensureDefaultNetworking ensures that the default LXD bridge exists, 128 // that it is not configured to use IPv6, and that a NIC device exists in 129 // the input profile. 130 // An error is returned if the bridge exists with IPv6 configuration. 131 // If the bridge does not exist, it is created. 132 func (s *Server) ensureDefaultNetworking(profile *api.Profile, eTag string) error { 133 net, _, err := s.GetNetwork(network.DefaultLXDBridge) 134 if err != nil { 135 if !IsLXDNotFound(err) { 136 return errors.Trace(err) 137 } 138 req := api.NetworksPost{ 139 Name: network.DefaultLXDBridge, 140 Type: "bridge", 141 Managed: true, 142 NetworkPut: api.NetworkPut{Config: map[string]string{ 143 "ipv4.address": "auto", 144 "ipv4.nat": "true", 145 "ipv6.address": "none", 146 "ipv6.nat": "false", 147 }}, 148 } 149 err := s.CreateNetwork(req) 150 if err != nil { 151 return errors.Trace(err) 152 } 153 net, _, err = s.GetNetwork(network.DefaultLXDBridge) 154 if err != nil { 155 return errors.Trace(err) 156 } 157 } else { 158 if err := verifyNoIPv6(net); err != nil { 159 return errors.Trace(err) 160 } 161 } 162 163 s.localBridgeName = network.DefaultLXDBridge 164 165 nicName := generateNICDeviceName(profile) 166 if nicName == "" { 167 return errors.Errorf("failed to generate a unique device name for profile %q", profile.Name) 168 } 169 170 // Add the new device with the bridge as its parent. 171 nicType := nicTypeMACVLAN 172 if net.Type == "bridge" { 173 nicType = nicTypeBridged 174 } 175 profile.Devices[nicName] = device{ 176 "type": nic, 177 "nictype": nicType, 178 "parent": network.DefaultLXDBridge, 179 } 180 181 if err := s.UpdateProfile(profile.Name, profile.Writable(), eTag); err != nil { 182 return errors.Trace(err) 183 } 184 logger.Debugf("created new nic device %q in profile %q", nicName, profile.Name) 185 return nil 186 } 187 188 // verifyNICsWithAPI uses the LXD network API to check if one of the input NIC 189 // devices is suitable for LXD to work with Juju. 190 func (s *Server) verifyNICsWithAPI(nics map[string]device) error { 191 checked := make([]string, 0, len(nics)) 192 var ipV6ErrMsg error 193 for name, nic := range nics { 194 checked = append(checked, name) 195 196 if !isValidNICType(nic) { 197 continue 198 } 199 200 netName := nic["parent"] 201 if netName == "" { 202 continue 203 } 204 205 net, _, err := s.GetNetwork(netName) 206 if err != nil { 207 return errors.Annotatef(err, "retrieving network %q", netName) 208 } 209 if err := verifyNoIPv6(net); err != nil { 210 ipV6ErrMsg = err 211 continue 212 } 213 214 logger.Tracef("found usable network device %q with parent %q", name, netName) 215 s.localBridgeName = netName 216 return nil 217 } 218 219 // A nic with valid type found, but the network configures IPv6. 220 if ipV6ErrMsg != nil { 221 return ipV6ErrMsg 222 } 223 224 // No nics with a nictype of nicTypeBridged, nicTypeMACVLAN was found. 225 return errors.Errorf(fmt.Sprintf( 226 "no network device found with nictype %q or %q"+ 227 "\n\tthe following devices were checked: %s"+ 228 "\nNote: juju does not support IPv6."+ 229 "\nReconfigure lxd to use a network of type %q or %q, disabling IPv6.", 230 nicTypeBridged, nicTypeMACVLAN, strings.Join(checked, ", "), nicTypeBridged, nicTypeMACVLAN)) 231 } 232 233 // verifyNICsWithConfigFile is recruited for legacy LXD installations. 234 // It checks the LXD bridge configuration file and ensure that one of the input 235 // devices is suitable for LXD to work with Juju. 236 func (s *Server) verifyNICsWithConfigFile(nics map[string]device, reader func(string) ([]byte, error)) error { 237 netName, err := checkBridgeConfigFile(reader) 238 if err != nil { 239 return errors.Trace(err) 240 } 241 242 checked := make([]string, 0, len(nics)) 243 for name, nic := range nics { 244 checked = append(checked, name) 245 246 if nic["parent"] != netName { 247 continue 248 } 249 if !isValidNICType(nic) { 250 continue 251 } 252 253 logger.Tracef("found usable network device %q with parent %q", name, netName) 254 s.localBridgeName = netName 255 return nil 256 } 257 258 return errors.Errorf("no network device found with nictype %q or %q that uses the configured bridge in %s"+ 259 "\n\tthe following devices were checked: %v", nicTypeBridged, nicTypeMACVLAN, BridgeConfigFile, checked) 260 } 261 262 // generateNICDeviceName attempts to generate a new NIC device name that is not 263 // already in the input profile. If none can be determined in a reasonable 264 // search space, an empty name is returned. This should never really happen, 265 // but the name generation aborts to be safe from (theoretical) integer overflow. 266 func generateNICDeviceName(profile *api.Profile) string { 267 template := "eth%d" 268 for i := 0; i < 100; i++ { 269 name := fmt.Sprintf(template, i) 270 unique := true 271 for d := range profile.Devices { 272 if d == name { 273 unique = false 274 break 275 } 276 } 277 if unique { 278 return name 279 } 280 } 281 return "" 282 } 283 284 // getProfileNICs iterates over the devices in the input profile and returns 285 // any that are of type "nic". 286 func getProfileNICs(profile *api.Profile) map[string]device { 287 nics := make(map[string]device, len(profile.Devices)) 288 for k, v := range profile.Devices { 289 if v["type"] == nic { 290 nics[k] = v 291 } 292 } 293 return nics 294 } 295 296 // verifyNoIPv6 checks that the input network has no IPv6 configuration. 297 // An error is returned when it does. 298 // TODO (manadart 2018-05-28) The intention is to support IPv6, so this 299 // restriction is temporary. 300 func verifyNoIPv6(net *api.Network) error { 301 if !net.Managed { 302 return nil 303 } 304 cfg, ok := net.Config["ipv6.address"] 305 if !ok { 306 return nil 307 } 308 if cfg == "none" { 309 return nil 310 } 311 312 return errors.Errorf("juju does not support IPv6. Disable IPv6 in LXD via:\n"+ 313 "\tlxc network set %s ipv6.address none\n"+ 314 "and run the command again", net.Name) 315 } 316 317 func isValidNICType(nic device) bool { 318 return nic["nictype"] == nicTypeBridged || nic["nictype"] == nicTypeMACVLAN 319 } 320 321 const BridgeConfigFile = "/etc/default/lxd-bridge" 322 323 // checkBridgeConfigFile verifies that the file configuration for the LXD 324 // bridge has a a bridge name, that it is set to be used by LXD and that 325 // it has (only) IPv4 configuration. 326 // TODO (manadart 2018-05-28) The error messages are invalid for LXD 327 // installations that pre-date the network API support and that were installed 328 // via Snap. The question of the correct user action was posed on the #lxd IRC 329 // channel, but has not be answered to-date. 330 func checkBridgeConfigFile(reader func(string) ([]byte, error)) (string, error) { 331 bridgeConfig, err := reader(BridgeConfigFile) 332 if os.IsNotExist(err) { 333 return "", bridgeConfigError("no config file found at " + BridgeConfigFile) 334 } else if err != nil { 335 return "", errors.Trace(err) 336 } 337 338 foundSubnetConfig := false 339 bridgeName := "" 340 for _, line := range strings.Split(string(bridgeConfig), "\n") { 341 if strings.HasPrefix(line, "USE_LXD_BRIDGE=") { 342 b, err := strconv.ParseBool(strings.Trim(line[len("USE_LXD_BRIDGE="):], " \"")) 343 if err != nil { 344 logger.Debugf("unable to parse bool, skipping USE_LXD_BRIDGE check: %s", err) 345 continue 346 } 347 if !b { 348 return "", bridgeConfigError(fmt.Sprintf("%s has USE_LXD_BRIDGE set to false", BridgeConfigFile)) 349 } 350 } else if strings.HasPrefix(line, "LXD_BRIDGE=") { 351 bridgeName = strings.Trim(line[len("LXD_BRIDGE="):], " \"") 352 if bridgeName == "" { 353 return "", bridgeConfigError(fmt.Sprintf("%s has no LXD_BRIDGE set", BridgeConfigFile)) 354 } 355 } else if strings.HasPrefix(line, "LXD_IPV4_ADDR=") { 356 contents := strings.Trim(line[len("LXD_IPV4_ADDR="):], " \"") 357 if len(contents) > 0 { 358 foundSubnetConfig = true 359 } 360 } else if strings.HasPrefix(line, "LXD_IPV6_ADDR=") { 361 contents := strings.Trim(line[len("LXD_IPV6_ADDR="):], " \"") 362 if len(contents) > 0 { 363 return "", ipv6BridgeConfigError(BridgeConfigFile) 364 } 365 } 366 } 367 368 if !foundSubnetConfig { 369 // TODO (hml) 2018-08-09 Question 370 // Should the error mention ipv6 is not enabled if juju doesn't support it? 371 return "", bridgeConfigError(bridgeName + " has no ipv4 or ipv6 subnet enabled") 372 } 373 return bridgeName, nil 374 } 375 376 func bridgeConfigError(err string) error { 377 return errors.Errorf("%s\nIt looks like your LXD bridge has not yet been configured. Configure it via:\n\n"+ 378 "\tsudo dpkg-reconfigure -p medium lxd\n\n"+ 379 "and run the command again.", err) 380 } 381 382 func ipv6BridgeConfigError(fileName string) error { 383 return errors.Errorf("%s has IPv6 enabled.\nJuju doesn't currently support IPv6.\n"+ 384 "Disable IPv6 via:\n\n"+ 385 "\tsudo dpkg-reconfigure -p medium lxd\n\n"+ 386 "and run the command again.", fileName) 387 } 388 389 // InterfaceInfoFromDevices returns a slice of interface info congruent with the 390 // input LXD NIC devices. 391 // The output is used to generate cloud-init user-data congruent with the NICs 392 // that end up in the container. 393 func InterfaceInfoFromDevices(nics map[string]device) ([]network.InterfaceInfo, error) { 394 interfaces := make([]network.InterfaceInfo, len(nics)) 395 var i int 396 for name, device := range nics { 397 iInfo := network.InterfaceInfo{ 398 InterfaceName: name, 399 ParentInterfaceName: device["parent"], 400 MACAddress: device["hwaddr"], 401 ConfigType: network.ConfigDHCP, 402 } 403 if device["mtu"] != "" { 404 mtu, err := strconv.Atoi(device["mtu"]) 405 if err != nil { 406 return nil, errors.Annotate(err, "parsing device MTU") 407 } 408 iInfo.MTU = mtu 409 } 410 411 interfaces[i] = iInfo 412 i++ 413 } 414 415 sortInterfacesByName(interfaces) 416 return interfaces, nil 417 } 418 419 type interfaceInfoSlice []network.InterfaceInfo 420 421 func (s interfaceInfoSlice) Len() int { return len(s) } 422 func (s interfaceInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 423 func (s interfaceInfoSlice) Less(i, j int) bool { 424 return s[i].InterfaceName < s[j].InterfaceName 425 } 426 427 func sortInterfacesByName(interfaces []network.InterfaceInfo) { 428 sort.Sort(interfaceInfoSlice(interfaces)) 429 } 430 431 // errIPV6NotSupported is the error returned by glibc for attempts at 432 // unsupported protocols. 433 const errIPV6NotSupported = `socket: address family not supported by protocol` 434 435 // DevicesFromInterfaceInfo uses the input interface info collection to create a 436 // map of network device configuration in the LXD format. 437 // Names for any networks without a known CIDR are returned in a slice. 438 func DevicesFromInterfaceInfo(interfaces []network.InterfaceInfo) (map[string]device, []string, error) { 439 nics := make(map[string]device, len(interfaces)) 440 var unknown []string 441 442 for _, v := range interfaces { 443 if v.InterfaceType == network.LoopbackInterface { 444 continue 445 } 446 if v.InterfaceType != network.EthernetInterface { 447 return nil, nil, errors.Errorf("interface type %q not supported", v.InterfaceType) 448 } 449 if v.ParentInterfaceName == "" { 450 return nil, nil, errors.Errorf("parent interface name is empty") 451 } 452 if v.CIDR == "" { 453 unknown = append(unknown, v.ParentInterfaceName) 454 } 455 nics[v.InterfaceName] = newNICDevice(v.InterfaceName, v.ParentInterfaceName, v.MACAddress, v.MTU) 456 } 457 458 return nics, unknown, nil 459 } 460 461 // newNICDevice creates and returns a LXD-compatible config for a network 462 // device from the input arguments. 463 // TODO (manadart 2018-06-21) We want to support nictype=macvlan too. 464 // This will involve interrogating the parent device, via the server if it is 465 // LXD managed, or via the container.NetworkConfig.DeviceType that this is 466 // being generated from. 467 func newNICDevice(deviceName, parentDevice, hwAddr string, mtu int) device { 468 device := map[string]string{ 469 "type": "nic", 470 "nictype": "bridged", 471 "name": deviceName, 472 "parent": parentDevice, 473 } 474 if hwAddr != "" { 475 device["hwaddr"] = hwAddr 476 } 477 if mtu > 0 { 478 device["mtu"] = fmt.Sprintf("%v", mtu) 479 } 480 return device 481 }