github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/openstack/networking.go (about)

     1  // Copyright 2012-2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package openstack
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"strings"
    10  
    11  	"github.com/go-goose/goose/v5/neutron"
    12  	"github.com/go-goose/goose/v5/nova"
    13  	"github.com/juju/collections/set"
    14  	"github.com/juju/collections/transform"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/utils/v3"
    17  
    18  	"github.com/juju/juju/core/instance"
    19  	"github.com/juju/juju/core/network"
    20  	corenetwork "github.com/juju/juju/core/network"
    21  	"github.com/juju/juju/environs"
    22  )
    23  
    24  type networkingBase struct {
    25  	env *Environ
    26  }
    27  
    28  func (n networkingBase) client() NetworkingAuthenticatingClient {
    29  	return n.env.client()
    30  }
    31  
    32  func (n networkingBase) neutron() NetworkingNeutron {
    33  	return n.env.neutron()
    34  }
    35  
    36  func (n networkingBase) nova() NetworkingNova {
    37  	return n.env.nova()
    38  }
    39  
    40  func (n networkingBase) ecfg() NetworkingEnvironConfig {
    41  	return n.env.ecfg()
    42  }
    43  
    44  // NeutronNetworking is an implementation of Networking that uses the Neutron
    45  // network APIs.
    46  type NeutronNetworking struct {
    47  	NetworkingBase
    48  }
    49  
    50  // projectIDFilter returns a neutron.Filter to match Neutron Networks with
    51  // the given projectID.
    52  func projectIDFilter(projectID string) *neutron.Filter {
    53  	filter := neutron.NewFilter()
    54  	filter.Set(neutron.FilterProjectId, projectID)
    55  	return filter
    56  }
    57  
    58  // externalNetworkFilter returns a neutron.Filter to match Neutron Networks with
    59  // router:external = true.
    60  func externalNetworkFilter() *neutron.Filter {
    61  	filter := neutron.NewFilter()
    62  	filter.Set(neutron.FilterRouterExternal, "true")
    63  	return filter
    64  }
    65  
    66  // internalNetworkFilter returns a neutron.Filter to match Neutron Networks with
    67  // router:external = false.
    68  func internalNetworkFilter() *neutron.Filter {
    69  	filter := neutron.NewFilter()
    70  	filter.Set(neutron.FilterRouterExternal, "false")
    71  	return filter
    72  }
    73  
    74  // networkFilter returns a neutron.Filter to match Neutron Networks with
    75  // the exact given name AND router:external boolean result.
    76  func networkFilter(name string, external bool) *neutron.Filter {
    77  	filter := neutron.NewFilter()
    78  	filter.Set(neutron.FilterNetwork, fmt.Sprintf("%s", name))
    79  	filter.Set(neutron.FilterRouterExternal, fmt.Sprintf("%t", external))
    80  	return filter
    81  }
    82  
    83  func newNetworking(e *Environ) Networking {
    84  	return &NeutronNetworking{NetworkingBase: networkingBase{env: e}}
    85  }
    86  
    87  // AllocatePublicIP is part of the Networking interface.
    88  func (n *NeutronNetworking) AllocatePublicIP(id instance.Id) (*string, error) {
    89  	// Look for external networks, in the same AZ as the server's networks,
    90  	// to use for the FIP.
    91  	detail, err := n.nova().GetServer(string(id))
    92  	if err != nil {
    93  		return nil, errors.Trace(err)
    94  	}
    95  	extNetworkIds, err := n.getExternalNetworkIDsFromHostAddrs(detail.Addresses)
    96  	if err != nil {
    97  		return nil, errors.Trace(err)
    98  	}
    99  
   100  	// Look for FIPs in same project as the credentials.
   101  	// Admins have visibility into other projects.
   102  	fips, err := n.neutron().ListFloatingIPsV2(projectIDFilter(n.client().TenantId()))
   103  	if err != nil {
   104  		return nil, errors.Trace(err)
   105  	}
   106  
   107  	// Is there an unused FloatingIP on an external network
   108  	// in the instance's availability zone?
   109  	for _, fip := range fips {
   110  		if fip.FixedIP == "" {
   111  			// Not a perfect solution.  If an external network was specified in
   112  			// the config, it'll be at the top of the extNetworkIds, but may be
   113  			// not used if the available FIP isn't it in. However the instance
   114  			// and the FIP will be in the same availability zone.
   115  			for _, extNetId := range extNetworkIds {
   116  				if fip.FloatingNetworkId == extNetId {
   117  					logger.Debugf("found unassigned public ip: %v", fip.IP)
   118  					return &fip.IP, nil
   119  				}
   120  			}
   121  		}
   122  	}
   123  
   124  	// No unused FIPs exist, allocate a new IP and use it.
   125  	var lastErr error
   126  	for _, extNetId := range extNetworkIds {
   127  		var newfip *neutron.FloatingIPV2
   128  		newfip, lastErr = n.neutron().AllocateFloatingIPV2(extNetId)
   129  		if lastErr == nil {
   130  			logger.Debugf("allocated new public IP: %s", newfip.IP)
   131  			return &newfip.IP, nil
   132  		}
   133  	}
   134  
   135  	logger.Debugf("Unable to allocate a public IP")
   136  	return nil, lastErr
   137  }
   138  
   139  // getExternalNetworkIDsFromHostAddrs returns a slice of external network IDs.
   140  // If specified, the configured external network is returned. Otherwise search
   141  // for an external network in the same availability zones as the provided
   142  // server addresses.
   143  func (n *NeutronNetworking) getExternalNetworkIDsFromHostAddrs(addrs map[string][]nova.IPAddress) ([]string, error) {
   144  	var extNetworkIds []string
   145  	externalNetwork := n.ecfg().externalNetwork()
   146  	if externalNetwork != "" {
   147  		// The config specified an external network, try it first.
   148  		networks, err := n.ResolveNetworks(externalNetwork, true)
   149  		if err != nil {
   150  			logger.Warningf("resolving configured external network %q: %s", externalNetwork, err.Error())
   151  		} else {
   152  			logger.Debugf("using external network %q", externalNetwork)
   153  			toID := func(n neutron.NetworkV2) string { return n.Id }
   154  			extNetworkIds = transform.Slice(networks, toID)
   155  		}
   156  	}
   157  
   158  	// We have a single external network ID, no need to search.
   159  	if len(extNetworkIds) == 1 {
   160  		return extNetworkIds, nil
   161  	}
   162  
   163  	logger.Debugf("unique match for external network %q not found; searching for one", externalNetwork)
   164  
   165  	hostAddrAZs, err := n.findNetworkAZForHostAddrs(addrs)
   166  	if err != nil {
   167  		return nil, errors.NewNotFound(nil,
   168  			fmt.Sprintf("could not find an external network in availability zone(s) %q", hostAddrAZs.SortedValues()))
   169  	}
   170  
   171  	// Create slice of network.Ids for external networks in the same AZ as
   172  	// the instance's networks, to find an existing floating ip in, or allocate
   173  	// a new floating ip from.
   174  	extNetIds, _ := getExternalNeutronNetworksByAZ(n, hostAddrAZs)
   175  
   176  	// We have an external network ID, no need for specific error message.
   177  	if len(extNetIds) > 0 {
   178  		return extNetIds, nil
   179  	}
   180  
   181  	return nil, errors.NewNotFound(nil,
   182  		fmt.Sprintf("could not find an external network in availability zone(s) %q", strings.Join(hostAddrAZs.SortedValues(), ", ")))
   183  }
   184  
   185  func (n *NeutronNetworking) findNetworkAZForHostAddrs(addrs map[string][]nova.IPAddress) (set.Strings, error) {
   186  	netNames := set.NewStrings()
   187  	for name := range addrs {
   188  		netNames.Add(name)
   189  	}
   190  	if netNames.Size() == 0 {
   191  		return nil, errors.NotFoundf("no networks found with server")
   192  	}
   193  	azNames := set.NewStrings()
   194  	networks, err := n.neutron().ListNetworksV2(internalNetworkFilter())
   195  	if err != nil {
   196  		return nil, err
   197  	}
   198  	for _, network := range networks {
   199  		if netNames.Contains(network.Name) {
   200  			azNames = azNames.Union(set.NewStrings(network.AvailabilityZones...))
   201  		}
   202  	}
   203  	return azNames, nil
   204  }
   205  
   206  // getExternalNeutronNetworksByAZ returns all external networks within the
   207  // given availability zones. If azName is empty, return all external networks
   208  // with no AZ.  If no network has an AZ, return all external networks.
   209  func getExternalNeutronNetworksByAZ(e NetworkingBase, azNames set.Strings) ([]string, error) {
   210  	neutronClient := e.neutron()
   211  	// Find all external networks in availability zone.
   212  	networks, err := neutronClient.ListNetworksV2(externalNetworkFilter())
   213  	if err != nil {
   214  		return nil, errors.Trace(err)
   215  	}
   216  	netIds := make([]string, 0)
   217  	for _, network := range networks {
   218  		for _, netAZ := range network.AvailabilityZones {
   219  			if azNames.Contains(netAZ) {
   220  				netIds = append(netIds, network.Id)
   221  				break
   222  			}
   223  		}
   224  		if azNames.IsEmpty() || len(network.AvailabilityZones) == 0 {
   225  			logger.Debugf(
   226  				"Adding %q to potential external networks for Floating IPs, no availability zones found", network.Name)
   227  			netIds = append(netIds, network.Id)
   228  		}
   229  	}
   230  	if len(netIds) == 0 {
   231  		return nil, errors.NewNotFound(nil, "No External networks found to allocate a Floating IP")
   232  	}
   233  	return netIds, nil
   234  }
   235  
   236  // CreatePort creates a port for a given network id with a subnet ID.
   237  func (n *NeutronNetworking) CreatePort(name, networkID string, subnetID corenetwork.Id) (*neutron.PortV2, error) {
   238  	client := n.neutron()
   239  
   240  	// To prevent name clashes to existing ports, generate a unique one from a
   241  	// given name.
   242  	portName := generateUniquePortName(name)
   243  
   244  	port, err := client.CreatePortV2(neutron.PortV2{
   245  		Name:        portName,
   246  		Description: "Port created by juju for space aware networking",
   247  		NetworkId:   networkID,
   248  		FixedIPs: []neutron.PortFixedIPsV2{
   249  			{
   250  				SubnetID: subnetID.String(),
   251  			},
   252  		},
   253  	})
   254  	if err != nil {
   255  		return nil, errors.Annotate(err, "unable to create port")
   256  	}
   257  	return port, nil
   258  }
   259  
   260  // DeletePortByID attempts to remove a port using the given port ID.
   261  func (n *NeutronNetworking) DeletePortByID(portID string) error {
   262  	client := n.neutron()
   263  
   264  	return client.DeletePortV2(portID)
   265  }
   266  
   267  // FindNetworks returns a set of internal or external network names
   268  // depending on the provided argument.
   269  func (n *NeutronNetworking) FindNetworks(internal bool) (set.Strings, error) {
   270  	var filter *neutron.Filter
   271  	switch internal {
   272  	case true:
   273  		filter = internalNetworkFilter()
   274  	case false:
   275  		filter = externalNetworkFilter()
   276  	}
   277  	client := n.neutron()
   278  	networks, err := client.ListNetworksV2(filter)
   279  	if err != nil {
   280  		return nil, errors.Trace(err)
   281  	}
   282  	names := set.NewStrings()
   283  	for _, network := range networks {
   284  		names.Add(network.Name)
   285  	}
   286  	return names, nil
   287  }
   288  
   289  // ResolveNetworks is part of the Networking interface.
   290  func (n *NeutronNetworking) ResolveNetworks(name string, external bool) ([]neutron.NetworkV2, error) {
   291  	if utils.IsValidUUIDString(name) {
   292  		// NOTE: There is an OpenStack cloud, "whitestack", which has the
   293  		// network used to create servers specified as an External network,
   294  		// contrary to how all the other OpenStacks that we know of work.
   295  		// Here we just retrieve the network, regardless of whether it is
   296  		// internal or external
   297  		net, err := n.neutron().GetNetworkV2(name)
   298  		if err != nil {
   299  			return nil, errors.Trace(err)
   300  		}
   301  		return []neutron.NetworkV2{*net}, nil
   302  	}
   303  
   304  	// Prior to OpenStack Rocky, empty strings in the neutron filters were
   305  	// ignored. So name="" AND external-router=false returned a list of all
   306  	// internal networks.
   307  	// If the list was length one, the OpenStack provider would use it
   308  	// without explicit user configuration.
   309  	//
   310  	// Rocky introduced an optional extension to neutron:
   311  	// empty-string-filtering.
   312  	// If configured, it means the empty string must be explicitly matched.
   313  	//
   314  	// To preserve the prior behavior, if the configured name is empty,
   315  	// look for all networks matching the `external` argument for the
   316  	// OpenStack project Juju is using.
   317  	var filter *neutron.Filter
   318  	switch {
   319  	case name == "" && !external:
   320  		filter = internalNetworkFilter()
   321  	case name == "" && external:
   322  		filter = externalNetworkFilter()
   323  	default:
   324  		filter = networkFilter(name, external)
   325  	}
   326  
   327  	networks, err := n.neutron().ListNetworksV2(filter)
   328  	return networks, errors.Trace(err)
   329  }
   330  
   331  func generateUniquePortName(name string) string {
   332  	unique := utils.RandomString(8, append(utils.LowerAlpha, utils.Digits...))
   333  	return fmt.Sprintf("juju-%s-%s", name, unique)
   334  }
   335  
   336  func makeSubnetInfo(neutron NetworkingNeutron, subnet neutron.SubnetV2) (corenetwork.SubnetInfo, error) {
   337  	_, _, err := net.ParseCIDR(subnet.Cidr)
   338  	if err != nil {
   339  		return corenetwork.SubnetInfo{}, errors.Annotatef(err, "skipping subnet %q, invalid CIDR", subnet.Cidr)
   340  	}
   341  	net, err := neutron.GetNetworkV2(subnet.NetworkId)
   342  	if err != nil {
   343  		return corenetwork.SubnetInfo{}, err
   344  	}
   345  
   346  	// TODO (hml) 2017-03-20:
   347  	// With goose updates, VLANTag can be updated to be
   348  	// network.segmentation_id, if network.network_type equals vlan
   349  	info := corenetwork.SubnetInfo{
   350  		CIDR:              subnet.Cidr,
   351  		ProviderId:        corenetwork.Id(subnet.Id),
   352  		ProviderNetworkId: corenetwork.Id(subnet.NetworkId),
   353  		VLANTag:           0,
   354  		AvailabilityZones: net.AvailabilityZones,
   355  	}
   356  	logger.Tracef("found subnet with info %#v", info)
   357  	return info, nil
   358  }
   359  
   360  // Subnets returns basic information about the specified subnets known
   361  // by the provider for the specified instance or list of ids. subnetIds can be
   362  // empty, in which case all known are returned.
   363  func (n *NeutronNetworking) Subnets(instId instance.Id, subnetIds []corenetwork.Id) ([]corenetwork.SubnetInfo, error) {
   364  	netIds := set.NewStrings()
   365  	internalNets := n.ecfg().networks()
   366  
   367  	for _, iNet := range internalNets {
   368  		networks, err := n.ResolveNetworks(iNet, false)
   369  		if err != nil {
   370  			logger.Warningf("could not resolve internal network id for %q: %v", iNet, err)
   371  			continue
   372  		}
   373  		for _, net := range networks {
   374  			netIds.Add(net.Id)
   375  		}
   376  	}
   377  
   378  	// Note, there are cases where we will detect an external
   379  	// network without it being explicitly configured by the user.
   380  	// When we get to a point where we start detecting spaces for users
   381  	// on Openstack, we'll probably need to include better logic here.
   382  	externalNet := n.ecfg().externalNetwork()
   383  	if externalNet != "" {
   384  		networks, err := n.ResolveNetworks(externalNet, true)
   385  		if err != nil {
   386  			logger.Warningf("could not resolve external network id for %q: %v", externalNet, err)
   387  		} else {
   388  			for _, net := range networks {
   389  				netIds.Add(net.Id)
   390  			}
   391  		}
   392  	}
   393  
   394  	logger.Debugf("finding subnets in networks: %s", strings.Join(netIds.Values(), ", "))
   395  
   396  	subIdSet := set.NewStrings()
   397  	for _, subId := range subnetIds {
   398  		subIdSet.Add(string(subId))
   399  	}
   400  
   401  	var results []corenetwork.SubnetInfo
   402  	if instId != instance.UnknownId {
   403  		// TODO(hml): 2017-03-20
   404  		// Implement Subnets() for case where instId is specified
   405  		return nil, errors.NotSupportedf("neutron subnets with instance Id")
   406  	} else {
   407  		// TODO(jam): 2018-05-23 It is likely that ListSubnetsV2 could
   408  		// take a Filter rather that doing the filtering client side.
   409  		neutron := n.neutron()
   410  		subnets, err := neutron.ListSubnetsV2()
   411  		if err != nil {
   412  			return nil, errors.Annotatef(err, "failed to retrieve subnets")
   413  		}
   414  		if len(subnetIds) == 0 {
   415  			for _, subnet := range subnets {
   416  				// TODO (manadart 2018-07-17): If there was an error resolving
   417  				// an internal network ID, then no subnets will be discovered.
   418  				// The user will get an error attempting to add machines to
   419  				// this model and will have to update model config with a
   420  				// network name; but this does not re-discover the subnets.
   421  				// If subnets/spaces become important, we will have to address
   422  				// this somehow.
   423  				if !netIds.Contains(subnet.NetworkId) {
   424  					logger.Tracef("ignoring subnet %q, part of network %q", subnet.Id, subnet.NetworkId)
   425  					continue
   426  				}
   427  				subIdSet.Add(subnet.Id)
   428  			}
   429  		}
   430  		for _, subnet := range subnets {
   431  			if !subIdSet.Contains(subnet.Id) {
   432  				logger.Tracef("subnet %q not in %v, skipping", subnet.Id, subnetIds)
   433  				continue
   434  			}
   435  			subIdSet.Remove(subnet.Id)
   436  			if info, err := makeSubnetInfo(neutron, subnet); err == nil {
   437  				// Error will already have been logged.
   438  				results = append(results, info)
   439  			}
   440  		}
   441  	}
   442  	if !subIdSet.IsEmpty() {
   443  		return nil, errors.Errorf("failed to find the following subnet ids: %v", subIdSet.Values())
   444  	}
   445  	return results, nil
   446  }
   447  
   448  // NetworkInterfaces implements environs.NetworkingEnviron. It returns a
   449  // slice where the i_th element contains the list of network interfaces
   450  // for the i_th input instance ID.
   451  //
   452  // If none of the provided instance IDs exist, ErrNoInstances will be returned.
   453  // If only a subset of the instance IDs exist, the result will contain a nil
   454  // value for the missing instances and a ErrPartialInstances error will be
   455  // returned.
   456  func (n *NeutronNetworking) NetworkInterfaces(instanceIDs []instance.Id) ([]corenetwork.InterfaceInfos, error) {
   457  	allSubnets, err := n.Subnets(instance.UnknownId, nil)
   458  	if err != nil {
   459  		return nil, errors.Annotate(err, "listing subnets")
   460  	}
   461  	subnetIDToCIDR := make(map[string]string)
   462  	for _, sub := range allSubnets {
   463  		subnetIDToCIDR[sub.ProviderId.String()] = sub.CIDR
   464  	}
   465  
   466  	neutronClient := n.neutron()
   467  	filter := projectIDFilter(n.client().TenantId())
   468  
   469  	fips, err := neutronClient.ListFloatingIPsV2(filter)
   470  	if err != nil {
   471  		return nil, errors.Annotate(err, "listing floating IPs")
   472  	}
   473  
   474  	// Map private IP to public IP for every assigned FIP.
   475  	fixedToFIP := make(map[string]string)
   476  	for _, fip := range fips {
   477  		if fip.FixedIP == "" {
   478  			continue
   479  		}
   480  		fixedToFIP[fip.FixedIP] = fip.IP
   481  	}
   482  
   483  	allInstPorts, err := neutronClient.ListPortsV2(filter)
   484  	if err != nil {
   485  		return nil, errors.Annotate(err, "listing ports")
   486  	}
   487  
   488  	// Group ports by device ID.
   489  	instIfaceMap := make(map[instance.Id][]neutron.PortV2)
   490  	for _, instPort := range allInstPorts {
   491  		devID := instance.Id(instPort.DeviceId)
   492  		instIfaceMap[devID] = append(instIfaceMap[devID], instPort)
   493  	}
   494  
   495  	res := make([]corenetwork.InterfaceInfos, len(instanceIDs))
   496  	var matchCount int
   497  
   498  	for resIdx, instID := range instanceIDs {
   499  		ifaceList, found := instIfaceMap[instID]
   500  		if !found {
   501  			continue
   502  		}
   503  
   504  		matchCount++
   505  		res[resIdx] = mapInterfaceList(ifaceList, subnetIDToCIDR, fixedToFIP)
   506  	}
   507  
   508  	if matchCount == 0 {
   509  		return nil, environs.ErrNoInstances
   510  	} else if matchCount < len(instanceIDs) {
   511  		return res, environs.ErrPartialInstances
   512  	}
   513  
   514  	return res, nil
   515  }
   516  
   517  func mapInterfaceList(
   518  	in []neutron.PortV2, subnetIDToCIDR, fixedToFIP map[string]string,
   519  ) network.InterfaceInfos {
   520  	var out = make(corenetwork.InterfaceInfos, len(in))
   521  
   522  	for idx, port := range in {
   523  		ni := corenetwork.InterfaceInfo{
   524  			DeviceIndex:       idx,
   525  			ProviderId:        corenetwork.Id(port.Id),
   526  			ProviderNetworkId: corenetwork.Id(port.NetworkId),
   527  			// NOTE(achilleasa): on microstack port.Name is always empty.
   528  			InterfaceName: port.Name,
   529  			Disabled:      port.Status != "ACTIVE",
   530  			NoAutoStart:   false,
   531  			InterfaceType: corenetwork.EthernetDevice,
   532  			Origin:        corenetwork.OriginProvider,
   533  			MACAddress:    corenetwork.NormalizeMACAddress(port.MACAddress),
   534  		}
   535  
   536  		for i, ipConf := range port.FixedIPs {
   537  			providerAddr := corenetwork.NewMachineAddress(
   538  				ipConf.IPAddress,
   539  				corenetwork.WithConfigType(corenetwork.ConfigStatic),
   540  				corenetwork.WithCIDR(subnetIDToCIDR[ipConf.SubnetID]),
   541  			).AsProviderAddress()
   542  
   543  			ni.Addresses = append(ni.Addresses, providerAddr)
   544  
   545  			// If there is a FIP associated with this private IP,
   546  			// add it as a public address.
   547  			if fip := fixedToFIP[ipConf.IPAddress]; fip != "" {
   548  				ni.ShadowAddresses = append(ni.ShadowAddresses, corenetwork.NewMachineAddress(
   549  					fip,
   550  					corenetwork.WithScope(corenetwork.ScopePublic),
   551  					// TODO (manadart 2022-02-08): Other providers add these
   552  					// addresses with the DHCP config type.
   553  					// But this is not really correct.
   554  					// We should consider another type for what are in effect
   555  					// NATing arrangements, that better conveys the topology.
   556  				).AsProviderAddress())
   557  			}
   558  
   559  			// If this is the first address, populate additional NIC details.
   560  			if i == 0 {
   561  				ni.ProviderSubnetId = corenetwork.Id(ipConf.SubnetID)
   562  			}
   563  		}
   564  
   565  		out[idx] = ni
   566  	}
   567  
   568  	return out
   569  }
   570  
   571  func networkForSubnet(networks []neutron.NetworkV2, subnetID network.Id) (neutron.NetworkV2, error) {
   572  	for _, neutronNet := range networks {
   573  		for _, netSubnetID := range neutronNet.SubnetIds {
   574  			if netSubnetID == subnetID.String() {
   575  				return neutronNet, nil
   576  			}
   577  		}
   578  	}
   579  
   580  	return neutron.NetworkV2{}, errors.NotFoundf("network for subnet %q", subnetID)
   581  }