Skip to content

Commit 5029e38

Browse files
Merge pull request #345 from rabi/bond_ctlplane
Add support for bonding multiple network interfaces
2 parents b4c8cca + 7f25c10 commit 5029e38

File tree

8 files changed

+291
-2
lines changed

8 files changed

+291
-2
lines changed

api/bases/baremetal.openstack.org_openstackbaremetalsets.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ spec:
146146
default: cloud-admin
147147
description: CloudUser to be configured for remote access
148148
type: string
149+
ctlplaneBond:
150+
description: CtlplaneBond - Bonding configuration for ctlplane network
151+
properties:
152+
bondInterfaces:
153+
description: BondInterfaces - List of physical interfaces to bond
154+
items:
155+
type: string
156+
minItems: 2
157+
type: array
158+
bondMode:
159+
default: active-backup
160+
description: BondMode - Bonding mode (e.g., active-backup, 802.3ad)
161+
type: string
162+
bondOptions:
163+
additionalProperties:
164+
type: string
165+
description: BondOptions - Additional bonding options as key-value
166+
pairs
167+
type: object
168+
required:
169+
- bondInterfaces
170+
type: object
149171
ctlplaneGateway:
150172
description: 'CtlplaneGateway - IP of gateway for ctrlplane network
151173
(TODO: acquire this is another manner?)'

api/v1beta1/openstackbaremetalset_types.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ import (
2626
// +kubebuilder:validation:Enum=metadata;disabled
2727
type AutomatedCleaningMode string
2828

29+
// BondConfig defines the bonding configuration for network interfaces
30+
type BondConfig struct {
31+
// BondInterfaces - List of physical interfaces to bond
32+
// +kubebuilder:validation:MinItems=2
33+
BondInterfaces []string `json:"bondInterfaces"`
34+
// +kubebuilder:default=active-backup
35+
// BondMode - Bonding mode (e.g., active-backup, 802.3ad)
36+
BondMode string `json:"bondMode,omitempty"`
37+
// +kubebuilder:validation:Optional
38+
// BondOptions - Additional bonding options as key-value pairs
39+
BondOptions map[string]string `json:"bondOptions,omitempty"`
40+
}
41+
2942
// InstanceSpec Instance specific attributes
3043
type InstanceSpec struct {
3144
// +kubebuilder:validation:Optional
@@ -90,6 +103,9 @@ type OpenStackBaremetalSetTemplateSpec struct {
90103
DeploymentSSHSecret string `json:"deploymentSSHSecret"`
91104
// CtlplaneInterface - Interface on the provisioned nodes to use for ctlplane network
92105
CtlplaneInterface string `json:"ctlplaneInterface"`
106+
// +kubebuilder:validation:Optional
107+
// CtlplaneBond - Bonding configuration for ctlplane network
108+
CtlplaneBond *BondConfig `json:"ctlplaneBond,omitempty"`
93109
// +kubebuilder:default=openshift-machine-api
94110
// +kubebuilder:validation:Optional
95111
// BmhNamespace Namespace to look for BaremetalHosts(default: openshift-machine-api)

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/baremetal.openstack.org_openstackbaremetalsets.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,28 @@ spec:
146146
default: cloud-admin
147147
description: CloudUser to be configured for remote access
148148
type: string
149+
ctlplaneBond:
150+
description: CtlplaneBond - Bonding configuration for ctlplane network
151+
properties:
152+
bondInterfaces:
153+
description: BondInterfaces - List of physical interfaces to bond
154+
items:
155+
type: string
156+
minItems: 2
157+
type: array
158+
bondMode:
159+
default: active-backup
160+
description: BondMode - Bonding mode (e.g., active-backup, 802.3ad)
161+
type: string
162+
bondOptions:
163+
additionalProperties:
164+
type: string
165+
description: BondOptions - Additional bonding options as key-value
166+
pairs
167+
type: object
168+
required:
169+
- bondInterfaces
170+
type: object
149171
ctlplaneGateway:
150172
description: 'CtlplaneGateway - IP of gateway for ctrlplane network
151173
(TODO: acquire this is another manner?)'

pkg/openstackbaremetalset/baremetalhost.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,16 @@ func BaremetalHostProvision(
126126
} else {
127127
templateParameters["CtlplaneGateway"] = instance.Spec.CtlplaneGateway
128128
}
129+
// Handle bonding configuration from template spec
130+
if instance.Spec.CtlplaneBond != nil {
131+
templateParameters["CtlplaneBondInterfaces"] = instance.Spec.CtlplaneBond.BondInterfaces
132+
if instance.Spec.CtlplaneBond.BondMode != "" {
133+
templateParameters["CtlplaneBondMode"] = instance.Spec.CtlplaneBond.BondMode
134+
}
135+
if len(instance.Spec.CtlplaneBond.BondOptions) > 0 {
136+
templateParameters["CtlplaneBondOptions"] = instance.Spec.CtlplaneBond.BondOptions
137+
}
138+
}
129139
templateParameters["CtlplaneNetmask"] = net.IP(ipNet.Mask)
130140
if len(instance.Spec.BootstrapDNS) > 0 {
131141
templateParameters["CtlplaneDns"] = instance.Spec.BootstrapDNS

templates/openstackbaremetalset/cloudinit/networkdata

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,32 @@
11
links:
2+
{{- if (index . "CtlplaneBondInterfaces") }}
3+
{{- range $iface := .CtlplaneBondInterfaces }}
4+
- name: {{ $iface }}
5+
id: {{ $iface }}
6+
type: vif
7+
{{- end }}
8+
- name: {{ .CtlplaneInterface }}
9+
id: {{ .CtlplaneInterface }}
10+
type: bond
11+
bond_interfaces:
12+
{{- range $iface := .CtlplaneBondInterfaces }}
13+
- {{ $iface }}
14+
{{- end }}
15+
bond_mode: {{ if .CtlplaneBondMode }}{{ .CtlplaneBondMode }}{{ else }}active-backup{{ end }}
16+
{{- if (index . "CtlplaneBondOptions") }}
17+
params:
18+
{{- range $key, $value := .CtlplaneBondOptions }}
19+
{{ $key }}: {{ $value }}
20+
{{- end }}
21+
{{- else }}
22+
params:
23+
bond-miimon: 100
24+
{{- end }}
25+
{{- else }}
226
- name: {{ .CtlplaneInterface }}
327
id: {{ .CtlplaneInterface }}
428
type: vif
29+
{{- end }}
530
{{- if (index . "CtlplaneVlan") }}
631
- name: {{ .CtlplaneInterface }}.{{ .CtlplaneVlan }}
732
id: {{ .CtlplaneInterface }}.{{ .CtlplaneVlan }}

tests/functional/openstackbaremetalset_controller_test.go

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import (
2222
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
2323
. "github.com/onsi/gomega" //revive:disable:dot-imports
2424
baremetalv1 "github.com/openstack-k8s-operators/openstack-baremetal-operator/api/v1beta1"
25+
corev1 "k8s.io/api/core/v1"
2526
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
2627

2728
//revive:disable-next-line:dot-imports
2829
"github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2930
. "github.com/openstack-k8s-operators/lib-common/modules/common/test/helpers"
30-
corev1 "k8s.io/api/core/v1"
31+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
3132
"k8s.io/apimachinery/pkg/types"
33+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
3234
)
3335

3436
var _ = Describe("BaremetalSet Test", func() {
@@ -267,8 +269,8 @@ var _ = Describe("BaremetalSet Test", func() {
267269
When("BMH provisioned with generated userdata and networkdata", func() {
268270
BeforeEach(func() {
269271
DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName))
270-
bmh := GetBaremetalHost(bmhName)
271272
Eventually(func(g Gomega) {
273+
bmh := GetBaremetalHost(bmhName)
272274
bmh.Status.Provisioning.State = metal3v1.StateAvailable
273275
g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed())
274276
}, th.Timeout, th.Interval).Should(Succeed())
@@ -327,6 +329,11 @@ var _ = Describe("BaremetalSet Test", func() {
327329
})
328330
Expect(networkDataSecret.Data).To(HaveKey("networkData"))
329331
networkData := string(networkDataSecret.Data["networkData"])
332+
// Verify proper YAML formatting - links: should be on its own line
333+
Expect(networkData).To(MatchRegexp("(?m)^links:\n"))
334+
Expect(networkData).To(MatchRegexp("(?m)^networks:\n"))
335+
Expect(networkData).NotTo(ContainSubstring("links:- "))
336+
Expect(networkData).NotTo(ContainSubstring("networks:- "))
330337
Expect(networkData).To(ContainSubstring("links:"))
331338
Expect(networkData).To(ContainSubstring("name: eth0"))
332339
Expect(networkData).To(ContainSubstring("ip_address: 10.0.0.1"))
@@ -454,6 +461,9 @@ var _ = Describe("BaremetalSet Test", func() {
454461
Namespace: bmhName.Namespace,
455462
})
456463
networkData := string(networkDataSecret.Data["networkData"])
464+
// Verify proper YAML formatting for routes
465+
Expect(networkData).To(MatchRegexp("(?m)^ routes:\n"))
466+
Expect(networkData).NotTo(ContainSubstring("routes: - network:"))
457467
Expect(networkData).To(ContainSubstring("gateway: 10.0.0.254"))
458468
Expect(networkData).To(ContainSubstring("routes:"))
459469
})
@@ -472,6 +482,11 @@ var _ = Describe("BaremetalSet Test", func() {
472482
Namespace: bmhName.Namespace,
473483
})
474484
networkData := string(networkDataSecret.Data["networkData"])
485+
// Verify proper YAML formatting for DNS sections
486+
Expect(networkData).To(MatchRegexp("(?m)^ dns_nameservers:\n"))
487+
Expect(networkData).NotTo(ContainSubstring("dns_nameservers: - "))
488+
Expect(networkData).To(MatchRegexp("(?m)^ dns_search:\n"))
489+
Expect(networkData).NotTo(ContainSubstring("dns_search: - "))
475490
Expect(networkData).To(ContainSubstring("dns_nameservers:"))
476491
Expect(networkData).To(ContainSubstring("8.8.8.8"))
477492
Expect(networkData).To(ContainSubstring("8.8.4.4"))
@@ -947,4 +962,150 @@ var _ = Describe("BaremetalSet Test", func() {
947962
)
948963
})
949964
})
965+
966+
When("A BaremetalSet with bonding configuration generates network data", func() {
967+
BeforeEach(func() {
968+
DeferCleanup(th.DeleteInstance, CreateBaremetalHost(bmhName))
969+
bmh := GetBaremetalHost(bmhName)
970+
Eventually(func(g Gomega) {
971+
bmh.Status.Provisioning.State = metal3v1.StateAvailable
972+
g.Expect(th.K8sClient.Status().Update(th.Ctx, bmh)).To(Succeed())
973+
}, th.Timeout, th.Interval).Should(Succeed())
974+
975+
DeferCleanup(th.DeleteInstance, CreateSSHSecret(deploymentSecretName))
976+
})
977+
978+
// Helper to patch provision server to be ready
979+
patchProvisionServerReady := func() {
980+
provServerName := types.NamespacedName{
981+
Name: strings.Join([]string{baremetalSetName.Name, "provisionserver"}, "-"),
982+
Namespace: namespace,
983+
}
984+
985+
// Wait for provision server to be created by the controller
986+
// and patch it to be ready
987+
Eventually(func(g Gomega) {
988+
provServer := &baremetalv1.OpenStackProvisionServer{}
989+
g.Expect(th.K8sClient.Get(th.Ctx, provServerName, provServer)).To(Succeed())
990+
provServer.Status.ProvisionIP = "192.168.122.100"
991+
provServer.Status.LocalImageURL = "http://192.168.122.100:6190/images/edpm-hardened-uefi.qcow2"
992+
provServer.Status.LocalImageChecksumURL = "http://192.168.122.100:6190/images/edpm-hardened-uefi.qcow2.sha256sum"
993+
provServer.Status.ReadyCount = 1
994+
995+
g.Expect(th.K8sClient.Status().Update(th.Ctx, provServer)).To(Succeed())
996+
}, th.Timeout, th.Interval).Should(Succeed())
997+
}
998+
999+
It("Should generate networkdata secret with bonding configuration", func() {
1000+
bondSpec := DefaultBaremetalSetSpec(bmhName, true) // Need provision server
1001+
bondSpec["ctlplaneInterface"] = "bond0"
1002+
bondSpec["ctlplaneBond"] = map[string]any{
1003+
"bondInterfaces": []string{"eno1", "eno2"},
1004+
"bondMode": "active-backup",
1005+
}
1006+
DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, bondSpec))
1007+
1008+
// Verify default was applied
1009+
baremetalSetInstance := GetBaremetalSet(baremetalSetName)
1010+
Expect(baremetalSetInstance.Spec.CtlplaneBond.BondMode).Should(Equal("active-backup"))
1011+
1012+
//patch provision server to be ready
1013+
patchProvisionServerReady()
1014+
1015+
// Wait for network data secret with bonding config
1016+
Eventually(func(g Gomega) {
1017+
secretName := types.NamespacedName{
1018+
Name: strings.Join([]string{baremetalSetName.Name, "cloudinit-networkdata", "compute-0"}, "-"),
1019+
Namespace: namespace,
1020+
}
1021+
secret := &corev1.Secret{}
1022+
g.Expect(th.K8sClient.Get(th.Ctx, secretName, secret)).To(Succeed())
1023+
1024+
networkData := string(secret.Data["networkData"])
1025+
// Verify proper YAML formatting
1026+
g.Expect(networkData).To(MatchRegexp("(?m)^links:\n"))
1027+
g.Expect(networkData).NotTo(ContainSubstring("links:- "))
1028+
g.Expect(networkData).To(MatchRegexp("(?m)^ bond_interfaces:\n"))
1029+
g.Expect(networkData).NotTo(ContainSubstring("bond_interfaces: - "))
1030+
g.Expect(networkData).To(MatchRegexp("bond_mode: [^\n]+\n "))
1031+
g.Expect(networkData).NotTo(MatchRegexp("bond_mode: [^\n]+ params:"))
1032+
g.Expect(networkData).Should(ContainSubstring("type: bond"))
1033+
g.Expect(networkData).Should(ContainSubstring("bond_mode: active-backup"))
1034+
g.Expect(networkData).Should(ContainSubstring("eno1"))
1035+
g.Expect(networkData).Should(ContainSubstring("eno2"))
1036+
}, th.Timeout, th.Interval).Should(Succeed())
1037+
})
1038+
1039+
It("Should reject bonding with less than 2 interfaces", func() {
1040+
bondSpec := DefaultBaremetalSetSpec(bmhName, true)
1041+
bondSpec["ctlplaneInterface"] = "bond0"
1042+
bondSpec["ctlplaneBond"] = map[string]any{
1043+
"bondInterfaces": []string{"eno1"}, // Only one interface - should fail
1044+
"bondMode": "active-backup",
1045+
}
1046+
1047+
object := DefaultBaremetalSetTemplate(baremetalSetName, bondSpec)
1048+
unstructuredObj := &unstructured.Unstructured{Object: object}
1049+
_, err := controllerutil.CreateOrPatch(
1050+
th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil })
1051+
Expect(err).Should(HaveOccurred())
1052+
Expect(err.Error()).Should(ContainSubstring("bondInterfaces"))
1053+
})
1054+
1055+
It("Should generate networkdata without bonding (backward compatibility)", func() {
1056+
noBondSpec := DefaultBaremetalSetSpec(bmhName, true)
1057+
noBondSpec["ctlplaneInterface"] = "eth0"
1058+
1059+
DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, noBondSpec))
1060+
1061+
// Verify provision server to be ready
1062+
patchProvisionServerReady()
1063+
1064+
// Wait for network data secret without bonding
1065+
Eventually(func(g Gomega) {
1066+
secretName := types.NamespacedName{
1067+
Name: strings.Join([]string{baremetalSetName.Name, "cloudinit-networkdata", "compute-0"}, "-"),
1068+
Namespace: namespace,
1069+
}
1070+
secret := &corev1.Secret{}
1071+
g.Expect(th.K8sClient.Get(th.Ctx, secretName, secret)).To(Succeed())
1072+
1073+
networkData := string(secret.Data["networkData"])
1074+
g.Expect(networkData).ShouldNot(ContainSubstring("type: bond"))
1075+
g.Expect(networkData).Should(ContainSubstring("type: vif"))
1076+
}, th.Timeout, th.Interval).Should(Succeed())
1077+
})
1078+
1079+
It("Should generate networkdata with bonding and VLAN", func() {
1080+
bondVlanSpec := DefaultBaremetalSetSpec(bmhName, true)
1081+
bondVlanSpec["ctlplaneInterface"] = "bond0"
1082+
bondVlanSpec["ctlplaneVlan"] = 100
1083+
bondVlanSpec["ctlplaneBond"] = map[string]any{
1084+
"bondInterfaces": []string{"eno1", "eno2"},
1085+
"bondMode": "802.3ad",
1086+
}
1087+
1088+
DeferCleanup(th.DeleteInstance, CreateBaremetalSet(baremetalSetName, bondVlanSpec))
1089+
1090+
// Verify provision server to be ready
1091+
patchProvisionServerReady()
1092+
1093+
// Wait for network data secret with bonding + VLAN
1094+
Eventually(func(g Gomega) {
1095+
secretName := types.NamespacedName{
1096+
Name: strings.Join([]string{baremetalSetName.Name, "cloudinit-networkdata", "compute-0"}, "-"),
1097+
Namespace: namespace,
1098+
}
1099+
secret := &corev1.Secret{}
1100+
g.Expect(th.K8sClient.Get(th.Ctx, secretName, secret)).To(Succeed())
1101+
1102+
networkData := string(secret.Data["networkData"])
1103+
g.Expect(networkData).Should(ContainSubstring("type: bond"))
1104+
g.Expect(networkData).Should(ContainSubstring("bond_mode: 802.3ad"))
1105+
g.Expect(networkData).Should(ContainSubstring("type: vlan"))
1106+
g.Expect(networkData).Should(ContainSubstring("vlan_id: 100"))
1107+
g.Expect(networkData).Should(ContainSubstring("bond0.100"))
1108+
}, th.Timeout, th.Interval).Should(Succeed())
1109+
})
1110+
})
9501111
})

0 commit comments

Comments
 (0)