Skip to content

Commit 2ecded4

Browse files
committed
[NXOS] LLDP provider implementation
* Enables/disables LLDP feature and configures global and per-interface settings on Cisco NX-OS switches.
1 parent a035b54 commit 2ecded4

10 files changed

Lines changed: 496 additions & 4 deletions

File tree

cmd/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ import (
4040
_ "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos"
4141
_ "github.com/ironcore-dev/network-operator/internal/provider/openconfig"
4242

43+
// Import provider-specific config validators.
44+
_ "github.com/ironcore-dev/network-operator/internal/providerconfig/cisco/nx"
45+
4346
nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
4447
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
4548
nxcontroller "github.com/ironcore-dev/network-operator/internal/controller/cisco/nx"

internal/controller/core/lldp_controller.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/ironcore-dev/network-operator/internal/conditions"
3535
"github.com/ironcore-dev/network-operator/internal/deviceutil"
3636
"github.com/ironcore-dev/network-operator/internal/provider"
37+
"github.com/ironcore-dev/network-operator/internal/providerconfig"
3738
"github.com/ironcore-dev/network-operator/internal/resourcelock"
3839
)
3940

@@ -201,6 +202,8 @@ type lldpScope struct {
201202
Provider provider.LLDPProvider
202203
// ProviderConfig is the resource referenced by LLDP.Spec.ProviderConfigRef, if any.
203204
ProviderConfig *provider.ProviderConfig
205+
// ProviderConfigScope is the scope with the resources referenced in ProviderConfig, if applicable.
206+
ProviderConfigScope *providerconfig.Scope
204207
}
205208

206209
func (r *LLDPReconciler) reconcile(ctx context.Context, s *lldpScope) (_ ctrl.Result, reterr error) {
@@ -240,9 +243,17 @@ func (r *LLDPReconciler) reconcile(ctx context.Context, s *lldpScope) (_ ctrl.Re
240243
}()
241244

242245
// Ensure the LLDP is realized on the remote device.
246+
// Convert providerconfig.Scope to provider.ProviderConfigScope.
247+
// These are separate types to decouple the validation layer (which uses k8s client)
248+
// from the provider layer (which should not depend on k8s client).
249+
var providerScope *provider.ProviderConfigScope
250+
if s.ProviderConfigScope != nil {
251+
providerScope = provider.NewProviderConfigScope(s.ProviderConfigScope.Raw())
252+
}
243253
err = s.Provider.EnsureLLDP(ctx, &provider.LLDPRequest{
244-
LLDP: s.LLDP,
245-
ProviderConfig: s.ProviderConfig,
254+
LLDP: s.LLDP,
255+
ProviderConfig: s.ProviderConfig,
256+
ProviderConfigScope: providerScope,
246257
})
247258

248259
cond := conditions.FromError(err)
@@ -321,6 +332,24 @@ func (r *LLDPReconciler) validateProviderConfigRef(ctx context.Context, s *lldpS
321332
return nil, reconcile.TerminalError(fmt.Errorf("unsupported ProviderConfigRef Kind %q on this provider", gv))
322333
}
323334

335+
// if a provider-specific validator is registered, use it
336+
if validator, ok := providerconfig.GetValidator(gvk); ok {
337+
reader := r.APIReader
338+
if reader == nil {
339+
// fall back to the cached client if APIReader is not set
340+
reader = r.Client
341+
}
342+
s.ProviderConfigScope, err = validator(ctx, reader, s.LLDP, s.LLDP.Spec.ProviderConfigRef)
343+
if err != nil {
344+
conditions.Set(s.LLDP, metav1.Condition{
345+
Type: v1alpha1.ConfiguredCondition,
346+
Status: metav1.ConditionFalse,
347+
Reason: v1alpha1.IncompatibleProviderConfigRef,
348+
Message: fmt.Sprintf("ProviderConfigRef validation failed: %v", err),
349+
})
350+
return nil, reconcile.TerminalError(fmt.Errorf("configuration error in provider config ref %w", err))
351+
}
352+
}
324353
return cfg, nil
325354
}
326355

internal/controller/core/lldp_controller_test.go

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"k8s.io/apimachinery/pkg/api/meta"
1515

16+
nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
1617
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
1718
)
1819

@@ -56,6 +57,12 @@ var _ = Describe("LLDP Controller", func() {
5657
err := k8sClient.Get(ctx, resourceKey, lldp)
5758
if err == nil {
5859
Expect(k8sClient.Delete(ctx, lldp)).To(Succeed())
60+
61+
By("Waiting for LLDP resource to be fully deleted")
62+
Eventually(func(g Gomega) {
63+
err := k8sClient.Get(ctx, resourceKey, &v1alpha1.LLDP{})
64+
g.Expect(errors.IsNotFound(err)).To(BeTrue())
65+
}).Should(Succeed())
5966
}
6067

6168
By("Cleaning up the Device resource")
@@ -274,4 +281,245 @@ var _ = Describe("LLDP Controller", func() {
274281
}).Should(Succeed())
275282
})
276283
})
284+
285+
Context("When reconciling with ProviderConfigRef", func() {
286+
const (
287+
deviceName = "testlldp-provider-device"
288+
resourceName = "testlldp-provider-lldp"
289+
)
290+
291+
resourceKey := client.ObjectKey{Name: resourceName, Namespace: metav1.NamespaceDefault}
292+
deviceKey := client.ObjectKey{Name: deviceName, Namespace: metav1.NamespaceDefault}
293+
294+
var (
295+
device *v1alpha1.Device
296+
lldp *v1alpha1.LLDP
297+
)
298+
299+
BeforeEach(func() {
300+
By("Creating the custom resource for the Kind Device")
301+
device = &v1alpha1.Device{}
302+
if err := k8sClient.Get(ctx, deviceKey, device); errors.IsNotFound(err) {
303+
device = &v1alpha1.Device{
304+
ObjectMeta: metav1.ObjectMeta{
305+
Name: deviceName,
306+
Namespace: metav1.NamespaceDefault,
307+
},
308+
Spec: v1alpha1.DeviceSpec{
309+
Endpoint: v1alpha1.Endpoint{
310+
Address: "192.168.10.2:9339",
311+
},
312+
},
313+
}
314+
Expect(k8sClient.Create(ctx, device)).To(Succeed())
315+
}
316+
})
317+
318+
AfterEach(func() {
319+
By("Cleaning up the LLDP resource")
320+
lldp = &v1alpha1.LLDP{}
321+
err := k8sClient.Get(ctx, resourceKey, lldp)
322+
if err == nil {
323+
Expect(k8sClient.Delete(ctx, lldp)).To(Succeed())
324+
325+
By("Waiting for LLDP resource to be fully deleted")
326+
Eventually(func(g Gomega) {
327+
err := k8sClient.Get(ctx, resourceKey, &v1alpha1.LLDP{})
328+
g.Expect(errors.IsNotFound(err)).To(BeTrue())
329+
}).Should(Succeed())
330+
}
331+
332+
By("Cleaning up the Device resource")
333+
err = k8sClient.Get(ctx, deviceKey, device)
334+
if err == nil {
335+
Expect(k8sClient.Delete(ctx, device, client.PropagationPolicy(metav1.DeletePropagationForeground))).To(Succeed())
336+
}
337+
338+
By("Verifying the resource has been deleted")
339+
Eventually(func(g Gomega) {
340+
g.Expect(testProvider.LLDP).To(BeNil(), "Provider should have no LLDP configured")
341+
}).Should(Succeed())
342+
})
343+
344+
It("Should handle missing ProviderConfigRef", func() {
345+
By("Creating LLDP with a non-existent ProviderConfigRef")
346+
lldp = &v1alpha1.LLDP{
347+
ObjectMeta: metav1.ObjectMeta{
348+
Name: resourceName,
349+
Namespace: metav1.NamespaceDefault,
350+
},
351+
Spec: v1alpha1.LLDPSpec{
352+
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
353+
AdminState: v1alpha1.AdminStateUp,
354+
ProviderConfigRef: &v1alpha1.TypedLocalObjectReference{
355+
APIVersion: "nx.cisco.networking.metal.ironcore.dev/v1alpha1",
356+
Kind: "LLDPConfig",
357+
Name: "non-existent-config",
358+
},
359+
},
360+
}
361+
Expect(k8sClient.Create(ctx, lldp)).To(Succeed())
362+
363+
By("Verifying the controller sets ConfiguredCondition to False")
364+
Eventually(func(g Gomega) {
365+
err := k8sClient.Get(ctx, resourceKey, lldp)
366+
g.Expect(err).NotTo(HaveOccurred())
367+
368+
cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition)
369+
g.Expect(cond).ToNot(BeNil())
370+
g.Expect(cond.Status).To(Equal(metav1.ConditionFalse))
371+
g.Expect(cond.Reason).To(Equal(v1alpha1.IncompatibleProviderConfigRef))
372+
}).Should(Succeed())
373+
})
374+
375+
It("Should handle invalid ProviderConfigRef API version", func() {
376+
By("Creating LLDP with invalid API version in ProviderConfigRef")
377+
lldp = &v1alpha1.LLDP{
378+
ObjectMeta: metav1.ObjectMeta{
379+
Name: resourceName,
380+
Namespace: metav1.NamespaceDefault,
381+
},
382+
Spec: v1alpha1.LLDPSpec{
383+
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
384+
AdminState: v1alpha1.AdminStateUp,
385+
ProviderConfigRef: &v1alpha1.TypedLocalObjectReference{
386+
APIVersion: "invalid-api-version",
387+
Kind: "LLDPConfig",
388+
Name: "some-config",
389+
},
390+
},
391+
}
392+
Expect(k8sClient.Create(ctx, lldp)).To(Succeed())
393+
394+
By("Verifying the controller sets ConfiguredCondition to False with IncompatibleProviderConfigRef")
395+
Eventually(func(g Gomega) {
396+
err := k8sClient.Get(ctx, resourceKey, lldp)
397+
g.Expect(err).NotTo(HaveOccurred())
398+
399+
cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition)
400+
g.Expect(cond).ToNot(BeNil())
401+
g.Expect(cond.Status).To(Equal(metav1.ConditionFalse))
402+
g.Expect(cond.Reason).To(Equal(v1alpha1.IncompatibleProviderConfigRef))
403+
}).Should(Succeed())
404+
})
405+
406+
It("Should handle unsupported ProviderConfigRef Kind", func() {
407+
By("Creating LLDP with unsupported Kind in ProviderConfigRef")
408+
lldp = &v1alpha1.LLDP{
409+
ObjectMeta: metav1.ObjectMeta{
410+
Name: resourceName,
411+
Namespace: metav1.NamespaceDefault,
412+
},
413+
Spec: v1alpha1.LLDPSpec{
414+
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
415+
AdminState: v1alpha1.AdminStateUp,
416+
ProviderConfigRef: &v1alpha1.TypedLocalObjectReference{
417+
APIVersion: "v1",
418+
Kind: "ConfigMap",
419+
Name: "some-config",
420+
},
421+
},
422+
}
423+
Expect(k8sClient.Create(ctx, lldp)).To(Succeed())
424+
425+
By("Verifying the controller sets ConfiguredCondition to False with IncompatibleProviderConfigRef")
426+
Eventually(func(g Gomega) {
427+
err := k8sClient.Get(ctx, resourceKey, lldp)
428+
g.Expect(err).NotTo(HaveOccurred())
429+
430+
cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition)
431+
g.Expect(cond).ToNot(BeNil())
432+
g.Expect(cond.Status).To(Equal(metav1.ConditionFalse))
433+
g.Expect(cond.Reason).To(Equal(v1alpha1.IncompatibleProviderConfigRef))
434+
}).Should(Succeed())
435+
})
436+
437+
It("Should successfully reconcile with valid ProviderConfigRef", func() {
438+
const (
439+
interfaceName = "testlldp-provider-interface"
440+
lldpConfigName = "testlldp-provider-config"
441+
)
442+
443+
By("Creating the Interface resource")
444+
intf := &v1alpha1.Interface{
445+
ObjectMeta: metav1.ObjectMeta{
446+
Name: interfaceName,
447+
Namespace: metav1.NamespaceDefault,
448+
},
449+
Spec: v1alpha1.InterfaceSpec{
450+
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
451+
Name: "Ethernet1/1",
452+
Type: v1alpha1.InterfaceTypePhysical,
453+
AdminState: v1alpha1.AdminStateUp,
454+
},
455+
}
456+
Expect(k8sClient.Create(ctx, intf)).To(Succeed())
457+
458+
By("Creating the LLDPConfig resource")
459+
lldpConfig := &nxv1alpha1.LLDPConfig{
460+
ObjectMeta: metav1.ObjectMeta{
461+
Name: lldpConfigName,
462+
Namespace: metav1.NamespaceDefault,
463+
},
464+
Spec: nxv1alpha1.LLDPConfigSpec{
465+
InitDelay: 5,
466+
HoldTime: 180,
467+
InterfaceRefs: []nxv1alpha1.LLDPInterface{{
468+
LocalObjectReference: v1alpha1.LocalObjectReference{Name: interfaceName},
469+
AdminRxState: v1alpha1.AdminStateUp,
470+
AdminTxState: v1alpha1.AdminStateDown,
471+
}},
472+
},
473+
}
474+
Expect(k8sClient.Create(ctx, lldpConfig)).To(Succeed())
475+
476+
By("Creating LLDP with valid ProviderConfigRef")
477+
lldp = &v1alpha1.LLDP{
478+
ObjectMeta: metav1.ObjectMeta{
479+
Name: resourceName,
480+
Namespace: metav1.NamespaceDefault,
481+
},
482+
Spec: v1alpha1.LLDPSpec{
483+
DeviceRef: v1alpha1.LocalObjectReference{Name: deviceName},
484+
AdminState: v1alpha1.AdminStateUp,
485+
ProviderConfigRef: &v1alpha1.TypedLocalObjectReference{
486+
APIVersion: nxv1alpha1.GroupVersion.String(),
487+
Kind: "LLDPConfig",
488+
Name: lldpConfigName,
489+
},
490+
},
491+
}
492+
Expect(k8sClient.Create(ctx, lldp)).To(Succeed())
493+
494+
By("Verifying the controller sets all conditions to True")
495+
Eventually(func(g Gomega) {
496+
err := k8sClient.Get(ctx, resourceKey, lldp)
497+
g.Expect(err).NotTo(HaveOccurred())
498+
499+
cond := meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ReadyCondition)
500+
g.Expect(cond).ToNot(BeNil())
501+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
502+
503+
cond = meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.ConfiguredCondition)
504+
g.Expect(cond).ToNot(BeNil())
505+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
506+
507+
cond = meta.FindStatusCondition(lldp.Status.Conditions, v1alpha1.OperationalCondition)
508+
g.Expect(cond).ToNot(BeNil())
509+
g.Expect(cond.Status).To(Equal(metav1.ConditionTrue))
510+
}).Should(Succeed())
511+
512+
By("Verifying the LLDP is created in the provider")
513+
Eventually(func(g Gomega) {
514+
g.Expect(testProvider.LLDP).ToNot(BeNil())
515+
g.Expect(testProvider.LLDP.GetName()).To(Equal(resourceName))
516+
}).Should(Succeed())
517+
518+
By("Cleaning up the LLDPConfig resource")
519+
Expect(k8sClient.Delete(ctx, lldpConfig)).To(Succeed())
520+
521+
By("Cleaning up the Interface resource")
522+
Expect(k8sClient.Delete(ctx, intf)).To(Succeed())
523+
})
524+
})
277525
})

internal/controller/core/suite_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ import (
2828
logf "sigs.k8s.io/controller-runtime/pkg/log"
2929
"sigs.k8s.io/controller-runtime/pkg/log/zap"
3030

31+
nxv1alpha1 "github.com/ironcore-dev/network-operator/api/cisco/nx/v1alpha1"
3132
"github.com/ironcore-dev/network-operator/api/core/v1alpha1"
3233
"github.com/ironcore-dev/network-operator/internal/deviceutil"
3334
"github.com/ironcore-dev/network-operator/internal/provider"
3435
"github.com/ironcore-dev/network-operator/internal/resourcelock"
36+
37+
// Import provider-specific config validators to register them.
38+
_ "github.com/ironcore-dev/network-operator/internal/providerconfig/cisco/nx"
3539
// +kubebuilder:scaffold:imports
3640
)
3741

@@ -67,6 +71,9 @@ var _ = BeforeSuite(func() {
6771
err = v1alpha1.AddToScheme(scheme.Scheme)
6872
Expect(err).NotTo(HaveOccurred())
6973

74+
err = nxv1alpha1.AddToScheme(scheme.Scheme)
75+
Expect(err).NotTo(HaveOccurred())
76+
7077
// +kubebuilder:scaffold:scheme
7178

7279
By("bootstrapping test environment")

0 commit comments

Comments
 (0)