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 }