From 1e7816ea2c3aefd6f3ce1eee32a333b86d404709 Mon Sep 17 00:00:00 2001 From: Pujol Date: Fri, 15 Aug 2025 16:25:09 +0200 Subject: [PATCH] cisco-nxos-provider: configure ISIS Configure ISIS on a NXOS device allowing the parametrization of: 1) the instance name, 2) the Network Entitity Title, 3) the type ("Level1", "Level2", and "Level12"), 4) the number of seconds of the on-startup overload bit, and 5) two address families: IPv4 and IPv6 unicast. With this package we support the minimum configuration required to build the underlay as in this example: ``` router isis UNDERLAY net 49.0001.0001.0000.0001.00 is-type level-1 set-overload-bit on-startup 61 address-family ipv4 unicast address-family ipv6 unicast ``` --- internal/provider/cisco/nxos/isis/isis.go | 106 +++++++++++ .../provider/cisco/nxos/isis/isis_test.go | 168 ++++++++++++++++++ .../cisco/nxos/isis/isistype_string.go | 26 +++ 3 files changed, 300 insertions(+) create mode 100644 internal/provider/cisco/nxos/isis/isis.go create mode 100644 internal/provider/cisco/nxos/isis/isis_test.go create mode 100644 internal/provider/cisco/nxos/isis/isistype_string.go diff --git a/internal/provider/cisco/nxos/isis/isis.go b/internal/provider/cisco/nxos/isis/isis.go new file mode 100644 index 00000000..6bca5cf8 --- /dev/null +++ b/internal/provider/cisco/nxos/isis/isis.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company +// SPDX-License-Identifier: Apache-2.0 + +package isis + +import ( + "fmt" + + "github.com/openconfig/ygot/ygot" + + nxos "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/genyang" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/gnmiext" +) + +var _ gnmiext.DeviceConf = (*ISIS)(nil) + +type ISIS struct { + // name of the ISIS process, e.g., `router isis UNDERLAY` + Name string + // Network Entity Title, e.g., `net 49.0001.0001.0000.0001.00` + NET string + // Level is type. e.g., `is-type level-1` + Level ISISType + // overloadbit options + OverloadBit *OverloadBit + // supported families + AddressFamilies []ISISAFType +} + +type OverloadBit struct { + OnStartup uint32 +} + +//go:generate stringer -type=ISISType +type ISISType int + +const ( + Level1 ISISType = iota + 1 + Level2 + Level12 +) + +type ISISAFType int + +const ( + IPv4Unicast = iota + 1 + IPv6Unicast +) + +func (i *ISIS) ToYGOT(_ gnmiext.Client) ([]gnmiext.Update, error) { + if i.Name == "" { + return nil, fmt.Errorf("isis: name must be set") + } + if i.NET == "" { + return nil, fmt.Errorf("isis: NET must be set") + } + instList := &nxos.Cisco_NX_OSDevice_System_IsisItems_InstItems_InstList{ + Name: ygot.String(i.Name), + } + + domList := instList.GetOrCreateDomItems().GetOrCreateDomList("default") + domList.Net = ygot.String(i.NET) + switch i.Level { + case Level1: + domList.IsType = nxos.Cisco_NX_OSDevice_Isis_IsT_l1 + case Level2: + domList.IsType = nxos.Cisco_NX_OSDevice_Isis_IsT_l2 + case Level12: + domList.IsType = nxos.Cisco_NX_OSDevice_Isis_IsT_l12 + default: + return nil, fmt.Errorf("isis: invalid level type %d", i.Level) + } + + if i.OverloadBit != nil { + olItems := domList.GetOrCreateOverloadItems() + olItems.AdminSt = nxos.Cisco_NX_OSDevice_Isis_OverloadAdminSt_bootup + olItems.StartupTime = ygot.Uint32(i.OverloadBit.OnStartup) + } + + for af := range i.AddressFamilies { + switch i.AddressFamilies[af] { + case IPv4Unicast: + domList.GetOrCreateAfItems().GetOrCreateDomAfList(nxos.Cisco_NX_OSDevice_Isis_AfT_v4) + case IPv6Unicast: + domList.GetOrCreateAfItems().GetOrCreateDomAfList(nxos.Cisco_NX_OSDevice_Isis_AfT_v6) + default: + return nil, fmt.Errorf("isis: invalid address family type %d", i.AddressFamilies[af]) + } + } + + return []gnmiext.Update{ + gnmiext.ReplacingUpdate{ + XPath: "System/isis-items/inst-items/Inst-list[name=" + i.Name + "]", + Value: instList, + }, + }, nil +} + +// Reset removes the ISIS process with the given name from the device. +func (i *ISIS) Reset(_ gnmiext.Client) ([]gnmiext.Update, error) { + return []gnmiext.Update{ + gnmiext.DeletingUpdate{ + XPath: "System/isis-items/inst-items/Inst-list[name=" + i.Name + "]", + }, + }, nil +} diff --git a/internal/provider/cisco/nxos/isis/isis_test.go b/internal/provider/cisco/nxos/isis/isis_test.go new file mode 100644 index 00000000..68fe69a9 --- /dev/null +++ b/internal/provider/cisco/nxos/isis/isis_test.go @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company +// SPDX-License-Identifier: Apache-2.0 +package isis + +import ( + "testing" + + nxos "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/genyang" + "github.com/ironcore-dev/network-operator/internal/provider/cisco/nxos/gnmiext" +) + +// TestToYGOT tests a configuration with only ISIS for IPv6 +func TestToYGOT(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + NET: "49.0001.0001.0000.0001.00", + Level: Level12, + OverloadBit: &OverloadBit{ + OnStartup: 61, // seconds + }, + AddressFamilies: []ISISAFType{ + IPv6Unicast, + }, + } + got, err := isis.ToYGOT(nil) + if err != nil { + t.Fatalf("ToYGOT() error = %v", err) + } + if len(got) != 1 { + t.Fatalf("ToYGOT() expected 1 update, got %d", len(got)) + } + update, ok := got[0].(gnmiext.ReplacingUpdate) + if !ok { + t.Errorf("expected value to be of type ReplacingUpdate") + } + if update.XPath != "System/isis-items/inst-items/Inst-list[name=UNDERLAY]" { + t.Errorf("expected XPath 'System/isis-items/inst-items/Inst-list[name=UNDERLAY]', got %s", update.XPath) + } + instList, ok := update.Value.(*nxos.Cisco_NX_OSDevice_System_IsisItems_InstItems_InstList) + if !ok { + t.Errorf("expected value to be of type *nxos.Cisco_NX_OSDevice_System_IsisItems_InstItems_InstList") + } + if instList.Name == nil || *instList.Name != "UNDERLAY" { + t.Errorf("expected instList.Name to be 'UNDERLAY', got %v", instList.Name) + } + domList := instList.GetDomItems().GetDomList("default") + if domList == nil { + t.Fatalf("expected domList for default to be present") + } + if *domList.Net != isis.NET { + t.Errorf("Net not set correctly") + } + if domList.IsType != nxos.Cisco_NX_OSDevice_Isis_IsT_l12 { + t.Errorf("Level not set correctly") + } + if domList.GetOverloadItems().AdminSt != nxos.Cisco_NX_OSDevice_Isis_OverloadAdminSt_bootup { + t.Errorf("OverloadBit AdminSt not set correctly") + } + if *domList.GetOverloadItems().StartupTime != isis.OverloadBit.OnStartup { + t.Errorf("OverloadBit StartupTime not set correctly") + } + if len(domList.GetAfItems().DomAfList) != 1 { + t.Errorf("expected 1 address family") + } + if domList.GetAfItems().GetDomAfList(nxos.Cisco_NX_OSDevice_Isis_AfT_v6) == nil { + t.Errorf("expected IPv6 unicast to be enabled, but it is disabled") + } +} + +func TestISIS_ToYGOT_InvalidLevel(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + NET: "49.0001.0001.0000.0001.00", + Level: ISISType(99), + AddressFamilies: []ISISAFType{IPv4Unicast}, + } + _, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err == nil { + t.Error("expected error for invalid level, got nil") + } +} + +func TestISIS_ToYGOT_InvalidAddressFamily(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + NET: "49.0001.0001.0000.0001.00", + Level: Level1, + AddressFamilies: []ISISAFType{ISISAFType(99)}, + } + _, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err == nil { + t.Error("expected error for invalid address family, got nil") + } +} + +func TestISIS_ToYGOT_NoOverloadBit(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + NET: "49.0001.0001.0000.0001.00", + Level: Level1, + AddressFamilies: []ISISAFType{IPv4Unicast}, + OverloadBit: nil, + } + _, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err != nil { + t.Errorf("unexpected error when OverloadBit is nil: %v", err) + } +} + +func TestISIS_ToYGOT_EmptyAddressFamilies(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + NET: "49.0001.0001.0000.0001.00", + Level: Level1, + AddressFamilies: []ISISAFType{}, + } + updates, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err != nil { + t.Errorf("unexpected error for empty address families: %v", err) + } + if len(updates) != 1 { + t.Errorf("expected 1 update, got %d", len(updates)) + } +} + +func TestISIS_Reset(t *testing.T) { + isis := &ISIS{Name: "UNDERLAY"} + updates, err := isis.Reset(&gnmiext.ClientMock{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(updates) != 1 { + t.Fatalf("expected 1 update, got %d", len(updates)) + } + du, ok := updates[0].(gnmiext.DeletingUpdate) + if !ok { + t.Fatalf("expected DeletingUpdate, got %T", updates[0]) + } + expectedXPath := "System/isis-items/inst-items/Inst-list[name=UNDERLAY]" + if du.XPath != expectedXPath { + t.Errorf("expected XPath %q, got %q", expectedXPath, du.XPath) + } +} + +func TestISIS_MissingMandatoryFields(t *testing.T) { + t.Run("missing name", func(t *testing.T) { + isis := &ISIS{ + NET: "49.0001.0001.0000.0001.00", + Level: Level1, + AddressFamilies: []ISISAFType{IPv4Unicast}, + } + _, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err == nil { + t.Error("expected error for empty name, got nil") + } + }) + t.Run("missing NET", func(t *testing.T) { + isis := &ISIS{ + Name: "UNDERLAY", + Level: Level1, + AddressFamilies: []ISISAFType{IPv4Unicast}, + } + _, err := isis.ToYGOT(&gnmiext.ClientMock{}) + if err == nil { + t.Error("expected error for empty NET, got nil") + } + }) +} diff --git a/internal/provider/cisco/nxos/isis/isistype_string.go b/internal/provider/cisco/nxos/isis/isistype_string.go new file mode 100644 index 00000000..27ea4a0c --- /dev/null +++ b/internal/provider/cisco/nxos/isis/isistype_string.go @@ -0,0 +1,26 @@ +// Code generated by "stringer -type=ISISType"; DO NOT EDIT. + +package isis + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Level1-1] + _ = x[Level2-2] + _ = x[Level12-3] +} + +const _ISISType_name = "Level1Level2Level12" + +var _ISISType_index = [...]uint8{0, 6, 12, 19} + +func (i ISISType) String() string { + i -= 1 + if i < 0 || i >= ISISType(len(_ISISType_index)-1) { + return "ISISType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _ISISType_name[_ISISType_index[i]:_ISISType_index[i+1]] +}