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

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package azure
     5  
     6  import (
     7  	"fmt"
     8  	"math/rand"
     9  	"strings"
    10  
    11  	azurenetwork "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/names/v5"
    14  
    15  	"github.com/juju/juju/core/instance"
    16  	"github.com/juju/juju/core/network"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/context"
    19  	"github.com/juju/juju/provider/azure/internal/errorutils"
    20  )
    21  
    22  var _ environs.NetworkingEnviron = &azureEnviron{}
    23  
    24  // SupportsSpaces implements environs.NetworkingEnviron.
    25  func (env *azureEnviron) SupportsSpaces(context.ProviderCallContext) (bool, error) {
    26  	return true, nil
    27  }
    28  
    29  func (env *azureEnviron) networkInfo() (vnetRG string, vnetName string) {
    30  	// The virtual network to use defaults to "juju-internal-network"
    31  	// but may also be specified by the user.
    32  	vnetName = internalNetworkName
    33  	vnetRG = env.resourceGroup
    34  	if env.config.virtualNetworkName != "" {
    35  		// network may be "mynetwork" or "resourceGroup/mynetwork"
    36  		parts := strings.Split(env.config.virtualNetworkName, "/")
    37  		vnetName = parts[0]
    38  		if len(parts) > 1 {
    39  			vnetRG = parts[0]
    40  			vnetName = parts[1]
    41  		}
    42  		logger.Debugf("user specified network name %q in resource group %q", vnetName, vnetRG)
    43  	}
    44  	return
    45  }
    46  
    47  // Subnets implements environs.NetworkingEnviron.
    48  func (env *azureEnviron) Subnets(
    49  	ctx context.ProviderCallContext, instanceID instance.Id, _ []network.Id) ([]network.SubnetInfo, error) {
    50  	if instanceID != instance.UnknownId {
    51  		return nil, errors.NotSupportedf("subnets for instance")
    52  	}
    53  	subnets, err := env.allSubnets(ctx)
    54  	return subnets, errorutils.HandleCredentialError(err, ctx)
    55  }
    56  
    57  func (env *azureEnviron) allProviderSubnets(ctx context.ProviderCallContext) ([]*azurenetwork.Subnet, error) {
    58  	// Subnet discovery happens immediately after model creation.
    59  	// We need to ensure that the asynchronously invoked resource creation has
    60  	// completed and added our networking assets.
    61  	if err := env.waitCommonResourcesCreated(ctx); err != nil {
    62  		return nil, errors.Annotate(
    63  			err, "waiting for common resources to be created",
    64  		)
    65  	}
    66  
    67  	subnets, err := env.subnetsClient()
    68  	if err != nil {
    69  		return nil, errors.Trace(err)
    70  	}
    71  	vnetRG, vnetName := env.networkInfo()
    72  	var result []*azurenetwork.Subnet
    73  	pager := subnets.NewListPager(vnetRG, vnetName, nil)
    74  	for pager.More() {
    75  		next, err := pager.NextPage(ctx)
    76  		if err != nil {
    77  			return nil, errors.Trace(err)
    78  		}
    79  		result = append(result, next.Value...)
    80  	}
    81  	return result, nil
    82  }
    83  
    84  func (env *azureEnviron) allSubnets(ctx context.ProviderCallContext) ([]network.SubnetInfo, error) {
    85  	values, err := env.allProviderSubnets(ctx)
    86  	if err != nil {
    87  		return nil, errors.Trace(err)
    88  	}
    89  
    90  	var results []network.SubnetInfo
    91  	for _, sub := range values {
    92  		id := toValue(sub.ID)
    93  		if sub.Properties == nil {
    94  			continue
    95  		}
    96  		// An empty CIDR is no use to us, so guard against it.
    97  		cidr := toValue(sub.Properties.AddressPrefix)
    98  		if cidr == "" {
    99  			logger.Debugf("ignoring subnet %q with empty address prefix", id)
   100  			continue
   101  		}
   102  
   103  		results = append(results, network.SubnetInfo{
   104  			CIDR:       cidr,
   105  			ProviderId: network.Id(id),
   106  		})
   107  	}
   108  	return results, nil
   109  }
   110  
   111  func (env *azureEnviron) allPublicIPs(ctx context.ProviderCallContext) (map[string]network.ProviderAddress, error) {
   112  	idToIPMap := make(map[string]network.ProviderAddress)
   113  
   114  	pipClient, err := env.publicAddressesClient()
   115  	if err != nil {
   116  		return nil, errors.Trace(err)
   117  	}
   118  	pager := pipClient.NewListPager(env.resourceGroup, nil)
   119  	for pager.More() {
   120  		next, err := pager.NextPage(ctx)
   121  		if err != nil {
   122  			return nil, errors.Trace(err)
   123  		}
   124  		for _, ipRes := range next.Value {
   125  			if ipRes.ID == nil || ipRes.Properties == nil || ipRes.Properties.IPAddress == nil {
   126  				continue
   127  			}
   128  
   129  			var cfgMethod = network.ConfigDHCP
   130  			if toValue(ipRes.Properties.PublicIPAllocationMethod) == azurenetwork.IPAllocationMethodStatic {
   131  				cfgMethod = network.ConfigStatic
   132  			}
   133  
   134  			idToIPMap[*ipRes.ID] = network.NewMachineAddress(
   135  				toValue(ipRes.Properties.IPAddress),
   136  				network.WithConfigType(cfgMethod),
   137  			).AsProviderAddress()
   138  		}
   139  	}
   140  
   141  	return idToIPMap, nil
   142  }
   143  
   144  // SuperSubnets implements environs.NetworkingEnviron.
   145  func (env *azureEnviron) SuperSubnets(context.ProviderCallContext) ([]string, error) {
   146  	return nil, errors.NotSupportedf("super subnets")
   147  }
   148  
   149  // SupportsContainerAddresses implements environs.NetworkingEnviron.
   150  func (env *azureEnviron) SupportsContainerAddresses(context.ProviderCallContext) (bool, error) {
   151  	return false, nil
   152  }
   153  
   154  // AllocateContainerAddresses implements environs.NetworkingEnviron.
   155  func (env *azureEnviron) AllocateContainerAddresses(
   156  	context.ProviderCallContext, instance.Id, names.MachineTag, network.InterfaceInfos,
   157  ) (network.InterfaceInfos, error) {
   158  	return nil, errors.NotSupportedf("container addresses")
   159  }
   160  
   161  // ReleaseContainerAddresses implements environs.NetworkingEnviron.
   162  func (env *azureEnviron) ReleaseContainerAddresses(context.ProviderCallContext, []network.ProviderInterfaceInfo) error {
   163  	return errors.NotSupportedf("container addresses")
   164  }
   165  
   166  // AreSpacesRoutable implements environs.NetworkingEnviron.
   167  func (*azureEnviron) AreSpacesRoutable(_ context.ProviderCallContext, _, _ *environs.ProviderSpaceInfo) (bool, error) {
   168  	return false, nil
   169  }
   170  
   171  // NetworkInterfaces implements environs.NetworkingEnviron. It returns back
   172  // a slice where the i_th element contains the list of network interfaces
   173  // for the i_th provided instance ID.
   174  //
   175  // If none of the provided instance IDs exist, ErrNoInstances will be returned.
   176  // If only a subset of the instance IDs exist, the result will contain a nil
   177  // value for the missing instances and a ErrPartialInstances error will be
   178  // returned.
   179  func (env *azureEnviron) NetworkInterfaces(ctx context.ProviderCallContext, instanceIDs []instance.Id) ([]network.InterfaceInfos, error) {
   180  	// Create a subnet (provider) ID to CIDR map so we can identify the
   181  	// subnet for each NIC address when mapping azure NIC details.
   182  	allSubnets, err := env.allSubnets(ctx)
   183  	if err != nil {
   184  		return nil, errors.Trace(err)
   185  	}
   186  	subnetIDToCIDR := make(map[string]string)
   187  	for _, sub := range allSubnets {
   188  		subnetIDToCIDR[sub.ProviderId.String()] = sub.CIDR
   189  	}
   190  
   191  	instIfaceMap, err := env.instanceNetworkInterfaces(ctx, env.resourceGroup)
   192  	if err != nil {
   193  		return nil, errors.Trace(err)
   194  	}
   195  
   196  	// Create a map of azure IP address IDs to provider addresses. We will
   197  	// use this information to associate public IP addresses with NICs
   198  	// when mapping the obtained azure NIC list.
   199  	ipMap, err := env.allPublicIPs(ctx)
   200  	if err != nil {
   201  		return nil, errors.Trace(err)
   202  	}
   203  
   204  	var (
   205  		res        = make([]network.InterfaceInfos, len(instanceIDs))
   206  		matchCount int
   207  	)
   208  
   209  	for resIdx, instID := range instanceIDs {
   210  		azInterfaceList, found := instIfaceMap[instID]
   211  		if !found {
   212  			continue
   213  		}
   214  
   215  		matchCount++
   216  		res[resIdx] = mapAzureInterfaceList(azInterfaceList, subnetIDToCIDR, ipMap)
   217  	}
   218  
   219  	if matchCount == 0 {
   220  		return nil, environs.ErrNoInstances
   221  	} else if matchCount < len(instanceIDs) {
   222  		return res, environs.ErrPartialInstances
   223  	}
   224  
   225  	return res, nil
   226  }
   227  
   228  func mapAzureInterfaceList(in []*azurenetwork.Interface, subnetIDToCIDR map[string]string, ipMap map[string]network.ProviderAddress) network.InterfaceInfos {
   229  	var out = make(network.InterfaceInfos, len(in))
   230  
   231  	for idx, azif := range in {
   232  		ni := network.InterfaceInfo{
   233  			DeviceIndex:   idx,
   234  			Disabled:      false,
   235  			NoAutoStart:   false,
   236  			InterfaceType: network.EthernetDevice,
   237  			Origin:        network.OriginProvider,
   238  		}
   239  
   240  		if azif.Properties != nil && azif.Properties.MacAddress != nil {
   241  			ni.MACAddress = network.NormalizeMACAddress(toValue(azif.Properties.MacAddress))
   242  		}
   243  		if azif.ID != nil {
   244  			ni.ProviderId = network.Id(*azif.ID)
   245  		}
   246  
   247  		if azif.Properties == nil || azif.Properties.IPConfigurations == nil {
   248  			out[idx] = ni
   249  			continue
   250  		}
   251  
   252  		for _, ipConf := range azif.Properties.IPConfigurations {
   253  			if ipConf.Properties == nil {
   254  				continue
   255  			}
   256  
   257  			isPrimary := ipConf.Properties.Primary != nil && toValue(ipConf.Properties.Primary)
   258  
   259  			// Azure does not include the public IP address values
   260  			// but it does provide us with the ID of any assigned
   261  			// public addresses which we can use to index the ipMap.
   262  			if ipConf.Properties.PublicIPAddress != nil && ipConf.Properties.PublicIPAddress.ID != nil {
   263  				if providerAddr, found := ipMap[toValue(ipConf.Properties.PublicIPAddress.ID)]; found {
   264  					// If this a primary address make sure it appears
   265  					// at the top of the shadow address list.
   266  					if isPrimary {
   267  						ni.ShadowAddresses = append(network.ProviderAddresses{providerAddr}, ni.ShadowAddresses...)
   268  						ni.ConfigType = providerAddr.AddressConfigType()
   269  					} else {
   270  						ni.ShadowAddresses = append(ni.ShadowAddresses, providerAddr)
   271  					}
   272  				}
   273  			}
   274  
   275  			// Check if the configuration also includes a private address component.
   276  			if ipConf.Properties.PrivateIPAddress == nil {
   277  				continue
   278  			}
   279  
   280  			var cfgMethod = network.ConfigDHCP
   281  			if toValue(ipConf.Properties.PrivateIPAllocationMethod) == azurenetwork.IPAllocationMethodStatic {
   282  				cfgMethod = network.ConfigStatic
   283  			}
   284  
   285  			addrOpts := []func(network.AddressMutator){
   286  				network.WithScope(network.ScopeCloudLocal),
   287  				network.WithConfigType(cfgMethod),
   288  			}
   289  
   290  			var subnetID string
   291  			if ipConf.Properties.Subnet != nil && ipConf.Properties.Subnet.ID != nil {
   292  				subnetID = toValue(ipConf.Properties.Subnet.ID)
   293  				if subnetCIDR := subnetIDToCIDR[subnetID]; subnetCIDR != "" {
   294  					addrOpts = append(addrOpts, network.WithCIDR(subnetCIDR))
   295  				}
   296  			}
   297  
   298  			providerAddr := network.NewMachineAddress(
   299  				toValue(ipConf.Properties.PrivateIPAddress),
   300  				addrOpts...,
   301  			).AsProviderAddress()
   302  
   303  			// If this is the primary address ensure that it appears
   304  			// at the top of the address list.
   305  			if isPrimary {
   306  				ni.Addresses = append(network.ProviderAddresses{providerAddr}, ni.Addresses...)
   307  			} else {
   308  				ni.Addresses = append(ni.Addresses, providerAddr)
   309  			}
   310  
   311  			// Record the subnetID and config mode to the NIC instance
   312  			if isPrimary && subnetID != "" {
   313  				ni.ProviderSubnetId = network.Id(subnetID)
   314  				ni.ConfigType = cfgMethod
   315  			}
   316  		}
   317  
   318  		out[idx] = ni
   319  	}
   320  
   321  	return out
   322  }
   323  
   324  // defaultControllerSubnet returns the subnet to use for starting a controller
   325  // if not otherwise specified using a placement directive.
   326  func (env *azureEnviron) defaultControllerSubnet() network.Id {
   327  	// By default, controller and non-controller machines are assigned to separate
   328  	// subnets. This enables us to create controller-specific NSG rules
   329  	// just by targeting the controller subnet.
   330  
   331  	vnetRG, vnetName := env.networkInfo()
   332  	subnetID := fmt.Sprintf(
   333  		`[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]`,
   334  		vnetName, controllerSubnetName,
   335  	)
   336  	if vnetRG != "" {
   337  		subnetID = fmt.Sprintf(
   338  			`[concat(resourceId('%s', 'Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]`,
   339  			vnetRG, vnetName, controllerSubnetName,
   340  		)
   341  	}
   342  	return network.Id(subnetID)
   343  }
   344  
   345  func (env *azureEnviron) findSubnetID(ctx context.ProviderCallContext, subnetName string) (network.Id, error) {
   346  	subnets, err := env.allProviderSubnets(ctx)
   347  	if err != nil {
   348  		return "", errorutils.HandleCredentialError(err, ctx)
   349  	}
   350  	for _, subnet := range subnets {
   351  		if toValue(subnet.Name) == subnetName {
   352  			return network.Id(toValue(subnet.ID)), nil
   353  		}
   354  	}
   355  	return "", errors.NotFoundf("subnet %q", subnetName)
   356  }
   357  
   358  // networkInfoForInstance returns the virtual network and subnet to use
   359  // when provisioning an instance.
   360  func (env *azureEnviron) networkInfoForInstance(
   361  	ctx context.ProviderCallContext,
   362  	args environs.StartInstanceParams,
   363  	bootstrapping, controller bool,
   364  	placementSubnetID network.Id,
   365  ) (vnetID string, subnetIDs []network.Id, _ error) {
   366  
   367  	vnetRG, vnetName := env.networkInfo()
   368  	vnetID = fmt.Sprintf(`[resourceId('Microsoft.Network/virtualNetworks', '%s')]`, vnetName)
   369  	if vnetRG != "" {
   370  		vnetID = fmt.Sprintf(`[resourceId('%s', 'Microsoft.Network/virtualNetworks', '%s')]`, vnetRG, vnetName)
   371  	}
   372  
   373  	constraints := args.Constraints
   374  
   375  	// We'll collect all the possible subnets and pick one
   376  	// based on constraints and placement.
   377  	var possibleSubnets [][]network.Id
   378  
   379  	if !constraints.HasSpaces() {
   380  		// Use placement if specified.
   381  		if placementSubnetID != "" {
   382  			return vnetID, []network.Id{placementSubnetID}, nil
   383  		}
   384  
   385  		// When bootstrapping the network doesn't exist yet so just
   386  		// return the relevant subnet ID and it is created as part of
   387  		// the bootstrap process.
   388  		if bootstrapping && env.config.virtualNetworkName == "" {
   389  			return vnetID, []network.Id{env.defaultControllerSubnet()}, nil
   390  		}
   391  
   392  		// Prefer the legacy default subnet if found.
   393  		defaultSubnetName := internalSubnetName
   394  		if controller {
   395  			defaultSubnetName = controllerSubnetName
   396  		}
   397  		defaultSubnetID, err := env.findSubnetID(ctx, defaultSubnetName)
   398  		if err != nil && !errors.IsNotFound(err) {
   399  			return "", nil, errors.Trace(err)
   400  		}
   401  		if err == nil {
   402  			return vnetID, []network.Id{defaultSubnetID}, nil
   403  		}
   404  
   405  		// For deployments without a spaces constraint, there's no subnets to zones mapping.
   406  		// So get all accessible subnets.
   407  		allSubnets, err := env.allSubnets(ctx)
   408  		if err != nil {
   409  			return "", nil, errorutils.HandleCredentialError(errors.Trace(err), ctx)
   410  		}
   411  		subnetIds := make([]network.Id, len(allSubnets))
   412  		for i, subnet := range allSubnets {
   413  			subnetIds[i] = subnet.ProviderId
   414  		}
   415  		possibleSubnets = [][]network.Id{subnetIds}
   416  	} else {
   417  		var err error
   418  		// Attempt to filter the subnet IDs for the configured availability zone.
   419  		// If there is no configured zone, consider all subnet IDs.
   420  		possibleSubnets, err = env.subnetsForZone(args.SubnetsToZones, args.AvailabilityZone)
   421  		if err != nil {
   422  			return "", nil, errors.Trace(err)
   423  		}
   424  	}
   425  
   426  	// For each list of subnet IDs that satisfy space and zone constraints,
   427  	// choose a single one at random.
   428  	var subnetIDForZone []network.Id
   429  	for _, zoneSubnetIDs := range possibleSubnets {
   430  		// Use placement to select a single subnet if needed.
   431  		var subnetIDs []network.Id
   432  		for _, id := range zoneSubnetIDs {
   433  			if placementSubnetID == "" || placementSubnetID == id {
   434  				subnetIDs = append(subnetIDs, id)
   435  			}
   436  		}
   437  		if len(subnetIDs) == 1 {
   438  			subnetIDForZone = append(subnetIDForZone, subnetIDs[0])
   439  			continue
   440  		} else if len(subnetIDs) > 0 {
   441  			subnetIDForZone = append(subnetIDForZone, subnetIDs[rand.Intn(len(subnetIDs))])
   442  		}
   443  	}
   444  	if len(subnetIDForZone) == 0 {
   445  		return "", nil, errors.NotFoundf("subnet for constraint %q", constraints.String())
   446  	}
   447  
   448  	// Put any placement subnet first in the list
   449  	// so it ia allocated to the primary NIC.
   450  	if placementSubnetID != "" {
   451  		subnetIDs = append(subnetIDs, placementSubnetID)
   452  	}
   453  	for _, id := range subnetIDForZone {
   454  		if id != placementSubnetID {
   455  			subnetIDs = append(subnetIDs, id)
   456  		}
   457  	}
   458  	return vnetID, subnetIDs, nil
   459  }
   460  
   461  func (env *azureEnviron) subnetsForZone(subnetsToZones []map[network.Id][]string, az string) ([][]network.Id, error) {
   462  	subnetIDsForZone := make([][]network.Id, len(subnetsToZones))
   463  	for i, nic := range subnetsToZones {
   464  		var subnetIDs []network.Id
   465  		if az != "" {
   466  			var err error
   467  			if subnetIDs, err = network.FindSubnetIDsForAvailabilityZone(az, nic); err != nil {
   468  				return nil, errors.Annotatef(err, "getting subnets in zone %q", az)
   469  			}
   470  			if len(subnetIDs) == 0 {
   471  				return nil, errors.Errorf("availability zone %q has no subnets satisfying space constraints", az)
   472  			}
   473  		} else {
   474  			for subnetID := range nic {
   475  				subnetIDs = append(subnetIDs, subnetID)
   476  			}
   477  		}
   478  
   479  		// Filter out any fan networks.
   480  		subnetIDsForZone[i] = network.FilterInFanNetwork(subnetIDs)
   481  	}
   482  	return subnetIDsForZone, nil
   483  }
   484  
   485  func (env *azureEnviron) parsePlacement(placement string) (string, error) {
   486  	pos := strings.IndexRune(placement, '=')
   487  	if pos == -1 {
   488  		return "", fmt.Errorf("unknown placement directive: %v", placement)
   489  	}
   490  	switch key, value := placement[:pos], placement[pos+1:]; key {
   491  	case "subnet":
   492  		return value, nil
   493  	}
   494  	return "", fmt.Errorf("unknown placement directive: %v", placement)
   495  }
   496  
   497  func (env *azureEnviron) findPlacementSubnet(ctx context.ProviderCallContext, placement string) (network.Id, error) {
   498  	if placement == "" {
   499  		return "", nil
   500  	}
   501  	subnetName, err := env.parsePlacement(placement)
   502  	if err != nil {
   503  		return "", errors.Trace(err)
   504  	}
   505  
   506  	logger.Debugf("searching for subnet matching placement directive %q", subnetName)
   507  	return env.findSubnetID(ctx, subnetName)
   508  }