Skip to content

Commit c4d5960

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. (cherry picked from commit 6e59991) Minor changes due to lack of ScopeFactory in 0.7, mostly in unit test due to lack of MockScopeFactory. Conflicts: go.mod
1 parent 3611142 commit c4d5960

File tree

4 files changed

+587
-51
lines changed

4 files changed

+587
-51
lines changed

docs/book/src/clusteropenstack/configuration.md

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,9 +310,6 @@ spec:
310310
ports:
311311
- network:
312312
id: <your-network-id>
313-
nameSuffix: <your-port-name>
314-
description: <your-custom-port-description>
315-
vnicType: normal
316313
fixedIPs:
317314
- subnet:
318315
id: <your-subnet-id>
@@ -322,6 +319,9 @@ spec:
322319
tags:
323320
- tag1
324321
- tag2
322+
nameSuffix: <your-port-name>
323+
description: <your-custom-port-description>
324+
vnicType: normal
325325
securityGroups:
326326
- <your-security-group-id>
327327
profile:
@@ -331,7 +331,70 @@ spec:
331331

332332
Any such ports are created in addition to ports used for connections to networks or subnets.
333333

334-
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.
334+
### Port network and IP addresses
335+
336+
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.
337+
338+
`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.
339+
340+
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).
341+
342+
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.
343+
344+
`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.
345+
346+
`subnet` is a filter which uniquely describe the Neutron subnet an address will be allocated from. Its operation is analogous to `network`, described above.
347+
348+
`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).
349+
350+
If no `fixedIPs` are specified, the port will get an address from every subnet in the network.
351+
352+
#### Examples
353+
354+
A single explicit network with a single explicit subnet.
355+
```yaml
356+
ports:
357+
- tags:
358+
- control-plane
359+
network:
360+
id: 0686143b-f0a7-481a-86f5-cc1f8ccde692
361+
fixedIPs:
362+
- subnet:
363+
id: a5e50a9c-58f9-4b6f-b8ee-2e7b4e4414ee
364+
```
365+
366+
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.
367+
```yaml
368+
ports:
369+
- tags:
370+
- control-plane
371+
```
372+
373+
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`.
374+
```yaml
375+
ports:
376+
- tags:
377+
- storage
378+
network:
379+
name: storage-network
380+
fixedIPs:
381+
- subnet:
382+
name: storage-subnet
383+
```
384+
385+
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.
386+
```yaml
387+
ports:
388+
- tags:
389+
- control-plane
390+
fixedIPs:
391+
- subnet:
392+
id: a5e50a9c-58f9-4b6f-b8ee-2e7b4e4414ee
393+
```
394+
395+
### Port Security
396+
397+
`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.
335398

336399
```yaml
337400
apiVersion: infrastructure.cluster.x-k8s.io/v1alpha6
@@ -341,7 +404,8 @@ metadata:
341404
namespace: <cluster-name>
342405
spec:
343406
ports:
344-
- networkId: <your-network-id>
407+
- network:
408+
id: <your-network-id>
345409
...
346410
disablePortSecurity: true
347411
...

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.3
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.1.0
1112
github.com/gophercloud/utils v0.0.0-20221128194715-5caf33c866da
@@ -66,7 +67,6 @@ require (
6667
github.com/golang/protobuf v1.5.2 // indirect
6768
github.com/google/cel-go v0.12.4 // indirect
6869
github.com/google/gnostic v0.6.9 // indirect
69-
github.com/google/go-cmp v0.5.9 // indirect
7070
github.com/google/go-github/v45 v45.2.0 // indirect
7171
github.com/google/go-querystring v1.1.0 // indirect
7272
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // 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"
@@ -34,6 +35,7 @@ import (
3435

3536
infrav1 "sigs.k8s.io/cluster-api-provider-openstack/api/v1alpha6"
3637
"sigs.k8s.io/cluster-api-provider-openstack/pkg/clients"
38+
"sigs.k8s.io/cluster-api-provider-openstack/pkg/cloud/services/networking"
3739
"sigs.k8s.io/cluster-api-provider-openstack/pkg/record"
3840
capoerrors "sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/errors"
3941
"sigs.k8s.io/cluster-api-provider-openstack/pkg/utils/hash"
@@ -45,58 +47,149 @@ const (
4547
timeoutInstanceDelete = 5 * time.Minute
4648
)
4749

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

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

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

102195
// no networks or ports found in the spec, so create a port on the cluster network
@@ -110,10 +203,19 @@ func (s *Service) constructNetworks(openStackCluster *infrav1.OpenStackCluster,
110203
Trunk: &instanceSpec.Trunk,
111204
},
112205
}}
113-
trunkRequired = instanceSpec.Trunk
114206
}
115207

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

0 commit comments

Comments
 (0)