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  }