Skip to content

Commit 91abc99

Browse files
committed
fix(pr1014): disable CAPO managed security groups for bm
Candidate upstream home: #1014. PR #1014 owns the server_type=bm path. Bare-metal tenant networks can have port security disabled so Ironic can use pre-created ports, but CAPO managed security groups cause generated OpenStackServer ports to include a security group. Neutron rejects that combination before Nova/Ironic can create the server. Add a ClusterClass variable that removes managedSecurityGroups only for server_type=bm clusters, keep VM clusters on the existing managed security group behavior, and cover the remove-patch path in the Rust test helper. Validation: - cargo test managed_security_groups - cargo test test_convert_values_to_cluster_topology_variables - uv run pytest magnum_cluster_api/tests/unit/test_sync.py magnum_cluster_api/tests/unit/test_utils.py magnum_cluster_api/tests/unit/test_resources.py magnum_cluster_api/tests/unit/test_driver.py -q Assisted-By: ChatGPT <noreply@openai.com> (cherry picked from commit 93e3406ff794f6e1bdd5cbee42f0ec61112691d5) (cherry picked from commit 86b807549c8a845c5e037e89ada46e1bd99e325c) Signed-off-by: Rico Lin <rlin@vexxhost.com>
1 parent 31c0500 commit 91abc99

6 files changed

Lines changed: 258 additions & 4 deletions

File tree

magnum_cluster_api/resources.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,13 @@ def get_object(self) -> dict:
11971197
utils.get_cluster_floating_ip_disabled(self.cluster),
11981198
),
11991199
},
1200+
{
1201+
"name": "disableManagedSecurityGroups",
1202+
"value": getattr(
1203+
self.cluster.cluster_template, "server_type", "vm"
1204+
)
1205+
== "bm",
1206+
},
12001207
{
12011208
"name": "dnsNameservers",
12021209
"value": self.cluster.cluster_template.dns_nameserver.split(

magnum_cluster_api/tests/unit/test_resources.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# License for the specific language governing permissions and limitations
1313
# under the License.
1414

15+
from types import SimpleNamespace
16+
1517
import pytest
1618
from magnum.objects import fields
1719
from magnum.tests.unit.objects import utils
@@ -82,6 +84,135 @@ def test_generate_machine_deployments_for_cluster_with_deleting_node_group(
8284
assert len(mds) == 2
8385

8486

87+
def test_cluster_variables_disable_managed_security_groups_for_bm(context, mocker):
88+
cluster = mocker.Mock()
89+
cluster.cluster_template = mocker.Mock(
90+
network_driver="calico",
91+
dns_nameserver="1.1.1.1",
92+
external_network_id="public",
93+
server_type="vm",
94+
)
95+
cluster.default_ng_master = mocker.Mock(image_id="image")
96+
cluster.docker_volume_size = None
97+
cluster.fixed_network = None
98+
cluster.fixed_subnet = None
99+
cluster.flavor_id = "worker"
100+
cluster.keypair = None
101+
cluster.labels = {}
102+
cluster.master_count = 1
103+
cluster.master_flavor_id = "control-plane"
104+
cluster.master_lb_enabled = True
105+
cluster.nodegroups = []
106+
cluster.stack_id = "kube-test"
107+
108+
default_volume_type = SimpleNamespace(name="fast")
109+
openstack_client = mocker.Mock()
110+
openstack_client.cinder.return_value.volume_types.default.return_value = (
111+
default_volume_type
112+
)
113+
mocker.patch(
114+
"magnum_cluster_api.resources.clients.get_openstack_api",
115+
return_value=openstack_client,
116+
)
117+
mocker.patch(
118+
"magnum_cluster_api.resources.cinder.get_default_boot_volume_type",
119+
return_value=default_volume_type.name,
120+
)
121+
mocker.patch(
122+
"magnum_cluster_api.resources.generate_machine_deployments_for_cluster",
123+
return_value=[],
124+
)
125+
mocker.patch(
126+
"magnum_cluster_api.resources.neutron.get_external_network_id",
127+
return_value="external-network",
128+
)
129+
mocker.patch(
130+
"magnum_cluster_api.resources.neutron.get_fixed_subnet_id",
131+
return_value=None,
132+
)
133+
mocker.patch(
134+
"magnum_cluster_api.resources.utils.ensure_controlplane_server_group",
135+
return_value="server-group",
136+
)
137+
mocker.patch(
138+
"magnum_cluster_api.resources.utils.generate_api_cert_san_list",
139+
return_value="",
140+
)
141+
mocker.patch(
142+
"magnum_cluster_api.resources.utils.generate_apt_proxy_config",
143+
return_value="",
144+
)
145+
mocker.patch(
146+
"magnum_cluster_api.resources.utils.generate_containerd_config",
147+
return_value="",
148+
)
149+
mocker.patch(
150+
"magnum_cluster_api.resources.utils.generate_systemd_proxy_config",
151+
return_value="",
152+
)
153+
mocker.patch(
154+
"magnum_cluster_api.resources.utils.get_cluster_container_infra_prefix",
155+
return_value="",
156+
)
157+
mocker.patch(
158+
"magnum_cluster_api.resources.utils.get_fixed_network_id",
159+
return_value=None,
160+
)
161+
mocker.patch(
162+
"magnum_cluster_api.resources.utils.get_kube_tag",
163+
return_value="v1.34.3",
164+
)
165+
mocker.patch(
166+
"magnum_cluster_api.resources.utils.get_operating_system",
167+
return_value="ubuntu",
168+
)
169+
mocker.patch(
170+
"magnum_cluster_api.resources.utils.is_controlplane_different_failure_domain",
171+
return_value=False,
172+
)
173+
mocker.patch(
174+
"magnum_cluster_api.resources.utils.lookup_flavor",
175+
side_effect=lambda _osc, flavor_id: SimpleNamespace(name=flavor_id),
176+
)
177+
mocker.patch(
178+
"magnum_cluster_api.resources.utils.lookup_image",
179+
return_value={"id": "image"},
180+
)
181+
182+
rust_driver = mocker.Mock()
183+
rust_driver.resolve_immutable_fields.return_value = {
184+
"apiServerLoadBalancer": {"enabled": True}
185+
}
186+
187+
resource = resources.Cluster(
188+
context,
189+
mocker.Mock(),
190+
mocker.Mock(),
191+
cluster,
192+
rust_driver,
193+
).get_object()
194+
195+
variables = {
196+
variable["name"]: variable["value"]
197+
for variable in resource["spec"]["topology"]["variables"]
198+
}
199+
assert variables["disableManagedSecurityGroups"] is False
200+
201+
cluster.cluster_template.server_type = "bm"
202+
resource = resources.Cluster(
203+
context,
204+
mocker.Mock(),
205+
mocker.Mock(),
206+
cluster,
207+
rust_driver,
208+
).get_object()
209+
variables = {
210+
variable["name"]: variable["value"]
211+
for variable in resource["spec"]["topology"]["variables"]
212+
}
213+
assert variables["disableManagedSecurityGroups"] is True
214+
215+
85216
@pytest.mark.parametrize(
86217
"auto_scaling_enabled",
87218
[True, False, None],
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use crate::{
2+
cluster_api::{
3+
clusterclasses::{
4+
ClusterClassPatches, ClusterClassPatchesDefinitions,
5+
ClusterClassPatchesDefinitionsJsonPatches, ClusterClassPatchesDefinitionsSelector,
6+
ClusterClassPatchesDefinitionsSelectorMatchResources, ClusterClassVariables,
7+
ClusterClassVariablesSchema,
8+
},
9+
openstackclustertemplates::OpenStackClusterTemplate,
10+
},
11+
features::{
12+
ClusterClassVariablesSchemaExt, ClusterFeatureEntry, ClusterFeaturePatches,
13+
ClusterFeatureVariables,
14+
},
15+
};
16+
use cluster_feature_derive::ClusterFeatureValues;
17+
use kube::CustomResourceExt;
18+
use serde::{Deserialize, Serialize};
19+
20+
#[derive(Serialize, Deserialize, ClusterFeatureValues)]
21+
#[allow(dead_code)]
22+
pub struct FeatureValues {
23+
#[serde(rename = "disableManagedSecurityGroups")]
24+
pub disable_managed_security_groups: bool,
25+
}
26+
27+
pub struct Feature {}
28+
29+
impl ClusterFeaturePatches for Feature {
30+
fn patches(&self) -> Vec<ClusterClassPatches> {
31+
vec![ClusterClassPatches {
32+
name: "disableManagedSecurityGroups".into(),
33+
enabled_if: Some("{{ if .disableManagedSecurityGroups }}true{{end}}".into()),
34+
definitions: Some(vec![ClusterClassPatchesDefinitions {
35+
selector: ClusterClassPatchesDefinitionsSelector {
36+
api_version: OpenStackClusterTemplate::api_resource().api_version,
37+
kind: OpenStackClusterTemplate::api_resource().kind,
38+
match_resources: ClusterClassPatchesDefinitionsSelectorMatchResources {
39+
infrastructure_cluster: Some(true),
40+
..Default::default()
41+
},
42+
},
43+
json_patches: vec![ClusterClassPatchesDefinitionsJsonPatches {
44+
op: "remove".into(),
45+
path: "/spec/template/spec/managedSecurityGroups".into(),
46+
..Default::default()
47+
}],
48+
}]),
49+
..Default::default()
50+
}]
51+
}
52+
}
53+
54+
inventory::submit! {
55+
ClusterFeatureEntry{ feature: &Feature {} }
56+
}
57+
58+
#[cfg(test)]
59+
mod tests {
60+
use super::*;
61+
use crate::{features::test::TestClusterResources, resources::fixtures::default_values};
62+
use pretty_assertions::assert_eq;
63+
64+
#[test]
65+
fn test_keeps_managed_security_groups_by_default() {
66+
let feature = Feature {};
67+
68+
let mut values = default_values();
69+
values.disable_managed_security_groups = false;
70+
71+
let patches = feature.patches();
72+
let mut resources = TestClusterResources::new();
73+
resources.apply_patches(&patches, &values);
74+
75+
assert!(resources
76+
.openstack_cluster_template
77+
.spec
78+
.template
79+
.spec
80+
.managed_security_groups
81+
.is_some());
82+
}
83+
84+
#[test]
85+
fn test_removes_managed_security_groups_for_baremetal() {
86+
let feature = Feature {};
87+
88+
let mut values = default_values();
89+
values.disable_managed_security_groups = true;
90+
91+
let patches = feature.patches();
92+
let mut resources = TestClusterResources::new();
93+
resources.apply_patches(&patches, &values);
94+
95+
assert_eq!(
96+
resources
97+
.openstack_cluster_template
98+
.spec
99+
.template
100+
.spec
101+
.managed_security_groups,
102+
None
103+
);
104+
}
105+
}

src/features/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ pub mod flavors;
6363
pub mod image_repository;
6464
pub mod images;
6565
pub mod keystone_auth;
66+
pub mod managed_security_groups;
6667
pub mod networks;
6768
pub mod openid_connect;
6869
pub mod operating_system;

src/features/test.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ impl ToRenderedPatchOperation for ClusterClassPatchesDefinitionsJsonPatches {
9090
version: &str,
9191
values: &T,
9292
) -> PatchOperation {
93+
if self.op == "remove" {
94+
return json_patch::PatchOperation::Remove(RemoveOperation {
95+
path: PointerBuf::parse(&self.path).unwrap(),
96+
});
97+
}
98+
9399
let value = match self.value_from {
94100
Some(value_from) => value_from.to_rendered_value(version, values),
95101
None => self.value.expect("value should be present").into(),
@@ -104,9 +110,6 @@ impl ToRenderedPatchOperation for ClusterClassPatchesDefinitionsJsonPatches {
104110
path: PointerBuf::parse(&self.path).unwrap(),
105111
value: value,
106112
}),
107-
"remove" => json_patch::PatchOperation::Remove(RemoveOperation {
108-
path: PointerBuf::parse(&self.path).unwrap(),
109-
}),
110113
_ => panic!("Unsupported patch operation: {}", self.op),
111114
}
112115
}

src/resources.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ pub mod fixtures {
236236
.control_plane_availability_zones(vec!["zone1".into(), "zone2".into()])
237237
.disable_api_server_floating_ip(true)
238238
.disable_api_server_floating_ip_managed(true)
239+
.disable_managed_security_groups(false)
239240
.external_network_id("external-network-id".into())
240241
.control_plane_flavor("control-plane".into())
241242
.flavor("worker".into())
@@ -310,7 +311,7 @@ mod tests {
310311
let values = default_values();
311312
let variables: Vec<ClusterTopologyVariables> = values.into();
312313

313-
assert_eq!(variables.len(), 39);
314+
assert_eq!(variables.len(), 40);
314315

315316
for var in &variables {
316317
match var.name.as_str() {
@@ -356,6 +357,12 @@ mod tests {
356357
json!(default_values().disable_api_server_floating_ip_managed)
357358
);
358359
}
360+
"disableManagedSecurityGroups" => {
361+
assert_eq!(
362+
var.value,
363+
json!(default_values().disable_managed_security_groups)
364+
);
365+
}
359366
"externalNetworkId" => {
360367
assert_eq!(var.value, json!(default_values().external_network_id));
361368
}

0 commit comments

Comments
 (0)