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 }