Skip to content

Commit 6e59991

Browse files
committed
Infer port network from subnet
NetworkID is a required field when creating a neutron port. We previously passed on this requirement in the Ports API. However we didn't have this restriction in the Networks API and inferred the network from a subnet if one was defined. This change eases the transition from Networks to Ports by removing this restriction for Ports.
1 parent 6a6b86a commit 6e59991

File tree

4 files changed

+579
-51
lines changed

4 files changed

+579
-51
lines changed

docs/book/src/clusteropenstack/configuration.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,6 @@ spec:
358358
ports:
359359
- network:
360360
id: <your-network-id>
361-
nameSuffix: <your-port-name>
362-
description: <your-custom-port-description>
363-
vnicType: normal
364361
fixedIPs:
365362
- subnet:
366363
id: <your-subnet-id>
@@ -370,6 +367,9 @@ spec:
370367
tags:
371368
- tag1
372369
- tag2
370+
nameSuffix: <your-port-name>
371+
description: <your-custom-port-description>
372+
vnicType: normal
373373
securityGroups:
374374
- <your-security-group-id>
375375
profile:
@@ -379,7 +379,70 @@ spec:
379379

380380
Any such ports are created in addition to ports used for connections to networks or subnets.
381381

382-
Also, `port security` can be applied to specific port to enable/disable the `port security` on that port; When not set, it takes the value of the corresponding field at the network level.
382+
### Port network and IP addresses
383+
384+
Together, `network` and `fixedIPs` define the network a port will be created on, and the addresses which will be assigned to the port on that network.
385+
386+
`network` is a filter which uniquely describes the Neutron network the port will be created be on. Machine creation will fail if the result is empty or not unique. If a network `id` is specified in the filter then no separate OpenStack query is required. This has the advantages of being both faster and unambiguous in all circumstances, so it is the preferred way to specify a network where possible.
387+
388+
The available fields are described in [the CRD](https://doc.crds.dev/github.com/kubernetes-sigs/cluster-api-provider-openstack/infrastructure.cluster.x-k8s.io/OpenStackMachine/[email protected]#spec-ports-network).
389+
390+
If `network` is not specified at all, it may be possible to infer the network from any uniquely defined subnets in `fixedIPs`. As this may result in additional OpenStack queries and the potential for ambiguity is greater, this is not recommended.
391+
392+
`fixedIPs` describes a list of addresses from the target `network` which will be allocated to the port. A `fixedIP` is either a specific `ipAddress`, a `subnet` from which an ip address will be allocated, or both. If only `ipAddress` is specified, it must be valid in at least one of the subnets defined in the current network. If both are defined, `ipAddress` must be valid in the specified subnet.
393+
394+
`subnet` is a filter which uniquely describe the Neutron subnet an address will be allocated from. Its operation is analogous to `network`, described above.
395+
396+
`fixedIPs`, including all fields available in the `subnet` filter, are described in [the CRD](https://doc.crds.dev/github.com/kubernetes-sigs/cluster-api-provider-openstack/infrastructure.cluster.x-k8s.io/OpenStackMachine/[email protected]#spec-ports-fixedIPs).
397+
398+
If no `fixedIPs` are specified, the port will get an address from every subnet in the network.
399+
400+
#### Examples
401+
402+
A single explicit network with a single explicit subnet.
403+
```yaml
404+
ports:
405+
- tags:
406+
- control-plane
407+
network:
408+
id: 0686143b-f0a7-481a-86f5-cc1f8ccde692
409+
fixedIPs:
410+
- subnet:
411+
id: a5e50a9c-58f9-4b6f-b8ee-2e7b4e4414ee
412+
```
413+
414+
No network or fixed IPs: the port will be created on the cluster default network, and will get a single address from the cluster default subnet.
415+
```yaml
416+
ports:
417+
- tags:
418+
- control-plane
419+
```
420+
421+
Network and subnet are specified by filter. They will be looked up. Note that this is not as efficient or reliable as specifying the network by `id`.
422+
```yaml
423+
ports:
424+
- tags:
425+
- storage
426+
network:
427+
name: storage-network
428+
fixedIPs:
429+
- subnet:
430+
name: storage-subnet
431+
```
432+
433+
No network, but a fixed IP with a subnet. The network will be inferred from the network of the subnet. Note that this is not as efficient or reliable as specifying the network explicitly.
434+
```yaml
435+
ports:
436+
- tags:
437+
- control-plane
438+
fixedIPs:
439+
- subnet:
440+
id: a5e50a9c-58f9-4b6f-b8ee-2e7b4e4414ee
441+
```
442+
443+
### Port Security
444+
445+
`port security` can be applied to specific port to enable/disable the `port security` on that port; When not set, it takes the value of the corresponding field at the network level.
383446

384447
```yaml
385448
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha7
@@ -389,7 +452,8 @@ metadata:
389452
namespace: <cluster-name>
390453
spec:
391454
ports:
392-
- networkId: <your-network-id>
455+
- network:
456+
id: <your-network-id>
393457
...
394458
disablePortSecurity: true
395459
...

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/davecgh/go-spew v1.1.1
77
github.com/go-logr/logr v1.2.4
88
github.com/golang/mock v1.6.0
9+
github.com/google/go-cmp v0.5.9
910
github.com/google/gofuzz v1.2.0
1011
github.com/gophercloud/gophercloud v1.3.0
1112
github.com/gophercloud/utils v0.0.0-20221207145018-e8fba78967ca
@@ -68,7 +69,6 @@ require (
6869
github.com/golang/protobuf v1.5.3 // indirect
6970
github.com/google/cel-go v0.14.0 // indirect
7071
github.com/google/gnostic v0.6.9 // indirect
71-
github.com/google/go-cmp v0.5.9 // indirect
7272
github.com/google/go-github/v48 v48.2.0 // indirect
7373
github.com/google/go-querystring v1.1.0 // indirect
7474
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect

pkg/cloud/services/compute/instance.go

Lines changed: 147 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package compute
1818

1919
import (
20+
"errors"
2021
"fmt"
2122
"os"
2223
"strconv"
@@ -35,6 +36,7 @@ import (
3536

3637
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha7"
3738
"sigs.k8s.io/cluster-api-provider-openstack/pkg/clients"
39+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking"
3840
"sigs.k8s.io/cluster-api-provider-openstack/pkg/record"
3941
capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
4042
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/hash"
@@ -46,58 +48,149 @@ const (
4648
timeoutInstanceDelete = 5 * time.Minute
4749
)
4850

49-
// constructNetworks builds an array of networks from the network, subnet and ports items in the instance spec.
50-
// If no networks or ports are in the spec, returns a single network item for a network connection to the default cluster network.
51-
func (s *Service) constructNetworks(openStackCluster *infrav1.OpenStackCluster, instanceSpec *InstanceSpec) ([]infrav1.Network, error) {
52-
trunkRequired := false
51+
// normalizePortTarget ensures that the port has a network ID.
52+
func (s *Service) normalizePortTarget(port *infrav1.PortOpts, openStackCluster *infrav1.OpenStackCluster, portIdx int) error {
53+
// Treat no Network and empty Network the same
54+
noNetwork := port.Network == nil || (*port.Network == infrav1.NetworkFilter{})
5355

54-
nets, err := s.getServerNetworks(instanceSpec.Networks)
55-
if err != nil {
56-
return nil, err
57-
}
58-
59-
for i := range instanceSpec.Ports {
60-
port := &instanceSpec.Ports[i]
61-
// No Trunk field specified for the port, inherit openStackMachine.Spec.Trunk.
62-
if port.Trunk == nil {
63-
port.Trunk = &instanceSpec.Trunk
56+
// No network or subnets defined: use cluster defaults
57+
if noNetwork && len(port.FixedIPs) == 0 {
58+
port.Network = &infrav1.NetworkFilter{
59+
ID: openStackCluster.Status.Network.ID,
6460
}
65-
if *port.Trunk {
66-
trunkRequired = true
61+
port.FixedIPs = []infrav1.FixedIP{
62+
{
63+
Subnet: &infrav1.SubnetFilter{
64+
ID: openStackCluster.Status.Network.Subnet.ID,
65+
},
66+
},
6767
}
68-
if port.Network != nil {
69-
netID := port.Network.ID
70-
if netID == "" {
71-
networkingService, err := s.getNetworkingService()
72-
if err != nil {
73-
return nil, err
68+
69+
return nil
70+
}
71+
72+
// No network, but fixed IPs are defined(we handled the no fixed
73+
// IPs case above): try to infer network from a subnet
74+
if noNetwork {
75+
s.scope.Logger().V(4).Info("No network defined for port %d, attempting to infer from subnet", portIdx)
76+
77+
// Look for a unique subnet defined in FixedIPs. If we find one
78+
// we can use it to infer the network ID. We don't need to worry
79+
// here about the case where different FixedIPs have different
80+
// networks because that will cause an error later when we try
81+
// to create the port.
82+
networkID, err := func() (string, error) {
83+
networkingService, err := s.getNetworkingService()
84+
if err != nil {
85+
return "", err
86+
}
87+
88+
for i, fixedIP := range port.FixedIPs {
89+
if fixedIP.Subnet == nil {
90+
continue
7491
}
7592

76-
netIDs, err := networkingService.GetNetworkIDsByFilter(port.Network.ToListOpt())
93+
subnet, err := networkingService.GetSubnetByFilter(fixedIP.Subnet)
7794
if err != nil {
78-
return nil, err
79-
}
80-
if len(netIDs) > 1 {
81-
return nil, fmt.Errorf("network filter for port %s returns more than one result", port.NameSuffix)
82-
} else if len(netIDs) == 0 {
83-
return nil, fmt.Errorf("network filter for port %s returns no networks", port.NameSuffix)
95+
// Multiple matches might be ok later when we restrict matches to a single network
96+
if errors.Is(err, networking.ErrMultipleMatches) {
97+
s.scope.Logger().V(4).Info("Can't infer network from subnet %d: %s", i, err)
98+
continue
99+
}
100+
101+
return "", err
84102
}
85-
netID = netIDs[0]
103+
104+
// Cache the subnet ID in the FixedIP
105+
fixedIP.Subnet.ID = subnet.ID
106+
return subnet.NetworkID, nil
86107
}
87-
nets = append(nets, infrav1.Network{
88-
ID: netID,
89-
Subnet: &infrav1.Subnet{},
90-
PortOpts: port,
91-
})
92-
} else {
93-
nets = append(nets, infrav1.Network{
94-
ID: openStackCluster.Status.Network.ID,
95-
Subnet: &infrav1.Subnet{
96-
ID: openStackCluster.Status.Network.Subnet.ID,
97-
},
98-
PortOpts: port,
99-
})
108+
109+
// TODO: This is a spec error: it should set the machine to failed
110+
return "", fmt.Errorf("port %d has no network and unable to infer from fixed IPs", portIdx)
111+
}()
112+
if err != nil {
113+
return err
100114
}
115+
116+
port.Network = &infrav1.NetworkFilter{
117+
ID: networkID,
118+
}
119+
120+
return nil
121+
}
122+
123+
// Nothing to do if network ID is already set
124+
if port.Network.ID != "" {
125+
return nil
126+
}
127+
128+
// Network is defined by Filter
129+
networkingService, err := s.getNetworkingService()
130+
if err != nil {
131+
return err
132+
}
133+
134+
netIDs, err := networkingService.GetNetworkIDsByFilter(port.Network.ToListOpt())
135+
if err != nil {
136+
return err
137+
}
138+
139+
// TODO: These are spec errors: they should set the machine to failed
140+
if len(netIDs) > 1 {
141+
return fmt.Errorf("network filter for port %d returns more than one result", portIdx)
142+
} else if len(netIDs) == 0 {
143+
return fmt.Errorf("network filter for port %d returns no networks", portIdx)
144+
}
145+
146+
port.Network.ID = netIDs[0]
147+
148+
return nil
149+
}
150+
151+
// normalizePorts ensures that a user-specified PortOpts has all required fields set. Specifically it:
152+
// - sets the Trunk field to the instance spec default if not specified
153+
// - sets the Network ID field if not specified.
154+
func (s *Service) normalizePorts(ports []infrav1.PortOpts, openStackCluster *infrav1.OpenStackCluster, instanceSpec *InstanceSpec) ([]infrav1.PortOpts, error) {
155+
normalizedPorts := make([]infrav1.PortOpts, 0, len(ports))
156+
for i := range ports {
157+
// Deep copy the port to avoid mutating the original
158+
port := ports[i].DeepCopy()
159+
160+
// No Trunk field specified for the port, inherit the machine default
161+
if port.Trunk == nil {
162+
port.Trunk = &instanceSpec.Trunk
163+
}
164+
165+
if err := s.normalizePortTarget(port, openStackCluster, i); err != nil {
166+
return nil, err
167+
}
168+
169+
normalizedPorts = append(normalizedPorts, *port)
170+
}
171+
return normalizedPorts, nil
172+
}
173+
174+
// constructNetworks builds an array of networks from the network, subnet and ports items in the instance spec.
175+
// If no networks or ports are in the spec, returns a single network item for a network connection to the default cluster network.
176+
func (s *Service) constructNetworks(openStackCluster *infrav1.OpenStackCluster, instanceSpec *InstanceSpec) ([]infrav1.Network, error) {
177+
nets, err := s.getServerNetworks(instanceSpec.Networks)
178+
if err != nil {
179+
return nil, err
180+
}
181+
182+
// Ensure user-specified ports have all required fields
183+
ports, err := s.normalizePorts(instanceSpec.Ports, openStackCluster, instanceSpec)
184+
if err != nil {
185+
return nil, err
186+
}
187+
for i := range ports {
188+
port := &ports[i]
189+
nets = append(nets, infrav1.Network{
190+
ID: port.Network.ID,
191+
Subnet: &infrav1.Subnet{},
192+
PortOpts: port,
193+
})
101194
}
102195

103196
// no networks or ports found in the spec, so create a port on the cluster network
@@ -111,10 +204,19 @@ func (s *Service) constructNetworks(openStackCluster *infrav1.OpenStackCluster,
111204
Trunk: &instanceSpec.Trunk,
112205
},
113206
}}
114-
trunkRequired = instanceSpec.Trunk
115207
}
116208

117-
if trunkRequired {
209+
// trunk support is required if any port has trunk enabled
210+
portUsesTrunk := func() bool {
211+
for _, net := range nets {
212+
port := net.PortOpts
213+
if port != nil && port.Trunk != nil && *port.Trunk {
214+
return true
215+
}
216+
}
217+
return false
218+
}
219+
if portUsesTrunk() {
118220
trunkSupported, err := s.isTrunkExtSupported()
119221
if err != nil {
120222
return nil, err

0 commit comments

Comments
 (0)