github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/lxd/environ_network.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "net" 9 "sort" 10 "strings" 11 12 lxdapi "github.com/canonical/lxd/shared/api" 13 "github.com/juju/collections/set" 14 "github.com/juju/collections/transform" 15 "github.com/juju/errors" 16 "github.com/juju/names/v5" 17 18 "github.com/juju/juju/core/instance" 19 "github.com/juju/juju/core/network" 20 "github.com/juju/juju/environs" 21 "github.com/juju/juju/environs/context" 22 ) 23 24 var _ environs.Networking = (*environ)(nil) 25 26 // Subnets returns basic information about subnets known by the provider for 27 // the environment. 28 func (e *environ) Subnets(ctx context.ProviderCallContext, inst instance.Id, subnetIDs []network.Id) ([]network.SubnetInfo, error) { 29 srv := e.server() 30 31 // All containers will have the same view on the LXD network. If an 32 // instance ID is provided, the best we can do is to also ensure the 33 // container actually exists at the cost of an additional API call. 34 if inst != instance.UnknownId { 35 contList, err := srv.FilterContainers(string(inst)) 36 if err != nil { 37 return nil, errors.Trace(err) 38 } else if len(contList) == 0 { 39 return nil, errors.NotFoundf("container with instance ID %q", inst) 40 } 41 } 42 43 availabilityZones, err := e.AvailabilityZones(ctx) 44 if err != nil { 45 return nil, errors.Annotate(err, "retrieving lxd availability zones") 46 } 47 48 networks, err := srv.GetNetworks() 49 if err != nil { 50 if isErrMissingAPIExtension(err, "network") { 51 return nil, errors.NewNotSupported(nil, `subnet discovery requires the "network" extension to be enabled on the lxd server`) 52 } 53 return nil, errors.Trace(err) 54 } 55 56 var keepList set.Strings 57 if len(subnetIDs) != 0 { 58 keepList = set.NewStrings() 59 for _, id := range subnetIDs { 60 keepList.Add(string(id)) 61 } 62 } 63 64 var ( 65 subnets []network.SubnetInfo 66 uniqueSubnetIDs = set.NewStrings() 67 ) 68 for _, networkDetails := range networks { 69 if networkDetails.Type != "bridge" { 70 continue 71 } 72 73 networkName := networkDetails.Name 74 state, err := srv.GetNetworkState(networkName) 75 if err != nil { 76 // Unfortunately, LXD on bionic and earlier does not 77 // support the network_state extension out of the box 78 // so this call will fail. If that's the case then 79 // use a fallback method for detecting subnets. 80 if isErrMissingAPIExtension(err, "network_state") { 81 return e.subnetDetectionFallback(srv, inst, keepList, availabilityZones) 82 } 83 return nil, errors.Annotatef(err, "querying lxd server for state of network %q", networkName) 84 } 85 86 // We are only interested in networks that are up. 87 if state.State != "up" { 88 continue 89 } 90 91 for _, stateAddr := range state.Addresses { 92 netAddr := network.NewMachineAddress(stateAddr.Address).AsProviderAddress() 93 if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal { 94 continue 95 } 96 97 subnetID, cidr, err := makeSubnetIDForNetwork(networkName, stateAddr.Address, stateAddr.Netmask) 98 if err != nil { 99 return nil, errors.Trace(err) 100 } 101 102 if uniqueSubnetIDs.Contains(subnetID) { 103 continue 104 } else if keepList != nil && !keepList.Contains(subnetID) { 105 continue 106 } 107 108 uniqueSubnetIDs.Add(subnetID) 109 subnets = append(subnets, makeSubnetInfo(network.Id(subnetID), makeNetworkID(networkName), cidr, availabilityZones)) 110 } 111 } 112 113 return subnets, nil 114 } 115 116 // subnetDetectionFallback provides a fallback mechanism for subnet discovery 117 // on older LXD versions (e.g. the ones that ship with xenial and bionic) which 118 // do not come with the network_state API extension enabled. 119 // 120 // The fallback exploits the fact that subnet discovery is performed after the 121 // controller spins up. To this end, the method will query any of the available 122 // juju containers and attempt to reconstruct the subnet information based on 123 // the devices present inside the container. 124 // 125 // Caveat: this method offers lower data fidelity compared to Subnets() as it 126 // cannot accurately detect the CIDRs for any host devices that are not bridged 127 // into the container. 128 func (e *environ) subnetDetectionFallback(srv Server, inst instance.Id, keepSubnetIDs set.Strings, availabilityZones network.AvailabilityZones) ([]network.SubnetInfo, error) { 129 logger.Warningf("falling back to subnet discovery via introspection of devices bridged to the controller container; consider upgrading to a newer LXD version and running 'juju reload-spaces' to get full subnet discovery for the LXD host") 130 131 // If no instance ID is specified, list the alive containers, query the 132 // state of the first one on the list and use it to extrapolate the 133 // subnet layout. 134 if inst == instance.UnknownId { 135 aliveConts, err := srv.AliveContainers("juju-") 136 if err != nil { 137 return nil, errors.Trace(err) 138 } else if len(aliveConts) == 0 { 139 return nil, errors.New("no alive containers detected") 140 } 141 inst = instance.Id(aliveConts[0].Name) 142 } 143 144 container, state, err := getContainerDetails(srv, string(inst)) 145 if err != nil { 146 return nil, errors.Trace(err) 147 } 148 149 var ( 150 subnets []network.SubnetInfo 151 uniqueSubnetIDs = set.NewStrings() 152 ) 153 154 for guestNetworkName, netInfo := range state.Network { 155 hostNetworkName := hostNetworkForGuestNetwork(container, guestNetworkName) 156 if hostNetworkName == "" { // doesn't have a parent; assume non-bridged NIC 157 continue 158 } 159 160 // Ignore loopback devices and NICs in down state. 161 if detectInterfaceType(netInfo.Type) == network.LoopbackDevice || netInfo.State != "up" { 162 continue 163 } 164 165 for _, guestAddr := range netInfo.Addresses { 166 netAddr := network.NewMachineAddress(guestAddr.Address).AsProviderAddress() 167 if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal { 168 continue 169 } 170 171 // Use the detected host network name and the guest 172 // address details to generate a subnetID for the host. 173 subnetID, cidr, err := makeSubnetIDForNetwork(hostNetworkName, guestAddr.Address, guestAddr.Netmask) 174 if err != nil { 175 return nil, errors.Trace(err) 176 } 177 178 if uniqueSubnetIDs.Contains(subnetID) { 179 continue 180 } else if keepSubnetIDs != nil && !keepSubnetIDs.Contains(subnetID) { 181 continue 182 } 183 184 uniqueSubnetIDs.Add(subnetID) 185 subnets = append(subnets, makeSubnetInfo(network.Id(subnetID), makeNetworkID(hostNetworkName), cidr, availabilityZones)) 186 } 187 } 188 189 return subnets, nil 190 } 191 192 func makeNetworkID(networkName string) network.Id { 193 return network.Id(fmt.Sprintf("net-%s", networkName)) 194 } 195 196 func makeSubnetIDForNetwork(networkName, address, mask string) (string, string, error) { 197 _, netCIDR, err := net.ParseCIDR(fmt.Sprintf("%s/%s", address, mask)) 198 if err != nil { 199 return "", "", errors.Annotatef(err, "calculating CIDR for network %q", networkName) 200 } 201 202 cidr := netCIDR.String() 203 subnetID := fmt.Sprintf("subnet-%s-%s", networkName, cidr) 204 return subnetID, cidr, nil 205 } 206 207 func makeSubnetInfo(subnetID network.Id, networkID network.Id, cidr string, availabilityZones network.AvailabilityZones) network.SubnetInfo { 208 azNames := transform.Slice(availabilityZones, func(az network.AvailabilityZone) string { return az.Name() }) 209 return network.SubnetInfo{ 210 ProviderId: subnetID, 211 ProviderNetworkId: networkID, 212 CIDR: cidr, 213 VLANTag: 0, 214 AvailabilityZones: azNames, 215 } 216 } 217 218 // NetworkInterfaces returns a slice with the network interfaces that 219 // correspond to the given instance IDs. If no instances where found, but there 220 // was no other error, it will return ErrNoInstances. If some but not all of 221 // the instances were found, the returned slice will have some nil slots, and 222 // an ErrPartialInstances error will be returned. 223 func (e *environ) NetworkInterfaces(_ context.ProviderCallContext, ids []instance.Id) ([]network.InterfaceInfos, error) { 224 var ( 225 missing int 226 srv = e.server() 227 res = make([]network.InterfaceInfos, len(ids)) 228 ) 229 230 for instIdx, id := range ids { 231 container, state, err := getContainerDetails(srv, string(id)) 232 if err != nil { 233 if errors.IsNotFound(err) { 234 missing++ 235 continue 236 } 237 return nil, errors.Annotatef(err, "retrieving network interface info for instance %q", id) 238 } else if len(state.Network) == 0 { 239 continue 240 } 241 242 // Sort interfaces by name to ensure consistent device indexes 243 // across calls when we iterate the container's network map. 244 guestNetworkNames := make([]string, 0, len(state.Network)) 245 for network := range state.Network { 246 guestNetworkNames = append(guestNetworkNames, network) 247 } 248 sort.Strings(guestNetworkNames) 249 250 var devIdx int 251 for _, guestNetworkName := range guestNetworkNames { 252 netInfo := state.Network[guestNetworkName] 253 254 // Ignore loopback devices 255 if detectInterfaceType(netInfo.Type) == network.LoopbackDevice { 256 continue 257 } 258 259 ni, err := makeInterfaceInfo(container, guestNetworkName, netInfo) 260 if err != nil { 261 return nil, errors.Annotatef(err, "retrieving network interface info for instance %q", id) 262 } else if len(ni.Addresses) == 0 { 263 continue 264 } 265 266 ni.DeviceIndex = devIdx 267 devIdx++ 268 res[instIdx] = append(res[instIdx], ni) 269 } 270 } 271 272 if missing > 0 { 273 // Found at least one instance 274 if missing != len(res) { 275 return res, environs.ErrPartialInstances 276 } 277 278 return nil, environs.ErrNoInstances 279 } 280 return res, nil 281 } 282 283 func makeInterfaceInfo(container *lxdapi.Instance, guestNetworkName string, netInfo lxdapi.InstanceStateNetwork) (network.InterfaceInfo, error) { 284 var ni = network.InterfaceInfo{ 285 MACAddress: netInfo.Hwaddr, 286 MTU: netInfo.Mtu, 287 InterfaceName: guestNetworkName, 288 ParentInterfaceName: hostNetworkForGuestNetwork(container, guestNetworkName), 289 InterfaceType: detectInterfaceType(netInfo.Type), 290 Origin: network.OriginProvider, 291 } 292 293 // We cannot tell from the API response whether the 294 // interface uses a static or DHCP configuration. 295 // Assume static unless this is a loopback device. 296 configType := network.ConfigStatic 297 if ni.InterfaceType == network.LoopbackDevice { 298 configType = network.ConfigLoopback 299 } 300 301 if ni.ParentInterfaceName != "" { 302 ni.ProviderNetworkId = makeNetworkID(ni.ParentInterfaceName) 303 } 304 305 // Iterate the list of addresses assigned to this interface ignoring 306 // any link-local ones. The first non link-local address is treated as 307 // the primary address and is used to populate the interface CIDR and 308 // subnet ID fields. 309 for _, addr := range netInfo.Addresses { 310 netAddr := network.NewMachineAddress(addr.Address).AsProviderAddress() 311 if netAddr.Scope == network.ScopeLinkLocal || netAddr.Scope == network.ScopeMachineLocal { 312 continue 313 } 314 315 // Use the parent bridge name to match the subnet IDs reported 316 // by the Subnets() method. 317 subnetID, cidr, err := makeSubnetIDForNetwork(ni.ParentInterfaceName, addr.Address, addr.Netmask) 318 if err != nil { 319 return network.InterfaceInfo{}, errors.Trace(err) 320 } 321 322 netAddr.CIDR = cidr 323 netAddr.ConfigType = configType 324 ni.Addresses = append(ni.Addresses, netAddr) 325 326 // Only set provider IDs based on the first address. 327 // TODO (manadart 2021-03-24): We should associate the provider ID for 328 // the subnet with the address. 329 if len(ni.Addresses) > 1 { 330 continue 331 } 332 333 ni.ProviderSubnetId = network.Id(subnetID) 334 ni.ProviderId = network.Id(fmt.Sprintf("nic-%s", netInfo.Hwaddr)) 335 } 336 337 return ni, nil 338 } 339 340 func detectInterfaceType(lxdIfaceType string) network.LinkLayerDeviceType { 341 switch lxdIfaceType { 342 case "bridge": 343 return network.BridgeDevice 344 case "broadcast": 345 return network.EthernetDevice 346 case "loopback": 347 return network.LoopbackDevice 348 default: 349 return network.UnknownDevice 350 } 351 } 352 353 func hostNetworkForGuestNetwork(container *lxdapi.Instance, guestNetwork string) string { 354 if container.ExpandedDevices == nil { 355 return "" 356 } 357 devInfo, found := container.ExpandedDevices[guestNetwork] 358 if !found { 359 return "" 360 } 361 362 if name, found := devInfo["network"]; found { // lxd 4+ 363 return name 364 } else if name, found := devInfo["parent"]; found { // lxd 3 365 return name 366 } 367 return "" 368 } 369 370 func getContainerDetails(srv Server, containerID string) (*lxdapi.Instance, *lxdapi.InstanceState, error) { 371 cont, _, err := srv.GetInstance(containerID) 372 if err != nil { 373 if isErrNotFound(err) { 374 return nil, nil, errors.NotFoundf("container %q", containerID) 375 } 376 return nil, nil, errors.Trace(err) 377 } 378 379 state, _, err := srv.GetInstanceState(containerID) 380 if err != nil { 381 if isErrNotFound(err) { 382 return nil, nil, errors.NotFoundf("container %q", containerID) 383 } 384 return nil, nil, errors.Trace(err) 385 } 386 387 return cont, state, nil 388 } 389 390 // isErrNotFound returns true if the LXD server returned back a "not found" error. 391 func isErrNotFound(err error) bool { 392 // Unfortunately the lxd client does not expose error 393 // codes so we need to match against a string here. 394 return err != nil && strings.Contains(err.Error(), "not found") 395 } 396 397 // isErrMissingAPIExtension returns true if the LXD server returned back an 398 // "API extension not found" error. 399 func isErrMissingAPIExtension(err error, ext string) bool { 400 // Unfortunately the lxd client does not expose error 401 // codes so we need to match against a string here. 402 return err != nil && strings.Contains(err.Error(), fmt.Sprintf("server is missing the required %q API extension", ext)) 403 } 404 405 // SuperSubnets returns information about aggregated subnet. 406 func (*environ) SuperSubnets(context.ProviderCallContext) ([]string, error) { 407 return nil, errors.NotSupportedf("super subnets") 408 } 409 410 // SupportsSpaces returns whether the current environment supports 411 // spaces. The returned error satisfies errors.IsNotSupported(), 412 // unless a general API failure occurs. 413 func (e *environ) SupportsSpaces(context.ProviderCallContext) (bool, error) { 414 // Really old lxd versions (e.g. xenial/ppc64) do not even support the 415 // network API extension so the subnet discovery code path will not 416 // work there. 417 return e.server().HasExtension("network"), nil 418 } 419 420 // AreSpacesRoutable returns whether the communication between the 421 // two spaces can use cloud-local addresses. 422 func (*environ) AreSpacesRoutable(context.ProviderCallContext, *environs.ProviderSpaceInfo, *environs.ProviderSpaceInfo) (bool, error) { 423 return false, errors.NotSupportedf("spaces") 424 } 425 426 // SupportsContainerAddresses returns true if the current environment is 427 // able to allocate addresses for containers. 428 func (*environ) SupportsContainerAddresses(context.ProviderCallContext) (bool, error) { 429 return false, nil 430 } 431 432 // AllocateContainerAddresses allocates a static subnets for each of the 433 // container NICs in preparedInfo, hosted by the hostInstanceID. Returns the 434 // network config including all allocated addresses on success. 435 func (*environ) AllocateContainerAddresses(context.ProviderCallContext, instance.Id, names.MachineTag, network.InterfaceInfos) (network.InterfaceInfos, error) { 436 return nil, errors.NotSupportedf("container address allocation") 437 } 438 439 // ReleaseContainerAddresses releases the previously allocated 440 // addresses matching the interface details passed in. 441 func (*environ) ReleaseContainerAddresses(context.ProviderCallContext, []network.ProviderInterfaceInfo) error { 442 return errors.NotSupportedf("container address allocation") 443 }