diff --git a/.gitignore b/.gitignore index cfcc3d81..14d7b9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ clab-containerlab-test-setup/ tls/* testbin/* testdata/.containerlab.yaml.bak +pkg/*/testdata* # Test binary, build with `go test -c` *.test diff --git a/cmd/manager/main.go b/cmd/manager/main.go index a6fb97c0..536390ab 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -38,6 +38,7 @@ import ( "github.com/telekom/das-schiff-network-operator/pkg/macvlan" "github.com/telekom/das-schiff-network-operator/pkg/managerconfig" "github.com/telekom/das-schiff-network-operator/pkg/monitoring" + "github.com/telekom/das-schiff-network-operator/pkg/nl" "github.com/telekom/das-schiff-network-operator/pkg/notrack" "github.com/telekom/das-schiff-network-operator/pkg/reconciler" @@ -138,7 +139,7 @@ func main() { os.Exit(1) } - anycastTracker := &anycast.Tracker{} + anycastTracker := anycast.NewTracker(&nl.Toolkit{}) if err = (&networkv1alpha1.VRFRouteConfiguration{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "VRFRouteConfiguration") diff --git a/pkg/anycast/anycast.go b/pkg/anycast/anycast.go index c1b5def9..04d17e37 100644 --- a/pkg/anycast/anycast.go +++ b/pkg/anycast/anycast.go @@ -2,10 +2,12 @@ package anycast import ( "bytes" + "fmt" "net" "time" "github.com/go-logr/logr" + "github.com/telekom/das-schiff-network-operator/pkg/nl" "github.com/vishvananda/netlink" "golang.org/x/sys/unix" ctrl "sigs.k8s.io/controller-runtime" @@ -19,6 +21,12 @@ var ( type Tracker struct { TrackedBridges []int + toolkit nl.ToolkitInterface +} + +func NewTracker(toolkit nl.ToolkitInterface) *Tracker { + return &Tracker{TrackedBridges: []int{}, + toolkit: toolkit} } // TODO: Anycast Support is currently highly experimental. @@ -26,13 +34,13 @@ type Tracker struct { func (t *Tracker) checkTrackedInterfaces() { logger := ctrl.Log.WithName("anycast") for _, intfIdx := range t.TrackedBridges { - intf, err := netlink.LinkByIndex(intfIdx) + intf, err := t.toolkit.LinkByIndex(intfIdx) if err != nil { logger.Error(err, "couldn't load interface", "index", intfIdx) continue } - syncInterface(intf.(*netlink.Bridge), logger) + _ = syncInterface(intf.(*netlink.Bridge), t.toolkit, logger) } } @@ -81,11 +89,11 @@ func filterNeighbors(neighIn []netlink.Neigh) (neighOut []netlink.Neigh) { return neighOut } -func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32, logger logr.Logger) { - bridgeNeighbors, err := netlink.NeighList(intf.Attrs().Index, family) +func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32, toolkit nl.ToolkitInterface, logger logr.Logger) error { + bridgeNeighbors, err := toolkit.NeighList(intf.Attrs().Index, family) if err != nil { logger.Error(err, "error getting v4 neighbors of interface", "interface", intf.Attrs().Name) - return + return fmt.Errorf("error getting v4 neighbors of interface %s: %w", intf.Attrs().Name, err) } bridgeNeighbors = filterNeighbors(bridgeNeighbors) @@ -94,16 +102,16 @@ func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32 Table: int(routingTable), Protocol: anycastRoutesProt, } - routes, err := netlink.RouteListFiltered(family, routeFilterV4, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL) + routes, err := toolkit.RouteListFiltered(family, routeFilterV4, netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL) if err != nil { logger.Error(err, "error getting v4 routes of interface", "interface", intf.Attrs().Name) - return + return fmt.Errorf("error getting v4 routes of interface %s: %w", intf.Attrs().Name, err) } alreadyV4Existing := []*net.IPNet{} for i := range routes { if !containsIPAddress(bridgeNeighbors, routes[i].Dst) { - if err := netlink.RouteDel(&routes[i]); err != nil { + if err := toolkit.RouteDel(&routes[i]); err != nil { logger.Error(err, "error deleting route", "route", routes[i]) } } else { @@ -112,33 +120,37 @@ func syncInterfaceByFamily(intf *netlink.Bridge, family int, routingTable uint32 } for i := range bridgeNeighbors { - ipnet := netlink.NewIPNet(bridgeNeighbors[i].IP) + ipnet := toolkit.NewIPNet(bridgeNeighbors[i].IP) if !containsIPNetwork(alreadyV4Existing, ipnet) { route := buildRoute(family, intf, ipnet, routingTable) - if err := netlink.RouteAdd(route); err != nil { + if err := toolkit.RouteAdd(route); err != nil { logger.Error(err, "error adding route", "route", routes[i]) } } } + + return nil } -func syncInterface(intf *netlink.Bridge, logger logr.Logger) { +func syncInterface(intf *netlink.Bridge, toolkit nl.ToolkitInterface, logger logr.Logger) error { routingTable := uint32(defaultVrfAnycastTable) if intf.Attrs().MasterIndex > 0 { - nl, err := netlink.LinkByIndex(intf.Attrs().MasterIndex) + link, err := toolkit.LinkByIndex(intf.Attrs().MasterIndex) if err != nil { logger.Error(err, "error getting VRF parent of interface", "interface", intf.Attrs().Name) - return + return fmt.Errorf("error getting VRF parent of interface %s: %w", intf.Attrs().Name, err) } - if nl.Type() != "vrf" { + if link.Type() != "vrf" { logger.Info("parent of the interface is not a VRF", "interface", intf.Attrs().Name) - return + return fmt.Errorf("parent interface of %s is not a VRF", intf.Attrs().Name) } - routingTable = nl.(*netlink.Vrf).Table + routingTable = link.(*netlink.Vrf).Table } - syncInterfaceByFamily(intf, unix.AF_INET, routingTable, logger) - syncInterfaceByFamily(intf, unix.AF_INET6, routingTable, logger) + _ = syncInterfaceByFamily(intf, unix.AF_INET, routingTable, toolkit, logger) + _ = syncInterfaceByFamily(intf, unix.AF_INET6, routingTable, toolkit, logger) + + return nil } func (t *Tracker) RunAnycastSync() { diff --git a/pkg/anycast/anycast_test.go b/pkg/anycast/anycast_test.go new file mode 100644 index 00000000..b7217267 --- /dev/null +++ b/pkg/anycast/anycast_test.go @@ -0,0 +1,186 @@ +package anycast + +import ( + "errors" + "net" + "testing" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/telekom/das-schiff-network-operator/pkg/nl" + mock_nl "github.com/telekom/das-schiff-network-operator/pkg/nl/mock" + "github.com/vishvananda/netlink" + "go.uber.org/mock/gomock" + ctrl "sigs.k8s.io/controller-runtime" +) + +var ( + mockctrl *gomock.Controller + logger logr.Logger +) + +func TestAnycast(t *testing.T) { + RegisterFailHandler(Fail) + mockctrl = gomock.NewController(t) + defer mockctrl.Finish() + logger = ctrl.Log.WithName("anycast-test") + RunSpecs(t, + "Anycast Suite") +} + +var _ = Describe("buildRoute()", func() { + It("builds route", func() { + route := buildRoute(0, &netlink.Bridge{}, &net.IPNet{}, 0) + Expect(route).ToNot(BeNil()) + }) +}) + +var _ = Describe("containsIPAddress()", func() { + It("returns false if list does not contain IP address", func() { + result := containsIPAddress([]netlink.Neigh{{IP: net.IPv4(0, 0, 0, 0)}}, &net.IPNet{IP: net.IPv4(1, 1, 1, 1)}) + Expect(result).To(BeFalse()) + }) + It("returns true if list does contain IP address", func() { + result := containsIPAddress([]netlink.Neigh{{IP: net.IPv4(0, 0, 0, 0)}}, &net.IPNet{IP: net.IPv4(0, 0, 0, 0)}) + Expect(result).To(BeTrue()) + }) +}) + +var _ = Describe("containsIPNetwork()", func() { + It("returns false if list does not contain IP network", func() { + result := containsIPNetwork([]*net.IPNet{{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 254)}) + Expect(result).To(BeFalse()) + }) + It("returns true if list does contain IP network", func() { + result := containsIPNetwork([]*net.IPNet{{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}}, + &net.IPNet{IP: net.IPv4(0, 0, 0, 0), Mask: net.IPv4Mask(255, 255, 255, 0)}) + Expect(result).To(BeTrue()) + }) +}) + +var _ = Describe("filterNeighbors()", func() { + It("returns empty list if flags not as expected", func() { + neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_LEARNED}} + result := filterNeighbors(neighIn) + Expect(result).To(BeEmpty()) + }) + It("returns empty list if state not as expected", func() { + neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, State: netlink.NUD_INCOMPLETE}} + result := filterNeighbors(neighIn) + Expect(result).To(BeEmpty()) + }) + It("returns non empty list", func() { + neighIn := []netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED}} + result := filterNeighbors(neighIn) + Expect(result).ToNot(BeEmpty()) + }) +}) + +var _ = Describe("syncInterfaceByFamily()", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + It("returns error if cannot get neighbors", func() { + netlinkMock.EXPECT().NeighList(0, 0).Return(nil, errors.New("fake error")) + err := syncInterfaceByFamily(&netlink.Bridge{}, 0, 0, netlinkMock, logger) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot filter routes", func() { + family := 0 + linkIndex := 0 + netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED}}, nil) + netlinkMock.EXPECT().RouteListFiltered(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil, errors.New("fake error")) + err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger) + Expect(err).To(HaveOccurred()) + }) + It("returns no error if cannot delete route", func() { + family := 0 + linkIndex := 0 + route := netlink.Route{Flags: netlink.NTF_EXT_MANAGED, Dst: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))} + netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, IP: net.IPv4(0, 0, 0, 0)}}, nil) + netlinkMock.EXPECT().RouteListFiltered(0, gomock.Any(), netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL). + Return([]netlink.Route{route}, nil) + netlinkMock.EXPECT().RouteDel(gomock.Any()).Return(errors.New("fake error")) // error is only logged, not returned + netlinkMock.EXPECT().NewIPNet(gomock.Any()).Return(netlink.NewIPNet(net.IPv4(1, 1, 1, 1))) + netlinkMock.EXPECT().RouteAdd(gomock.Any()).Return(errors.New("fake error")) // error is only logged, not returned + err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger) + Expect(err).ToNot(HaveOccurred()) + }) + It("returns no error if appended route", func() { + family := 0 + linkIndex := 0 + route := netlink.Route{Flags: netlink.NTF_EXT_MANAGED, Dst: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))} + netlinkMock.EXPECT().NeighList(linkIndex, family).Return([]netlink.Neigh{{Flags: netlink.NTF_EXT_MANAGED, IP: net.IPv4(1, 1, 1, 1)}}, nil) + netlinkMock.EXPECT().RouteListFiltered(0, gomock.Any(), netlink.RT_FILTER_OIF|netlink.RT_FILTER_TABLE|netlink.RT_FILTER_PROTOCOL). + Return([]netlink.Route{route}, nil) + netlinkMock.EXPECT().NewIPNet(gomock.Any()).Return(netlink.NewIPNet(net.IPv4(1, 1, 1, 1))) + err := syncInterfaceByFamily(&netlink.Bridge{}, family, 0, netlinkMock, logger) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("syncInterface()", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + It("returns no error if interface's Master Index <= 0", func() { + intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: 0}} + // returning error just to quit syncInterfaceByFamily call + netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2) + err := syncInterface(intf, netlinkMock, logger) + Expect(err).ToNot(HaveOccurred()) + }) + It("returns error if cannot get interface by index", func() { + masterIndex := 1 + intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}} + netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(nil, errors.New("fake error")) + err := syncInterface(intf, netlinkMock, logger) + Expect(err).To(HaveOccurred()) + }) + It("returns error if could get interface but it's type is not vrf", func() { + masterIndex := 1 + intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}} + netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(&netlink.Gretun{}, nil) + err := syncInterface(intf, netlinkMock, logger) + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + masterIndex := 1 + intf := &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{MasterIndex: masterIndex}} + netlinkMock.EXPECT().LinkByIndex(masterIndex).Return(&netlink.Vrf{}, nil) + // returning error just to quit syncInterfaceByFamily call + netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2) + err := syncInterface(intf, netlinkMock, logger) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("NewTracker()", func() { + It("returns new anycast tracker", func() { + toolkit := &nl.Toolkit{} + tracker := NewTracker(toolkit) + Expect(tracker).ToNot(BeNil()) + Expect(tracker.toolkit).To(Equal(toolkit)) + }) +}) + +var _ = Describe("checkTrackedInterfaces()", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + It("returns no error if cannot get link by index", func() { + netlinkMock.EXPECT().LinkByIndex(gomock.Any()).Return(nil, errors.New("fake error")) + tracker := NewTracker(netlinkMock) + Expect(tracker).ToNot(BeNil()) + Expect(tracker.toolkit).To(Equal(netlinkMock)) + tracker.TrackedBridges = []int{0} + tracker.checkTrackedInterfaces() + }) + It("returns no error", func() { + netlinkMock.EXPECT().LinkByIndex(gomock.Any()).Return(&netlink.Bridge{}, nil) + // returning error just to quit syncInterface call + netlinkMock.EXPECT().NeighList(gomock.Any(), gomock.Any()).Return(nil, errors.New("fake error")).Times(2) + tracker := NewTracker(netlinkMock) + Expect(tracker).ToNot(BeNil()) + Expect(tracker.toolkit).To(Equal(netlinkMock)) + tracker.TrackedBridges = []int{0} + tracker.checkTrackedInterfaces() + }) +}) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 00000000..8e1769b1 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,72 @@ +package config + +import ( + "os" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +const operatorConfigEnv = "OPERATOR_CONFIG" + +func TestDebounce(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, + "Config Suite") +} + +var _ = Describe("LoadConfig()", func() { + It("returns error if cannot read config", func() { + oldEnv, isSet := os.LookupEnv(operatorConfigEnv) + os.Setenv(operatorConfigEnv, "some-invalid-path") + _, err := LoadConfig() + Expect(err).To(HaveOccurred()) + if isSet { + err = os.Setenv(operatorConfigEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } else { + err = os.Unsetenv(operatorConfigEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) + It("returns error if cannot unmarshal config", func() { + oldEnv, isSet := os.LookupEnv(operatorConfigEnv) + os.Setenv(operatorConfigEnv, "./testdata/invalidConfig.yaml") + _, err := LoadConfig() + Expect(err).To(HaveOccurred()) + if isSet { + err = os.Setenv(operatorConfigEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } else { + err = os.Unsetenv(operatorConfigEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) + It("returns no error", func() { + oldEnv, isSet := os.LookupEnv(operatorConfigEnv) + os.Setenv(operatorConfigEnv, "./testdata/config.yaml") + _, err := LoadConfig() + Expect(err).ToNot(HaveOccurred()) + if isSet { + err = os.Setenv(operatorConfigEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } else { + err = os.Unsetenv(operatorConfigEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) +}) + +var _ = Describe("ShouldSkipVRFConfig()", func() { + It("returns false", func() { + cfg := &Config{SkipVRFConfig: []string{"", "", ""}} + result := cfg.ShouldSkipVRFConfig("skip") + Expect(result).To(BeFalse()) + }) + It("returns true", func() { + cfg := &Config{SkipVRFConfig: []string{"", "skip", ""}} + result := cfg.ShouldSkipVRFConfig("skip") + Expect(result).To(BeTrue()) + }) +}) diff --git a/pkg/config/testdata/config.yaml b/pkg/config/testdata/config.yaml new file mode 100644 index 00000000..b4608b8b --- /dev/null +++ b/pkg/config/testdata/config.yaml @@ -0,0 +1,10 @@ +vnimap: # VRF name to VNI mapping for the site + vrf1: 100 + vrf2: 101 + vrf3: 102 +skipVRFConfig: # Only configure route-maps / prefix-lists for VRFs from the provisioning process +- mgmt_vrf +bpfInterfaces: # Attach eBPF program to the interfaces from the provisioning process +- "br.mgmt_vrf" +- "mgmt_vrf_def" +- "vx.200" \ No newline at end of file diff --git a/pkg/config/testdata/invalidConfig.yaml b/pkg/config/testdata/invalidConfig.yaml new file mode 100644 index 00000000..b40004fb --- /dev/null +++ b/pkg/config/testdata/invalidConfig.yaml @@ -0,0 +1 @@ +-invalid \ No newline at end of file diff --git a/pkg/debounce/debounce_test.go b/pkg/debounce/debounce_test.go new file mode 100644 index 00000000..ded751b4 --- /dev/null +++ b/pkg/debounce/debounce_test.go @@ -0,0 +1,35 @@ +package debounce + +import ( + "testing" + "time" + + "github.com/go-logr/logr" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + ctrl "sigs.k8s.io/controller-runtime" +) + +var ( + logger logr.Logger +) + +var _ = BeforeSuite(func() { + +}) + +func TestDebounce(t *testing.T) { + RegisterFailHandler(Fail) + logger = ctrl.Log.WithName("debounce-test") + RunSpecs(t, + "Debounce Suite") +} + +var _ = Describe("debounce", func() { + Context("NewDebouncer() should", func() { + It("create new debouncer", func() { + d := NewDebouncer(nil, time.Millisecond, logger) + Expect(d).ToNot(BeNil()) + }) + }) +}) diff --git a/pkg/frr/configure.go b/pkg/frr/configure.go index 62daca6b..824005ed 100644 --- a/pkg/frr/configure.go +++ b/pkg/frr/configure.go @@ -24,10 +24,11 @@ type templateConfig struct { Hostname string UnderlayRouterID string + HostRouterID string } -func (m *Manager) Configure(in Configuration) (bool, error) { - config, err := renderSubtemplates(in) +func (m *Manager) Configure(in Configuration, nm *nl.Manager) (bool, error) { + config, err := renderSubtemplates(in, nm) if err != nil { return false, err } @@ -53,8 +54,8 @@ func (m *Manager) Configure(in Configuration) (bool, error) { return false, nil } -func renderSubtemplates(in Configuration) (*templateConfig, error) { - vrfRouterID, err := (&nl.NetlinkManager{}).GetUnderlayIP() +func renderSubtemplates(in Configuration, nlManager *nl.Manager) (*templateConfig, error) { + vrfRouterID, err := nlManager.GetUnderlayIP() if err != nil { return nil, fmt.Errorf("error getting underlay IP: %w", err) } diff --git a/pkg/frr/dbus/dbus.go b/pkg/frr/dbus/dbus.go new file mode 100644 index 00000000..f7bf9196 --- /dev/null +++ b/pkg/frr/dbus/dbus.go @@ -0,0 +1,30 @@ +package dbus + +import ( + "context" + "fmt" + + "github.com/coreos/go-systemd/v22/dbus" +) + +//go:generate mockgen -destination ./mock/mock_frr.go . System,Connection +type System interface { + NewConn(ctx context.Context) (Connection, error) +} + +type Connection interface { + Close() + ReloadUnitContext(context.Context, string, string, chan<- string) (int, error) + GetUnitPropertiesContext(ctx context.Context, unit string) (map[string]interface{}, error) + RestartUnitContext(ctx context.Context, name string, mode string, ch chan<- string) (int, error) +} + +type Toolkit struct{} + +func (*Toolkit) NewConn(ctx context.Context) (Connection, error) { + conn, err := dbus.NewSystemConnectionContext(ctx) + if err != nil { + return nil, fmt.Errorf("error creating new D-Bus connection: %w", err) + } + return conn, nil +} diff --git a/pkg/frr/dbus/mock/mock_frr.go b/pkg/frr/dbus/mock/mock_frr.go new file mode 100644 index 00000000..9140ae48 --- /dev/null +++ b/pkg/frr/dbus/mock/mock_frr.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/telekom/das-schiff-network-operator/pkg/frr/dbus (interfaces: System,Connection) + +// Package mock_dbus is a generated GoMock package. +package mock_dbus + +import ( + context "context" + reflect "reflect" + + dbus "github.com/telekom/das-schiff-network-operator/pkg/frr/dbus" + gomock "go.uber.org/mock/gomock" +) + +// MockSystem is a mock of System interface. +type MockSystem struct { + ctrl *gomock.Controller + recorder *MockSystemMockRecorder +} + +// MockSystemMockRecorder is the mock recorder for MockSystem. +type MockSystemMockRecorder struct { + mock *MockSystem +} + +// NewMockSystem creates a new mock instance. +func NewMockSystem(ctrl *gomock.Controller) *MockSystem { + mock := &MockSystem{ctrl: ctrl} + mock.recorder = &MockSystemMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockSystem) EXPECT() *MockSystemMockRecorder { + return m.recorder +} + +// NewConn mocks base method. +func (m *MockSystem) NewConn(arg0 context.Context) (dbus.Connection, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewConn", arg0) + ret0, _ := ret[0].(dbus.Connection) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewConn indicates an expected call of NewConn. +func (mr *MockSystemMockRecorder) NewConn(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewConn", reflect.TypeOf((*MockSystem)(nil).NewConn), arg0) +} + +// MockConnection is a mock of Connection interface. +type MockConnection struct { + ctrl *gomock.Controller + recorder *MockConnectionMockRecorder +} + +// MockConnectionMockRecorder is the mock recorder for MockConnection. +type MockConnectionMockRecorder struct { + mock *MockConnection +} + +// NewMockConnection creates a new mock instance. +func NewMockConnection(ctrl *gomock.Controller) *MockConnection { + mock := &MockConnection{ctrl: ctrl} + mock.recorder = &MockConnectionMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockConnection) EXPECT() *MockConnectionMockRecorder { + return m.recorder +} + +// Close mocks base method. +func (m *MockConnection) Close() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Close") +} + +// Close indicates an expected call of Close. +func (mr *MockConnectionMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConnection)(nil).Close)) +} + +// GetUnitPropertiesContext mocks base method. +func (m *MockConnection) GetUnitPropertiesContext(arg0 context.Context, arg1 string) (map[string]interface{}, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUnitPropertiesContext", arg0, arg1) + ret0, _ := ret[0].(map[string]interface{}) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUnitPropertiesContext indicates an expected call of GetUnitPropertiesContext. +func (mr *MockConnectionMockRecorder) GetUnitPropertiesContext(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnitPropertiesContext", reflect.TypeOf((*MockConnection)(nil).GetUnitPropertiesContext), arg0, arg1) +} + +// ReloadUnitContext mocks base method. +func (m *MockConnection) ReloadUnitContext(arg0 context.Context, arg1, arg2 string, arg3 chan<- string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReloadUnitContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ReloadUnitContext indicates an expected call of ReloadUnitContext. +func (mr *MockConnectionMockRecorder) ReloadUnitContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReloadUnitContext", reflect.TypeOf((*MockConnection)(nil).ReloadUnitContext), arg0, arg1, arg2, arg3) +} + +// RestartUnitContext mocks base method. +func (m *MockConnection) RestartUnitContext(arg0 context.Context, arg1, arg2 string, arg3 chan<- string) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RestartUnitContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RestartUnitContext indicates an expected call of RestartUnitContext. +func (mr *MockConnectionMockRecorder) RestartUnitContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestartUnitContext", reflect.TypeOf((*MockConnection)(nil).RestartUnitContext), arg0, arg1, arg2, arg3) +} diff --git a/pkg/frr/frr_test.go b/pkg/frr/frr_test.go new file mode 100644 index 00000000..c9ef3ac2 --- /dev/null +++ b/pkg/frr/frr_test.go @@ -0,0 +1,203 @@ +package frr + +import ( + "errors" + "net" + "os" + "testing" + "text/template" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/telekom/das-schiff-network-operator/pkg/config" + mock_dbus "github.com/telekom/das-schiff-network-operator/pkg/frr/dbus/mock" + "github.com/telekom/das-schiff-network-operator/pkg/healthcheck" + "github.com/telekom/das-schiff-network-operator/pkg/nl" + mock_nl "github.com/telekom/das-schiff-network-operator/pkg/nl/mock" + "github.com/vishvananda/netlink" + "go.uber.org/mock/gomock" +) + +const ( + frrConf = "frr.conf" +) + +var ( + mockctrl *gomock.Controller + tmpDir string +) + +var _ = BeforeSuite(func() { + var err error + tmpDir, err = os.MkdirTemp(".", "testdata") + Expect(err).ToNot(HaveOccurred()) + + f, err := os.Create(tmpDir + "/" + frrConf) + Expect(err).ToNot(HaveOccurred()) + err = f.Close() + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + err := os.RemoveAll(tmpDir) + Expect(err).ToNot(HaveOccurred()) +}) + +func TestFRR(t *testing.T) { + RegisterFailHandler(Fail) + mockctrl = gomock.NewController(t) + defer mockctrl.Finish() + RunSpecs(t, + "FRR Suite") +} + +var _ = Describe("frr", func() { + Context("NewFRRManager() should", func() { + It("create new FRR Manager", func() { + m := NewFRRManager() + Expect(m).ToNot(BeNil()) + }) + }) + Context("Init() should", func() { + It("return error if cannot read template config", func() { + m := &Manager{} + err := m.Init() + Expect(err).To(HaveOccurred()) + }) + It("return error if cannot write template config file", func() { + m := &Manager{ConfigPath: "testdata/" + frrConf} + err := m.Init() + Expect(err).To(HaveOccurred()) + }) + It("return no error", func() { + m := &Manager{ + ConfigPath: tmpDir + "/" + frrConf, + TemplatePath: tmpDir + "/frr.tpl.conf", + } + err := m.Init() + Expect(err).ToNot(HaveOccurred()) + }) + }) + Context("ShouldTemplateVRF() should", func() { + It("return false", func() { + v := &VRFConfiguration{VNI: config.SkipVrfTemplateVni} + result := v.ShouldTemplateVRF() + Expect(result).To(BeFalse()) + }) + It("return true", func() { + v := &VRFConfiguration{VNI: 0} + result := v.ShouldTemplateVRF() + Expect(result).To(BeTrue()) + }) + }) + Context("ShouldDefineRT() should", func() { + It("return false", func() { + v := &VRFConfiguration{RT: ""} + result := v.ShouldDefineRT() + Expect(result).To(BeFalse()) + }) + It("return true", func() { + v := &VRFConfiguration{RT: "value"} + result := v.ShouldDefineRT() + Expect(result).To(BeTrue()) + }) + }) + Context("ReloadFRR() should", func() { + dbusMock := mock_dbus.NewMockSystem(mockctrl) + dbusConnMock := mock_dbus.NewMockConnection(mockctrl) + m := &Manager{ + dbusToolkit: dbusMock, + } + It("return error if cannot create new D-Bus connection", func() { + dbusMock.EXPECT().NewConn(gomock.Any()).Return(nil, errors.New("error creating new connection")) + err := m.ReloadFRR() + Expect(err).To(HaveOccurred()) + }) + It("return error if cannot reload FRR unit", func() { + dbusMock.EXPECT().NewConn(gomock.Any()).Return(dbusConnMock, nil) + dbusConnMock.EXPECT().ReloadUnitContext(gomock.Any(), frrUnit, "fail", gomock.Any()).Return(-1, errors.New("error reloading context")) + dbusConnMock.EXPECT().Close() + err := m.ReloadFRR() + Expect(err).To(HaveOccurred()) + }) + // It("return no error", func() { + // dbusMock.EXPECT().NewConn(gomock.Any()).Return(dbusConnMock, nil) + // dbusConnMock.EXPECT().ReloadUnitContext(gomock.Any(), frrUnit, "fail", gomock.Any()).Return(0, nil) + // dbusConnMock.EXPECT().Close() + // err := m.ReloadFRR() + // Expect(err).ToNot(HaveOccurred()) + // }) + }) + Context("Configure() should", func() { + nlMock := mock_nl.NewMockToolkitInterface(mockctrl) + It("return error if cannot get underlay IP", func() { + m := &Manager{} + nlMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("error listing addresses")) + _, err := m.Configure(Configuration{}, nl.NewManager(nlMock)) + Expect(err).To(HaveOccurred()) + }) + It("return error if cannot node's name", func() { + oldEnv, isSet := os.LookupEnv(healthcheck.NodenameEnv) + if isSet { + err := os.Unsetenv(healthcheck.NodenameEnv) + Expect(err).ToNot(HaveOccurred()) + } + + m := &Manager{} + nlMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{ + {IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}, + }, nil) + _, err := m.Configure(Configuration{}, nl.NewManager(nlMock)) + Expect(err).To(HaveOccurred()) + + if isSet { + err := os.Setenv(healthcheck.NodenameEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) + It("return error if cannot read config file", func() { + oldEnv, isSet := os.LookupEnv(healthcheck.NodenameEnv) + err := os.Setenv(healthcheck.NodenameEnv, "test") + Expect(err).ToNot(HaveOccurred()) + + m := &Manager{} + nlMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{ + {IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}, + }, nil) + _, err = m.Configure(Configuration{}, nl.NewManager(nlMock)) + Expect(err).To(HaveOccurred()) + + if isSet { + err := os.Setenv(healthcheck.NodenameEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } else { + err := os.Unsetenv(healthcheck.NodenameEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) + It("return error if cannot render target config", func() { + oldEnv, isSet := os.LookupEnv(healthcheck.NodenameEnv) + err := os.Setenv(healthcheck.NodenameEnv, "test") + Expect(err).ToNot(HaveOccurred()) + + m := &Manager{ + ConfigPath: tmpDir + "/frr.conf", + configTemplate: &template.Template{}, + } + + nlMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{ + {IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}, + }, nil) + _, err = m.Configure(Configuration{}, nl.NewManager(nlMock)) + Expect(err).To(HaveOccurred()) + + if isSet { + err := os.Setenv(healthcheck.NodenameEnv, oldEnv) + Expect(err).ToNot(HaveOccurred()) + } else { + err := os.Unsetenv(healthcheck.NodenameEnv) + Expect(err).ToNot(HaveOccurred()) + } + }) + }) +}) diff --git a/pkg/frr/manager.go b/pkg/frr/manager.go index 6a1e7fc9..1de9dfba 100644 --- a/pkg/frr/manager.go +++ b/pkg/frr/manager.go @@ -9,8 +9,8 @@ import ( "os" "text/template" - "github.com/coreos/go-systemd/v22/dbus" "github.com/telekom/das-schiff-network-operator/pkg/config" + "github.com/telekom/das-schiff-network-operator/pkg/frr/dbus" ) const defaultPermissions = 0o640 @@ -25,6 +25,7 @@ type Manager struct { ConfigPath string TemplatePath string Cli *Cli + dbusToolkit dbus.System } type PrefixList struct { @@ -64,6 +65,7 @@ func NewFRRManager() *Manager { ConfigPath: "/etc/frr/frr.conf", TemplatePath: "/etc/frr/frr.conf.tpl", Cli: NewCli(), + dbusToolkit: &dbus.Toolkit{}, } } @@ -87,10 +89,10 @@ func (m *Manager) Init() error { return nil } -func (*Manager) ReloadFRR() error { - con, err := dbus.NewSystemConnectionContext(context.Background()) +func (m *Manager) ReloadFRR() error { + con, err := m.dbusToolkit.NewConn(context.Background()) if err != nil { - return fmt.Errorf("error creating nee D-Bus connection: %w", err) + return fmt.Errorf("error creating new D-Bus connection: %w", err) } defer con.Close() @@ -105,8 +107,8 @@ func (*Manager) ReloadFRR() error { return nil } -func (*Manager) RestartFRR() error { - con, err := dbus.NewSystemConnectionContext(context.Background()) +func (m *Manager) RestartFRR() error { + con, err := m.dbusToolkit.NewConn(context.Background()) if err != nil { return fmt.Errorf("error creating nee D-Bus connection: %w", err) } @@ -123,8 +125,8 @@ func (*Manager) RestartFRR() error { return nil } -func (*Manager) GetStatusFRR() (activeState, subState string, err error) { - con, err := dbus.NewSystemConnectionContext(context.Background()) +func (m *Manager) GetStatusFRR() (activeState, subState string, err error) { + con, err := m.dbusToolkit.NewConn(context.Background()) if err != nil { return "", "", fmt.Errorf("error creating D-Bus connection: %w", err) } diff --git a/pkg/healthcheck/healthcheck_test.go b/pkg/healthcheck/healthcheck_test.go index 4326397f..2ef639be 100644 --- a/pkg/healthcheck/healthcheck_test.go +++ b/pkg/healthcheck/healthcheck_test.go @@ -47,220 +47,218 @@ func TestHealthCheck(t *testing.T) { "HealthCheck Suite") } -var _ = Describe("HealthCheck", func() { - Context("LoadConfig() should", func() { - It("return error if config is mandatory but does not exist", func() { - oldValue := os.Getenv(configEnv) - Expect(os.Setenv(configEnv, "/some/invalid/path")).To(Succeed()) - _, err := LoadConfig("") - Expect(err).To(HaveOccurred()) - Expect(os.Setenv(configEnv, oldValue)).To(Succeed()) - }) - It("return error if config is invalid", func() { - _, err := LoadConfig("./testdata/invalidconfig.yaml") - Expect(err).To(HaveOccurred()) - }) - It("return no error if valid config exists", func() { - _, err := LoadConfig("./testdata/simpleconfig.yaml") - Expect(err).ToNot(HaveOccurred()) - }) - It("return no error if config does not exist but is not mandatory", func() { - conf, err := LoadConfig("/some/invalid/path") - Expect(err).ToNot(HaveOccurred()) - Expect(conf).ToNot(BeNil()) - }) +var _ = Describe("LoadConfig()", func() { + It("returns error if config is mandatory but does not exist", func() { + oldValue := os.Getenv(configEnv) + Expect(os.Setenv(configEnv, "/some/invalid/path")).To(Succeed()) + _, err := LoadConfig("") + Expect(err).To(HaveOccurred()) + Expect(os.Setenv(configEnv, oldValue)).To(Succeed()) }) - Context("IsFRRActive() should", func() { - frm := mock_healthcheck.NewMockFRRInterface(ctrl) - It("return error if FRR Manager returns error", func() { - c := fake.NewClientBuilder().Build() - frm.EXPECT().GetStatusFRR().Return("", "", errors.New("fake error")) - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - result, err := hc.IsFRRActive() - Expect(err).To(HaveOccurred()) - Expect(result).To(BeFalse()) - }) - It("return error if FRR is inactive", func() { - c := fake.NewClientBuilder().Build() - frm.EXPECT().GetStatusFRR().Return("inactive", "stopped", nil) - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - result, err := hc.IsFRRActive() - Expect(err).To(HaveOccurred()) - Expect(result).To(BeFalse()) - }) - It("return no error if FRR is active", func() { - c := fake.NewClientBuilder().Build() - frm.EXPECT().GetStatusFRR().Return("active", "running", nil) - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - result, err := hc.IsFRRActive() - Expect(err).ToNot(HaveOccurred()) - Expect(result).To(BeTrue()) - }) + It("returns error if config is invalid", func() { + _, err := LoadConfig("./testdata/invalidconfig.yaml") + Expect(err).To(HaveOccurred()) }) - Context("RemoveTaints() should", func() { - It("return error about no nodes", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, nil, nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.RemoveTaints(context.Background()) - Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - }) - It("return error when trying to remove taint (update node)", func() { - c := &updateErrorClient{} - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, nil, nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.RemoveTaints(context.Background()) - Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - }) - It("remove taint and set isInitialized true", func() { - c := fake.NewClientBuilder().WithRuntimeObjects(fakeNodes).Build() - nc := &NetHealthcheckConfig{} - hc, err := NewHealthChecker(c, nil, nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.RemoveTaints(context.Background()) - Expect(err).ToNot(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeTrue()) - }) + It("returns no error if valid config exists", func() { + _, err := LoadConfig("./testdata/simpleconfig.yaml") + Expect(err).ToNot(HaveOccurred()) }) - Context("CheckInterfaces() should", func() { - It("return error if interface is not present", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeErrorGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckInterfaces() - Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - }) - It("return error if interface is not up", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeDownGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckInterfaces() - Expect(err).To(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - }) - It("return error if all links are up", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckInterfaces() - Expect(err).ToNot(HaveOccurred()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - }) + It("returns no error if config does not exist but is not mandatory", func() { + conf, err := LoadConfig("/some/invalid/path") + Expect(err).ToNot(HaveOccurred()) + Expect(conf).ToNot(BeNil()) }) - Context("NewTcpDialer() should", func() { - It("should use dialer with 3s timeout", func() { - c := fake.NewClientBuilder().Build() - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), &NetHealthcheckConfig{}) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - d := hc.toolkit.tcpDialer.(*net.Dialer) - Expect(d.Timeout).To(Equal(time.Second * 3)) - }) - It("should use dialer with 5s timeout", func() { - c := fake.NewClientBuilder().Build() - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("5")), &NetHealthcheckConfig{}) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - d := hc.toolkit.tcpDialer.(*net.Dialer) - Expect(d.Timeout).To(Equal(time.Second * 5)) - }) - It("should use dialer with 500ms timeout", func() { - c := fake.NewClientBuilder().Build() - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("500ms")), &NetHealthcheckConfig{}) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - d := hc.toolkit.tcpDialer.(*net.Dialer) - Expect(d.Timeout).To(Equal(time.Millisecond * 500)) - }) +}) +var _ = Describe("IsFRRActive()", func() { + frm := mock_healthcheck.NewMockFRRInterface(ctrl) + It("returns error if FRR Manager returns error", func() { + c := fake.NewClientBuilder().Build() + frm.EXPECT().GetStatusFRR().Return("", "", errors.New("fake error")) + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + result, err := hc.IsFRRActive() + Expect(err).To(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + It("returns error if FRR is inactive", func() { + c := fake.NewClientBuilder().Build() + frm.EXPECT().GetStatusFRR().Return("inactive", "stopped", nil) + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + result, err := hc.IsFRRActive() + Expect(err).To(HaveOccurred()) + Expect(result).To(BeFalse()) }) - Context("CheckReachability() should", func() { - dialerMock := mock_healthcheck.NewMockTCPDialerInterface(ctrl) + It("returns no error if FRR is active", func() { + c := fake.NewClientBuilder().Build() + frm.EXPECT().GetStatusFRR().Return("active", "running", nil) + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, NewDefaultHealthcheckToolkit(frm, NewTCPDialer("")), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + result, err := hc.IsFRRActive() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + }) +}) +var _ = Describe("RemoveTaints()", func() { + It("returns error about no nodes", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, nil, nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.RemoveTaints(context.Background()) + Expect(err).To(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + }) + It("returns error when trying to remove taint (update node)", func() { + c := &updateErrorClient{} + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, nil, nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.RemoveTaints(context.Background()) + Expect(err).To(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + }) + It("removes taint and set isInitialized true", func() { + c := fake.NewClientBuilder().WithRuntimeObjects(fakeNodes).Build() + nc := &NetHealthcheckConfig{} + hc, err := NewHealthChecker(c, nil, nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.RemoveTaints(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeTrue()) + }) +}) +var _ = Describe("CheckInterfaces()", func() { + It("returns error if interface is not present", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeErrorGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckInterfaces() + Expect(err).To(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + }) + It("returns error if interface is not up", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeDownGetByName, &net.Dialer{Timeout: time.Duration(3)}), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckInterfaces() + Expect(err).To(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + }) + It("returns error if all links are up", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{Interfaces: []string{"A", "B"}} + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckInterfaces() + Expect(err).ToNot(HaveOccurred()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + }) +}) +var _ = Describe("NewTcpDialer()", func() { + It("should use dialer with 3s timeout", func() { + c := fake.NewClientBuilder().Build() + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("")), &NetHealthcheckConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + d := hc.toolkit.tcpDialer.(*net.Dialer) + Expect(d.Timeout).To(Equal(time.Second * 3)) + }) + It("should use dialer with 5s timeout", func() { + c := fake.NewClientBuilder().Build() + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("5")), &NetHealthcheckConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + d := hc.toolkit.tcpDialer.(*net.Dialer) + Expect(d.Timeout).To(Equal(time.Second * 5)) + }) + It("should use dialer with 500ms timeout", func() { + c := fake.NewClientBuilder().Build() + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, NewTCPDialer("500ms")), &NetHealthcheckConfig{}) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + d := hc.toolkit.tcpDialer.(*net.Dialer) + Expect(d.Timeout).To(Equal(time.Millisecond * 500)) + }) +}) +var _ = Describe("CheckReachability()", func() { + dialerMock := mock_healthcheck.NewMockTCPDialerInterface(ctrl) - It("should return error if cannot reach host", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{ - Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, - } - dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(nil, errors.New("fake error")).Times(defaultRetries) - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckReachability() - Expect(err).To(HaveOccurred()) - }) - It("should return no error if can reach host", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{ - Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, - } - dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(&fakeConn{}, nil).Times(1) - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckReachability() - Expect(err).ToNot(HaveOccurred()) - }) - It("should return no error if can reach host but connection was refused", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{ - Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, - } - dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(nil, errors.New("connect: connection refused")).Times(1) - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckReachability() - Expect(err).ToNot(HaveOccurred()) - }) - It("should return error if cannot close connection - with just 1 try", func() { - c := fake.NewClientBuilder().Build() - nc := &NetHealthcheckConfig{ - Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, - Retries: 1, - } - dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(&fakeConnCloseError{}, nil).Times(1) - hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) - Expect(err).ToNot(HaveOccurred()) - Expect(hc).ToNot(BeNil()) - Expect(hc.IsNetworkingHealthy()).To(BeFalse()) - err = hc.CheckReachability() - Expect(err).To(HaveOccurred()) - }) + It("should return error if cannot reach host", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{ + Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, + } + dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(nil, errors.New("fake error")).Times(defaultRetries) + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckReachability() + Expect(err).To(HaveOccurred()) + }) + It("should return no error if can reach host", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{ + Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, + } + dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(&fakeConn{}, nil).Times(1) + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckReachability() + Expect(err).ToNot(HaveOccurred()) + }) + It("should return no error if can reach host but connection was refused", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{ + Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, + } + dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(nil, errors.New("connect: connection refused")).Times(1) + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckReachability() + Expect(err).ToNot(HaveOccurred()) + }) + It("should return error if cannot close connection - with just 1 try", func() { + c := fake.NewClientBuilder().Build() + nc := &NetHealthcheckConfig{ + Reachability: []netReachabilityItem{{Host: "someHost", Port: 42}}, + Retries: 1, + } + dialerMock.EXPECT().Dial("tcp", "someHost:42").Return(&fakeConnCloseError{}, nil).Times(1) + hc, err := NewHealthChecker(c, NewHealthCheckToolkit(nil, fakeUpGetByName, dialerMock), nc) + Expect(err).ToNot(HaveOccurred()) + Expect(hc).ToNot(BeNil()) + Expect(hc.IsNetworkingHealthy()).To(BeFalse()) + err = hc.CheckReachability() + Expect(err).To(HaveOccurred()) }) }) diff --git a/pkg/managerconfig/managerconfig_test.go b/pkg/managerconfig/managerconfig_test.go index 6311e8fa..1ca53f82 100644 --- a/pkg/managerconfig/managerconfig_test.go +++ b/pkg/managerconfig/managerconfig_test.go @@ -18,19 +18,17 @@ func TestHealthCheck(t *testing.T) { "HealthCheck Suite") } -var _ = Describe("managerconfig", func() { - Context("Load() should", func() { - It("return error if cannot open file", func() { - _, err := Load("", runtime.NewScheme()) - Expect(err).To(HaveOccurred()) - }) - It("return error if config is invalid", func() { - _, err := Load("testdata/invalid_config.yaml", runtime.NewScheme()) - Expect(err).To(HaveOccurred()) - }) - It("return no error", func() { - _, err := Load("testdata/config.yaml", runtime.NewScheme()) - Expect(err).ToNot(HaveOccurred()) - }) +var _ = Describe("Load()", func() { + It("returns error if cannot open file", func() { + _, err := Load("", runtime.NewScheme()) + Expect(err).To(HaveOccurred()) + }) + It("returns error if config is invalid", func() { + _, err := Load("testdata/invalid_config.yaml", runtime.NewScheme()) + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + _, err := Load("testdata/config.yaml", runtime.NewScheme()) + Expect(err).ToNot(HaveOccurred()) }) }) diff --git a/pkg/monitoring/nl.go b/pkg/monitoring/nl.go index cb39c602..fdb5d806 100644 --- a/pkg/monitoring/nl.go +++ b/pkg/monitoring/nl.go @@ -16,7 +16,7 @@ type netlinkCollector struct { basicCollector routesFibDesc typedFactoryDesc neighborsDesc typedFactoryDesc - netlink *nl.NetlinkManager + netlink *nl.Manager } func init() { @@ -44,7 +44,7 @@ func NewNetlinkCollector() (Collector, error) { ), valueType: prometheus.GaugeValue, }, - netlink: &nl.NetlinkManager{}, + netlink: &nl.Manager{}, } collector.name = nlCollectorName collector.logger = ctrl.Log.WithName("netlink.collector") diff --git a/pkg/nl/create.go b/pkg/nl/create.go index 42f40dbd..f427d6c2 100644 --- a/pkg/nl/create.go +++ b/pkg/nl/create.go @@ -15,7 +15,7 @@ const ( hwAddrByteSize = 6 ) -func (n *NetlinkManager) createVRF(vrfName string, table int) (*netlink.Vrf, error) { +func (n *Manager) createVRF(vrfName string, table int) (*netlink.Vrf, error) { netlinkVrf := netlink.Vrf{ LinkAttrs: netlink.LinkAttrs{ Name: vrfName, @@ -23,20 +23,20 @@ func (n *NetlinkManager) createVRF(vrfName string, table int) (*netlink.Vrf, err Table: uint32(table), } - if err := netlink.LinkAdd(&netlinkVrf); err != nil { + if err := n.toolkit.LinkAdd(&netlinkVrf); err != nil { return nil, fmt.Errorf("error adding link: %w", err) } if err := n.disableEUIAutogeneration(vrfName); err != nil { return nil, err } - if err := netlink.LinkSetUp(&netlinkVrf); err != nil { + if err := n.toolkit.LinkSetUp(&netlinkVrf); err != nil { return nil, fmt.Errorf("error setting link up: %w", err) } return &netlinkVrf, nil } -func (n *NetlinkManager) createBridge(bridgeName string, macAddress *net.HardwareAddr, masterIdx, mtu int, underlayRMAC bool) (*netlink.Bridge, error) { +func (n *Manager) createBridge(bridgeName string, macAddress *net.HardwareAddr, masterIdx, mtu int, underlayRMAC bool) (*netlink.Bridge, error) { netlinkBridge := netlink.Bridge{ LinkAttrs: netlink.LinkAttrs{ Name: bridgeName, @@ -49,7 +49,7 @@ func (n *NetlinkManager) createBridge(bridgeName string, macAddress *net.Hardwar if macAddress != nil { netlinkBridge.LinkAttrs.HardwareAddr = *macAddress } else if underlayRMAC { - _, vxlanIP, err := getUnderlayInterfaceAndIP() + _, vxlanIP, err := n.getUnderlayInterfaceAndIP() if err != nil { return nil, err } @@ -61,7 +61,7 @@ func (n *NetlinkManager) createBridge(bridgeName string, macAddress *net.Hardwar netlinkBridge.LinkAttrs.HardwareAddr = generatedMac } - if err := netlink.LinkAdd(&netlinkBridge); err != nil { + if err := n.toolkit.LinkAdd(&netlinkBridge); err != nil { return nil, fmt.Errorf("error adding link: %w", err) } if err := n.disableEUIAutogeneration(bridgeName); err != nil { @@ -71,8 +71,8 @@ func (n *NetlinkManager) createBridge(bridgeName string, macAddress *net.Hardwar return &netlinkBridge, nil } -func (n *NetlinkManager) createVXLAN(vxlanName string, bridgeIdx, vni, mtu int, hairpin, neighSuppression bool) (*netlink.Vxlan, error) { - vxlanIf, vxlanIP, err := getUnderlayInterfaceAndIP() +func (n *Manager) createVXLAN(vxlanName string, bridgeIdx, vni, mtu int, hairpin, neighSuppression bool) (*netlink.Vxlan, error) { + vxlanIf, vxlanIP, err := n.getUnderlayInterfaceAndIP() if err != nil { return nil, err } @@ -95,17 +95,17 @@ func (n *NetlinkManager) createVXLAN(vxlanName string, bridgeIdx, vni, mtu int, Learning: false, Port: vxlanPort, } - if err := netlink.LinkAdd(&netlinkVXLAN); err != nil { + if err := n.toolkit.LinkAdd(&netlinkVXLAN); err != nil { return nil, fmt.Errorf("error adding link: %w", err) } - if err := netlink.LinkSetLearning(&netlinkVXLAN, false); err != nil { + if err := n.toolkit.LinkSetLearning(&netlinkVXLAN, false); err != nil { return nil, fmt.Errorf("error disabling link learning: %w", err) } - if err := setNeighSuppression(&netlinkVXLAN, neighSuppression); err != nil { + if err := n.setNeighSuppression(&netlinkVXLAN, neighSuppression); err != nil { return nil, err } if hairpin { - if err := netlink.LinkSetHairpin(&netlinkVXLAN, true); err != nil { + if err := n.toolkit.LinkSetHairpin(&netlinkVXLAN, true); err != nil { return nil, fmt.Errorf("error setting link's hairpin mode: %w", err) } } @@ -116,8 +116,8 @@ func (n *NetlinkManager) createVXLAN(vxlanName string, bridgeIdx, vni, mtu int, return &netlinkVXLAN, nil } -func (*NetlinkManager) disableEUIAutogeneration(intfName string) error { - fileName := fmt.Sprintf("/proc/sys/net/ipv6/conf/%s/addr_gen_mode", intfName) +func (*Manager) disableEUIAutogeneration(intfName string) error { + fileName := fmt.Sprintf("%s/ipv6/conf/%s/addr_gen_mode", procSysNetPath, intfName) file, err := os.OpenFile(fileName, os.O_WRONLY, 0) if err != nil { return fmt.Errorf("error opening file: %w", err) @@ -129,7 +129,7 @@ func (*NetlinkManager) disableEUIAutogeneration(intfName string) error { return nil } -func (n *NetlinkManager) createLink(vethName, peerName string, masterIdx, mtu int, generateEUI bool) (*netlink.Veth, error) { +func (n *Manager) createLink(vethName, peerName string, masterIdx, mtu int, generateEUI bool) (*netlink.Veth, error) { netlinkVeth := netlink.Veth{ LinkAttrs: netlink.LinkAttrs{ Name: vethName, @@ -138,7 +138,7 @@ func (n *NetlinkManager) createLink(vethName, peerName string, masterIdx, mtu in }, PeerName: peerName, } - if err := netlink.LinkAdd(&netlinkVeth); err != nil { + if err := n.toolkit.LinkAdd(&netlinkVeth); err != nil { return nil, fmt.Errorf("error adding link: %w", err) } @@ -154,19 +154,19 @@ func (n *NetlinkManager) createLink(vethName, peerName string, masterIdx, mtu in return &netlinkVeth, nil } -func (*NetlinkManager) setUp(intfName string) error { - link, err := netlink.LinkByName(intfName) +func (n *Manager) setUp(intfName string) error { + link, err := n.toolkit.LinkByName(intfName) if err != nil { return fmt.Errorf("error getting link by name: %w", err) } - if err := netlink.LinkSetUp(link); err != nil { + if err := n.toolkit.LinkSetUp(link); err != nil { return fmt.Errorf("error setting link up: %w", err) } return nil } -func generateUnderlayMAC() (net.HardwareAddr, error) { - _, vxlanIP, err := getUnderlayInterfaceAndIP() +func (n *Manager) generateUnderlayMAC() (net.HardwareAddr, error) { + _, vxlanIP, err := n.getUnderlayInterfaceAndIP() if err != nil { return nil, err } @@ -178,10 +178,10 @@ func generateUnderlayMAC() (net.HardwareAddr, error) { return generatedMac, nil } -func getUnderlayInterfaceAndIP() (int, net.IP, error) { +func (n *Manager) getUnderlayInterfaceAndIP() (int, net.IP, error) { dummy := netlink.Dummy{LinkAttrs: netlink.LinkAttrs{Name: underlayLoopback}} - addresses, err := netlink.AddrList(&dummy, netlink.FAMILY_V4) + addresses, err := n.toolkit.AddrList(&dummy, netlink.FAMILY_V4) if err != nil { return -1, nil, fmt.Errorf("error listing link's addresses: %w", err) } @@ -202,7 +202,7 @@ func generateMAC(ip net.IP) (net.HardwareAddr, error) { return hwaddr, nil } -func setNeighSuppression(link netlink.Link, mode bool) error { +func (n *Manager) setNeighSuppression(link netlink.Link, mode bool) error { req := nl.NewNetlinkRequest(unix.RTM_SETLINK, unix.NLM_F_ACK) msg := nl.NewIfInfomsg(unix.AF_BRIDGE) @@ -212,7 +212,7 @@ func setNeighSuppression(link netlink.Link, mode bool) error { br := nl.NewRtAttr(unix.IFLA_PROTINFO|unix.NLA_F_NESTED, nil) br.AddRtAttr(iflaBrPortNeighSuppress, boolToByte(mode)) req.AddData(br) - _, err := req.Execute(unix.NETLINK_ROUTE, 0) + _, err := n.toolkit.ExecuteNetlinkRequest(req, unix.NETLINK_ROUTE, 0) if err != nil { return fmt.Errorf("error executing request: %w", err) } diff --git a/pkg/nl/delete.go b/pkg/nl/delete.go index 027368ba..3a6fa542 100644 --- a/pkg/nl/delete.go +++ b/pkg/nl/delete.go @@ -6,8 +6,8 @@ import ( "github.com/vishvananda/netlink" ) -func (*NetlinkManager) deleteLink(name string) error { - if err := netlink.LinkDel(&netlink.GenericLink{LinkAttrs: netlink.LinkAttrs{Name: name}}); err != nil { +func (n *Manager) deleteLink(name string) error { + if err := n.toolkit.LinkDel(&netlink.GenericLink{LinkAttrs: netlink.LinkAttrs{Name: name}}); err != nil { return fmt.Errorf("error while deleting link: %w", err) } return nil diff --git a/pkg/nl/interface.go b/pkg/nl/interface.go new file mode 100644 index 00000000..149a4c36 --- /dev/null +++ b/pkg/nl/interface.go @@ -0,0 +1,145 @@ +//nolint:wrapcheck +package nl + +import ( + "net" + + "github.com/vishvananda/netlink" + "github.com/vishvananda/netlink/nl" +) + +//go:generate mockgen -destination ./mock/mock_nl.go . ToolkitInterface +type ToolkitInterface interface { + LinkByIndex(index int) (netlink.Link, error) + LinkByName(name string) (netlink.Link, error) + LinkList() ([]netlink.Link, error) + NeighList(linkIndex int, family int) ([]netlink.Neigh, error) + NewIPNet(ip net.IP) *net.IPNet + RouteListFiltered(family int, filter *netlink.Route, filterMask uint64) ([]netlink.Route, error) + RouteDel(route *netlink.Route) error + RouteAdd(route *netlink.Route) error + AddrList(link netlink.Link, family int) ([]netlink.Addr, error) + VethPeerIndex(link *netlink.Veth) (int, error) + ParseAddr(s string) (*netlink.Addr, error) + LinkDel(link netlink.Link) error + LinkSetUp(link netlink.Link) error + LinkAdd(link netlink.Link) error + AddrAdd(link netlink.Link, addr *netlink.Addr) error + AddrDel(link netlink.Link, addr *netlink.Addr) error + LinkSetLearning(link netlink.Link, mode bool) error + LinkSetHairpin(link netlink.Link, mode bool) error + ExecuteNetlinkRequest(req *nl.NetlinkRequest, sockType int, resType uint16) ([][]byte, error) + LinkSetMTU(link netlink.Link, mtu int) error + LinkSetDown(link netlink.Link) error + LinkSetHardwareAddr(link netlink.Link, hwaddr net.HardwareAddr) error + LinkSetMasterByIndex(link netlink.Link, masterIndex int) error + LinkSetNoMaster(link netlink.Link) error + LinkGetProtinfo(link netlink.Link) (netlink.Protinfo, error) + LinkSetMaster(link netlink.Link, master netlink.Link) error +} + +type Toolkit struct{} + +func (*Toolkit) LinkByIndex(index int) (netlink.Link, error) { + return netlink.LinkByIndex(index) +} + +func (*Toolkit) LinkByName(name string) (netlink.Link, error) { + return netlink.LinkByName(name) +} + +func (*Toolkit) LinkList() ([]netlink.Link, error) { + return netlink.LinkList() +} + +func (*Toolkit) NeighList(linkIndex, family int) ([]netlink.Neigh, error) { + return netlink.NeighList(linkIndex, family) +} + +func (*Toolkit) NewIPNet(ip net.IP) *net.IPNet { + return netlink.NewIPNet(ip) +} + +func (*Toolkit) RouteListFiltered(family int, filter *netlink.Route, filterMask uint64) ([]netlink.Route, error) { + return netlink.RouteListFiltered(family, filter, filterMask) +} + +func (*Toolkit) RouteDel(route *netlink.Route) error { + return netlink.RouteDel(route) +} + +func (*Toolkit) RouteAdd(route *netlink.Route) error { + return netlink.RouteAdd(route) +} + +func (*Toolkit) AddrList(link netlink.Link, family int) ([]netlink.Addr, error) { + return netlink.AddrList(link, family) +} + +func (*Toolkit) VethPeerIndex(link *netlink.Veth) (int, error) { + return netlink.VethPeerIndex(link) +} + +func (*Toolkit) ParseAddr(s string) (*netlink.Addr, error) { + return netlink.ParseAddr(s) +} + +func (*Toolkit) LinkDel(link netlink.Link) error { + return netlink.LinkDel(link) +} + +func (*Toolkit) LinkSetUp(link netlink.Link) error { + return netlink.LinkSetUp(link) +} + +func (*Toolkit) LinkAdd(link netlink.Link) error { + return netlink.LinkAdd(link) +} + +func (*Toolkit) AddrAdd(link netlink.Link, addr *netlink.Addr) error { + return netlink.AddrAdd(link, addr) +} + +func (*Toolkit) LinkSetLearning(link netlink.Link, mode bool) error { + return netlink.LinkSetLearning(link, mode) +} + +func (*Toolkit) LinkSetHairpin(link netlink.Link, mode bool) error { + return netlink.LinkSetHairpin(link, mode) +} + +func (*Toolkit) ExecuteNetlinkRequest(req *nl.NetlinkRequest, sockType int, resType uint16) ([][]byte, error) { + return req.Execute(sockType, resType) +} + +func (*Toolkit) LinkSetMTU(link netlink.Link, mtu int) error { + return netlink.LinkSetMTU(link, mtu) +} + +func (*Toolkit) LinkSetDown(link netlink.Link) error { + return netlink.LinkSetDown(link) +} + +func (*Toolkit) LinkSetHardwareAddr(link netlink.Link, hwaddr net.HardwareAddr) error { + return netlink.LinkSetHardwareAddr(link, hwaddr) +} + +func (*Toolkit) LinkSetMasterByIndex(link netlink.Link, masterIndex int) error { + return netlink.LinkSetMasterByIndex(link, masterIndex) +} + +func (*Toolkit) LinkSetNoMaster(link netlink.Link) error { + return netlink.LinkSetNoMaster(link) +} + +func (*Toolkit) LinkGetProtinfo(link netlink.Link) (netlink.Protinfo, error) { + return netlink.LinkGetProtinfo(link) +} + +func (*Toolkit) LinkSetMaster(link, master netlink.Link) error { + return netlink.LinkSetMaster(link, master) +} + +func (*Toolkit) AddrDel(link netlink.Link, addr *netlink.Addr) error { + return netlink.AddrDel(link, addr) +} diff --git a/pkg/nl/layer2.go b/pkg/nl/layer2.go index 717a827e..c149bda9 100644 --- a/pkg/nl/layer2.go +++ b/pkg/nl/layer2.go @@ -18,6 +18,8 @@ const ( neighFilePermissions = 0o600 ) +var procSysNetPath = "/proc/sys/net" + type Layer2Information struct { VlanID int MTU int @@ -93,10 +95,10 @@ func getFlags(flag int) (string, error) { } } -func (*NetlinkManager) ParseIPAddresses(addresses []string) ([]*netlink.Addr, error) { +func (n *Manager) ParseIPAddresses(addresses []string) ([]*netlink.Addr, error) { addrs := []*netlink.Addr{} for _, ip := range addresses { - addr, err := netlink.ParseAddr(ip) + addr, err := n.toolkit.ParseAddr(ip) if err != nil { return nil, fmt.Errorf("error while parsing IP address: %w", err) } @@ -105,7 +107,7 @@ func (*NetlinkManager) ParseIPAddresses(addresses []string) ([]*netlink.Addr, er return addrs, nil } -func (n *NetlinkManager) CreateL2(info *Layer2Information) error { +func (n *Manager) CreateL2(info *Layer2Information) error { masterIdx := -1 if info.VRF != "" { l3Info, err := n.GetL3ByName(info.VRF) @@ -127,7 +129,7 @@ func (n *NetlinkManager) CreateL2(info *Layer2Information) error { return n.setupVXLAN(info, bridge) } -func (n *NetlinkManager) setupBridge(info *Layer2Information, masterIdx int) (*netlink.Bridge, error) { +func (n *Manager) setupBridge(info *Layer2Information, masterIdx int) (*netlink.Bridge, error) { bridge, err := n.createBridge(fmt.Sprintf("%s%d", layer2Prefix, info.VlanID), info.AnycastMAC, masterIdx, info.MTU, false) if err != nil { return nil, err @@ -143,7 +145,7 @@ func (n *NetlinkManager) setupBridge(info *Layer2Information, masterIdx int) (*n // Wait 500ms before configuring anycast gateways on newly added interface time.Sleep(interfaceConfigTimeout) for _, addr := range info.AnycastGateways { - err = netlink.AddrAdd(bridge, addr) + err = n.toolkit.AddrAdd(bridge, addr) if err != nil { return nil, fmt.Errorf("error while adding address: %w", err) } @@ -152,7 +154,7 @@ func (n *NetlinkManager) setupBridge(info *Layer2Information, masterIdx int) (*n return bridge, nil } -func (n *NetlinkManager) setupVXLAN(info *Layer2Information, bridge *netlink.Bridge) error { +func (n *Manager) setupVXLAN(info *Layer2Information, bridge *netlink.Bridge) error { neighSuppression := os.Getenv("NWOP_NEIGH_SUPPRESSION") == "true" if len(info.AnycastGateways) == 0 { neighSuppression = false @@ -198,20 +200,20 @@ func (n *NetlinkManager) setupVXLAN(info *Layer2Information, bridge *netlink.Bri return nil } -func (*NetlinkManager) CleanupL2(info *Layer2Information) []error { +func (n *Manager) CleanupL2(info *Layer2Information) []error { errors := []error{} if info.vxlan != nil { - if err := netlink.LinkDel(info.vxlan); err != nil { + if err := n.toolkit.LinkDel(info.vxlan); err != nil { errors = append(errors, err) } } if info.bridge != nil { - if err := netlink.LinkDel(info.bridge); err != nil { + if err := n.toolkit.LinkDel(info.bridge); err != nil { errors = append(errors, err) } } if info.CreateMACVLANInterface && info.macvlanBridge != nil { - if err := netlink.LinkDel(info.macvlanBridge); err != nil { + if err := n.toolkit.LinkDel(info.macvlanBridge); err != nil { errors = append(errors, err) } } @@ -227,17 +229,17 @@ func containsNetlinkAddress(list []*netlink.Addr, addr *netlink.Addr) bool { return false } -func (*NetlinkManager) reconcileIPAddresses(intf netlink.Link, current, desired []*netlink.Addr) error { +func (n *Manager) reconcileIPAddresses(intf netlink.Link, current, desired []*netlink.Addr) error { for _, addr := range desired { if !containsNetlinkAddress(current, addr) { - if err := netlink.AddrAdd(intf, addr); err != nil { + if err := n.toolkit.AddrAdd(intf, addr); err != nil { return fmt.Errorf("error adding desired IP address: %w", err) } } } for _, addr := range current { if !containsNetlinkAddress(desired, addr) { - if err := netlink.AddrDel(intf, addr); err != nil { + if err := n.toolkit.AddrDel(intf, addr); err != nil { return fmt.Errorf("error removing IP address: %w", err) } } @@ -245,21 +247,21 @@ func (*NetlinkManager) reconcileIPAddresses(intf netlink.Link, current, desired return nil } -func (n *NetlinkManager) ReconcileL2(current, desired *Layer2Information) error { +func (n *Manager) ReconcileL2(current, desired *Layer2Information) error { if len(desired.AnycastGateways) > 0 && desired.AnycastMAC == nil { return fmt.Errorf("anycastGateways require anycastMAC to be set") } - if err := setMtU(current, desired); err != nil { + if err := n.setMTU(current, desired); err != nil { return err } - vxlanMAC, err := generateUnderlayMAC() + vxlanMAC, err := n.generateUnderlayMAC() if err != nil { return fmt.Errorf("error generating MAC for vxlan device: %w", err) } - if err := setHardwareAddresses(current, desired, vxlanMAC); err != nil { + if err := n.setHardwareAddresses(current, desired, vxlanMAC); err != nil { return err } @@ -270,7 +272,7 @@ func (n *NetlinkManager) ReconcileL2(current, desired *Layer2Information) error } if shouldReattachL2VNI { - if err := reattachL2VNI(current); err != nil { + if err := n.reattachL2VNI(current); err != nil { return err } } @@ -284,7 +286,7 @@ func (n *NetlinkManager) ReconcileL2(current, desired *Layer2Information) error return err } - if err := doNeighSuppression(current, desired); err != nil { + if err := n.doNeighSuppression(current, desired); err != nil { return err } @@ -292,74 +294,74 @@ func (n *NetlinkManager) ReconcileL2(current, desired *Layer2Information) error return n.reconcileIPAddresses(current.bridge, current.AnycastGateways, desired.AnycastGateways) } -func setMtU(current, desired *Layer2Information) error { +func (n *Manager) setMTU(current, desired *Layer2Information) error { // Set MTU - if err := netlink.LinkSetMTU(current.bridge, desired.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(current.bridge, desired.MTU); err != nil { return fmt.Errorf("error setting bridge MTU: %w", err) } - if err := netlink.LinkSetMTU(current.vxlan, desired.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(current.vxlan, desired.MTU); err != nil { return fmt.Errorf("error setting vxlan MTU: %w", err) } if current.CreateMACVLANInterface { - if err := netlink.LinkSetMTU(current.macvlanBridge, desired.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(current.macvlanBridge, desired.MTU); err != nil { return fmt.Errorf("error setting veth bridge side MTU: %w", err) } - if err := netlink.LinkSetMTU(current.macvlanHost, desired.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(current.macvlanHost, desired.MTU); err != nil { return fmt.Errorf("error setting veth macvlan side MTU: %w", err) } } return nil } -func setHardwareAddresses(current, desired *Layer2Information, vxlanMAC net.HardwareAddr) error { +func (n *Manager) setHardwareAddresses(current, desired *Layer2Information, vxlanMAC net.HardwareAddr) error { if desired.AnycastMAC != nil && !bytes.Equal(current.bridge.HardwareAddr, *desired.AnycastMAC) { - if err := netlink.LinkSetDown(current.vxlan); err != nil { + if err := n.toolkit.LinkSetDown(current.vxlan); err != nil { return fmt.Errorf("error downing vxlan before changing MAC: %w", err) } time.Sleep(interfaceConfigTimeout) // Wait for FRR to pickup interface down - if err := netlink.LinkSetHardwareAddr(current.bridge, *desired.AnycastMAC); err != nil { + if err := n.toolkit.LinkSetHardwareAddr(current.bridge, *desired.AnycastMAC); err != nil { return fmt.Errorf("error setting vxlan mac address: %w", err) } time.Sleep(interfaceConfigTimeout) - if err := netlink.LinkSetUp(current.vxlan); err != nil { + if err := n.toolkit.LinkSetUp(current.vxlan); err != nil { return fmt.Errorf("error upping vxlan after changing MAC: %w", err) } } if !bytes.Equal(current.vxlan.HardwareAddr, vxlanMAC) { - if err := netlink.LinkSetHardwareAddr(current.vxlan, vxlanMAC); err != nil { + if err := n.toolkit.LinkSetHardwareAddr(current.vxlan, vxlanMAC); err != nil { return fmt.Errorf("error setting vxlan mac address: %w", err) } } return nil } -func reattachL2VNI(current *Layer2Information) error { +func (n *Manager) reattachL2VNI(current *Layer2Information) error { // First set VXLAN down and detach from L2VNI bridge - if err := netlink.LinkSetDown(current.vxlan); err != nil { + if err := n.toolkit.LinkSetDown(current.vxlan); err != nil { return fmt.Errorf("error downing vxlan before changing MAC: %w", err) } - if err := netlink.LinkSetNoMaster(current.vxlan); err != nil { + if err := n.toolkit.LinkSetNoMaster(current.vxlan); err != nil { return fmt.Errorf("error removing vxlan from bridge before changing MAC: %w", err) } // Reattach VXLAN to L2VNI bridge - if err := netlink.LinkSetMaster(current.vxlan, current.bridge); err != nil { + if err := n.toolkit.LinkSetMaster(current.vxlan, current.bridge); err != nil { return fmt.Errorf("error adding vxlan to bridge after changing MAC: %w", err) } // Disable learning on bridgeport - if err := netlink.LinkSetLearning(current.vxlan, false); err != nil { + if err := n.toolkit.LinkSetLearning(current.vxlan, false); err != nil { return fmt.Errorf("error setting vxlan learning to false: %w", err) } // Up VXLAN interface again - if err := netlink.LinkSetUp(current.vxlan); err != nil { + if err := n.toolkit.LinkSetUp(current.vxlan); err != nil { return fmt.Errorf("error uping vxlan after changing MAC: %w", err) } return nil } -func doNeighSuppression(current, desired *Layer2Information) error { +func (n *Manager) doNeighSuppression(current, desired *Layer2Information) error { neighSuppression := os.Getenv("NWOP_NEIGH_SUPPRESSION") == "true" if len(desired.AnycastGateways) == 0 { neighSuppression = false @@ -367,10 +369,10 @@ func doNeighSuppression(current, desired *Layer2Information) error { if desired.NeighSuppression != nil { neighSuppression = *desired.NeighSuppression } - return setNeighSuppression(current.vxlan, neighSuppression) + return n.setNeighSuppression(current.vxlan, neighSuppression) } -func (n *NetlinkManager) isL2VNIreattachRequired(current, desired *Layer2Information) (bool, error) { +func (n *Manager) isL2VNIreattachRequired(current, desired *Layer2Information) (bool, error) { shouldReattachL2VNI := false // Reconcile VRF if current.VRF != desired.VRF { @@ -380,19 +382,19 @@ func (n *NetlinkManager) isL2VNIreattachRequired(current, desired *Layer2Informa if err != nil { return shouldReattachL2VNI, fmt.Errorf("error while getting L3 by name: %w", err) } - if err := netlink.LinkSetMasterByIndex(current.bridge, l3Info.vrfID); err != nil { + if err := n.toolkit.LinkSetMasterByIndex(current.bridge, l3Info.vrfID); err != nil { return shouldReattachL2VNI, fmt.Errorf("error while setting master by index: %w", err) } } else { - if err := netlink.LinkSetNoMaster(current.bridge); err != nil { + if err := n.toolkit.LinkSetNoMaster(current.bridge); err != nil { return shouldReattachL2VNI, fmt.Errorf("error while trying to link set no master: %w", err) } } } - protinfo, err := netlink.LinkGetProtinfo(current.vxlan) + protinfo, err := n.toolkit.LinkGetProtinfo(current.vxlan) if err != nil { - return shouldReattachL2VNI, fmt.Errorf("error getting bridge port info: %w", err) + return shouldReattachL2VNI, fmt.Errorf("error getting bridge prot info: %w", err) } if protinfo.Learning { shouldReattachL2VNI = true @@ -401,9 +403,9 @@ func (n *NetlinkManager) isL2VNIreattachRequired(current, desired *Layer2Informa return shouldReattachL2VNI, nil } -func (n *NetlinkManager) setupMACVLANinterface(current, desired *Layer2Information) error { +func (n *Manager) setupMACVLANinterface(current, desired *Layer2Information) error { if current.CreateMACVLANInterface && !desired.CreateMACVLANInterface { - if err := netlink.LinkDel(current.macvlanBridge); err != nil { + if err := n.toolkit.LinkDel(current.macvlanBridge); err != nil { return fmt.Errorf("error deleting MACVLAN interface: %w", err) } } else if !current.CreateMACVLANInterface && desired.CreateMACVLANInterface { @@ -421,9 +423,9 @@ func (n *NetlinkManager) setupMACVLANinterface(current, desired *Layer2Informati return nil } -func (*NetlinkManager) configureBridge(intfName string) error { +func (*Manager) configureBridge(intfName string) error { // Ensure bridge can receive gratitious ARP - if err := os.WriteFile(fmt.Sprintf("/proc/sys/net/ipv4/conf/%s/arp_accept", intfName), []byte("1"), neighFilePermissions); err != nil { + if err := os.WriteFile(fmt.Sprintf("%s/ipv4/conf/%s/arp_accept", procSysNetPath, intfName), []byte("1"), neighFilePermissions); err != nil { return fmt.Errorf("error setting arp_accept = 1 for interface: %w", err) } @@ -433,17 +435,17 @@ func (*NetlinkManager) configureBridge(intfName string) error { } // Ensure Ipv4 Neighbor expiry is set to 30min - if err := os.WriteFile(fmt.Sprintf("/proc/sys/net/ipv4/neigh/%s/base_reachable_time_ms", intfName), []byte(baseTimer), neighFilePermissions); err != nil { + if err := os.WriteFile(fmt.Sprintf("%s/ipv4/neigh/%s/base_reachable_time_ms", procSysNetPath, intfName), []byte(baseTimer), neighFilePermissions); err != nil { return fmt.Errorf("error setting ipv4 base_reachable_time_ms = %s for interface: %w", baseTimer, err) } // Ensure IPv6 Neighbor expiry is set to 30min - if err := os.WriteFile(fmt.Sprintf("/proc/sys/net/ipv6/neigh/%s/base_reachable_time_ms", intfName), []byte(baseTimer), neighFilePermissions); err != nil { + if err := os.WriteFile(fmt.Sprintf("%s/ipv6/neigh/%s/base_reachable_time_ms", procSysNetPath, intfName), []byte(baseTimer), neighFilePermissions); err != nil { return fmt.Errorf("error setting ipv6 base_reachable_time_ms = %s for interface: %w", baseTimer, err) } return nil } -func (n *NetlinkManager) ListNeighborInformation() ([]NeighborInformation, error) { +func (n *Manager) ListNeighborInformation() ([]NeighborInformation, error) { netlinkNeighbors, err := n.listNeighbors() if err != nil { return nil, err @@ -500,9 +502,9 @@ func (n *NetlinkManager) ListNeighborInformation() ([]NeighborInformation, error return maps.Values(neighbors), nil } -func (*NetlinkManager) GetBridgeID(info *Layer2Information) (int, error) { +func (n *Manager) GetBridgeID(info *Layer2Information) (int, error) { bridgeName := fmt.Sprintf("%s%d", layer2Prefix, info.VlanID) - link, err := netlink.LinkByName(bridgeName) + link, err := n.toolkit.LinkByName(bridgeName) if err != nil { return -1, fmt.Errorf("error while getting link by name: %w", err) } diff --git a/pkg/nl/layer3.go b/pkg/nl/layer3.go index 8a15031b..9b6fd5fe 100644 --- a/pkg/nl/layer3.go +++ b/pkg/nl/layer3.go @@ -5,7 +5,6 @@ import ( "sort" "github.com/telekom/das-schiff-network-operator/pkg/bpf" - "github.com/vishvananda/netlink" ) const ( @@ -25,7 +24,7 @@ type VRFInformation struct { } // Create will create a VRF and all interfaces necessary to operate the EVPN and leaking. -func (n *NetlinkManager) CreateL3(info VRFInformation) error { +func (n *Manager) CreateL3(info VRFInformation) error { if len(info.Name) > maxVRFnameLen { return fmt.Errorf("name of VRF can not be longer than 12 (15-3 prefix) chars") } @@ -68,7 +67,7 @@ func (n *NetlinkManager) CreateL3(info VRFInformation) error { } // UpL3 will set all interfaces up. This is done after the FRR reload to not have a L2VNI for a short period of time. -func (n *NetlinkManager) UpL3(info VRFInformation) error { +func (n *Manager) UpL3(info VRFInformation) error { if err := n.setUp(bridgePrefix + info.Name); err != nil { return err } @@ -85,7 +84,7 @@ func (n *NetlinkManager) UpL3(info VRFInformation) error { } // Cleanup will try to delete all interfaces associated with this VRF and return a list of errors (for logging) as a slice. -func (n *NetlinkManager) CleanupL3(name string) []error { +func (n *Manager) CleanupL3(name string) []error { errors := []error{} err := n.deleteLink(vxlanPrefix + name) if err != nil { @@ -106,7 +105,7 @@ func (n *NetlinkManager) CleanupL3(name string) []error { return errors } -func (n *NetlinkManager) findFreeTableID() (int, error) { +func (n *Manager) findFreeTableID() (int, error) { configuredVRFs, err := n.ListL3() if err != nil { return -1, err @@ -131,7 +130,7 @@ func (n *NetlinkManager) findFreeTableID() (int, error) { return freeTableID, nil } -func (n *NetlinkManager) GetL3ByName(name string) (*VRFInformation, error) { +func (n *Manager) GetL3ByName(name string) (*VRFInformation, error) { list, err := n.ListL3() if err != nil { return nil, err @@ -144,20 +143,20 @@ func (n *NetlinkManager) GetL3ByName(name string) (*VRFInformation, error) { return nil, fmt.Errorf("no VRF with name %s", name) } -func (*NetlinkManager) EnsureBPFProgram(info VRFInformation) error { - if link, err := netlink.LinkByName(bridgePrefix + info.Name); err != nil { +func (n *Manager) EnsureBPFProgram(info VRFInformation) error { + if link, err := n.toolkit.LinkByName(bridgePrefix + info.Name); err != nil { return fmt.Errorf("error getting bridge interface of vrf %s: %w", info.Name, err) } else if err := bpf.AttachToInterface(link); err != nil { return fmt.Errorf("error attaching bpf program to bridge interface of vrf %s: %w", info.Name, err) } - if link, err := netlink.LinkByName(vrfToDefaultPrefix + info.Name); err != nil { + if link, err := n.toolkit.LinkByName(vrfToDefaultPrefix + info.Name); err != nil { return fmt.Errorf("error getting vrf2default interface of vrf %s: %w", info.Name, err) } else if err := bpf.AttachToInterface(link); err != nil { return fmt.Errorf("error attaching bpf program to vrf2default interface of vrf %s: %w", info.Name, err) } - if link, err := netlink.LinkByName(vxlanPrefix + info.Name); err != nil { + if link, err := n.toolkit.LinkByName(vxlanPrefix + info.Name); err != nil { return fmt.Errorf("error getting vxlan interface of vrf %s: %w", info.Name, err) } else if err := bpf.AttachToInterface(link); err != nil { return fmt.Errorf("error attaching bpf program to vxlan interface of vrf %s: %w", info.Name, err) @@ -173,23 +172,23 @@ func (info VRFInformation) linkMTU() int { return info.MTU } -func (*NetlinkManager) EnsureMTU(info VRFInformation) error { - link, err := netlink.LinkByName(vrfToDefaultPrefix + info.Name) +func (n *Manager) EnsureMTU(info VRFInformation) error { + link, err := n.toolkit.LinkByName(vrfToDefaultPrefix + info.Name) if err != nil { return fmt.Errorf("error getting vrf2default interface of vrf %s: %w", info.Name, err) } if link.Attrs().MTU != info.linkMTU() { - if err := netlink.LinkSetMTU(link, info.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(link, info.MTU); err != nil { return fmt.Errorf("error setting MTU of vrf2default interface of vrf %s: %w", info.Name, err) } } - link, err = netlink.LinkByName(defaultToVrfPrefix + info.Name) + link, err = n.toolkit.LinkByName(defaultToVrfPrefix + info.Name) if err != nil { return fmt.Errorf("error getting default2vrf interface of vrf %s: %w", info.Name, err) } if link.Attrs().MTU != info.linkMTU() { - if err := netlink.LinkSetMTU(link, info.MTU); err != nil { + if err := n.toolkit.LinkSetMTU(link, info.MTU); err != nil { return fmt.Errorf("error setting MTU of default2vrw interface of vrf %s: %w", info.Name, err) } } diff --git a/pkg/nl/list.go b/pkg/nl/list.go index 4498abcb..e9fba51a 100644 --- a/pkg/nl/list.go +++ b/pkg/nl/list.go @@ -9,8 +9,8 @@ import ( "golang.org/x/sys/unix" ) -func (*NetlinkManager) listRoutes() ([]netlink.Route, error) { - routes, err := netlink.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{ +func (n *Manager) listRoutes() ([]netlink.Route, error) { + routes, err := n.toolkit.RouteListFiltered(netlink.FAMILY_ALL, &netlink.Route{ Table: 0, }, netlink.RT_FILTER_TABLE) if err != nil { @@ -19,35 +19,34 @@ func (*NetlinkManager) listRoutes() ([]netlink.Route, error) { return routes, nil } -func (*NetlinkManager) listBridgeForwardingTable() ([]netlink.Neigh, error) { - entries, err := netlink.NeighList(0, unix.AF_BRIDGE) +func (n *Manager) listBridgeForwardingTable() ([]netlink.Neigh, error) { + entries, err := n.toolkit.NeighList(0, unix.AF_BRIDGE) if err != nil { return nil, fmt.Errorf("error listing bridge fdb entries: %w", err) } return entries, nil } -func (*NetlinkManager) listNeighbors() ([]netlink.Neigh, error) { - neighbors, err := netlink.NeighList(0, netlink.FAMILY_ALL) +func (n *Manager) listNeighbors() ([]netlink.Neigh, error) { + neighbors, err := n.toolkit.NeighList(0, netlink.FAMILY_ALL) if err != nil { - return nil, fmt.Errorf("error listing ipv4,ipv6 neighbors: %w", err) + return nil, fmt.Errorf("error listing all neighbors: %w", err) } return neighbors, nil } -func (*NetlinkManager) ListVRFInterfaces() (map[int]VRFInformation, error) { +func (n *Manager) ListVRFInterfaces() (map[int]VRFInformation, error) { // TODO: find a way to merge this with ListL3 infos := map[int]VRFInformation{} - links, err := netlink.LinkList() + links, err := n.toolkit.LinkList() if err != nil { - return nil, fmt.Errorf("error listing links: %w", err) + return nil, fmt.Errorf("cannot get links from netlink: %w", err) } for _, link := range links { if link.Type() != "vrf" { continue } - vrf, ok := link.(*netlink.Vrf) if !ok { return nil, fmt.Errorf("error casting link %v as netlink.Vrf", link) @@ -62,8 +61,8 @@ func (*NetlinkManager) ListVRFInterfaces() (map[int]VRFInformation, error) { return infos, nil } -func (*NetlinkManager) ListNeighborInterfaces() (map[int]netlink.Link, error) { - links, err := netlink.LinkList() +func (n *Manager) ListNeighborInterfaces() (map[int]netlink.Link, error) { + links, err := n.toolkit.LinkList() neighborLinks := map[int]netlink.Link{} if err != nil { return nil, fmt.Errorf("error listing links: %w", err) @@ -80,10 +79,10 @@ func (*NetlinkManager) ListNeighborInterfaces() (map[int]netlink.Link, error) { return neighborLinks, nil } -func (n *NetlinkManager) ListL3() ([]VRFInformation, error) { +func (n *Manager) ListL3() ([]VRFInformation, error) { infos := []VRFInformation{} - links, err := netlink.LinkList() + links, err := n.toolkit.LinkList() if err != nil { return nil, fmt.Errorf("error listing links: %w", err) } @@ -109,20 +108,20 @@ func (n *NetlinkManager) ListL3() ([]VRFInformation, error) { return infos, nil } -func (*NetlinkManager) updateL3Indices(info *VRFInformation) { - bridgeLink, err := netlink.LinkByName(bridgePrefix + info.Name) +func (n *Manager) updateL3Indices(info *VRFInformation) { + bridgeLink, err := n.toolkit.LinkByName(bridgePrefix + info.Name) if err == nil { info.bridgeID = bridgeLink.Attrs().Index } else { info.MarkForDelete = true } - vxlanLink, err := netlink.LinkByName(vxlanPrefix + info.Name) + vxlanLink, err := n.toolkit.LinkByName(vxlanPrefix + info.Name) if err == nil { info.VNI = vxlanLink.(*netlink.Vxlan).VxlanId } else { info.MarkForDelete = true } - vethLink, err := netlink.LinkByName(vrfToDefaultPrefix + info.Name) + vethLink, err := n.toolkit.LinkByName(vrfToDefaultPrefix + info.Name) if err == nil { info.MTU = vethLink.Attrs().MTU } else { @@ -130,24 +129,24 @@ func (*NetlinkManager) updateL3Indices(info *VRFInformation) { } } -func (*NetlinkManager) updateL2Indices(info *Layer2Information, links []netlink.Link) error { +func (n *Manager) updateL2Indices(info *Layer2Information, links []netlink.Link) error { for _, link := range links { // Check if master of interface is bridge if link.Attrs().MasterIndex != info.bridge.Attrs().Index { continue } - if err := updateLink(info, link); err != nil { + if err := n.updateLink(info, link); err != nil { return err } } // Read IP addresses - currentV4, err := netlink.AddrList(info.bridge, unix.AF_INET) + currentV4, err := n.toolkit.AddrList(info.bridge, unix.AF_INET) if err != nil { return fmt.Errorf("error listing link's IPv4 addresses: %w", err) } - currentV6, err := netlink.AddrList(info.bridge, unix.AF_INET6) + currentV6, err := n.toolkit.AddrList(info.bridge, unix.AF_INET6) if err != nil { return fmt.Errorf("error listing link's IPv6 addresses: %w", err) } @@ -166,7 +165,7 @@ func (*NetlinkManager) updateL2Indices(info *Layer2Information, links []netlink. return nil } -func updateLink(info *Layer2Information, link netlink.Link) error { +func (n *Manager) updateLink(info *Layer2Information, link netlink.Link) error { // If subinterface is VXLAN if link.Type() == "vxlan" && strings.HasPrefix(link.Attrs().Name, vxlanPrefix) { vxlan, ok := link.(*netlink.Vxlan) @@ -184,11 +183,11 @@ func updateLink(info *Layer2Information, link netlink.Link) error { return fmt.Errorf("error casting link %v as netlink.Veth", link) } info.macvlanBridge = macvlanBridge - peerIdx, err := netlink.VethPeerIndex(info.macvlanBridge) + peerIdx, err := n.toolkit.VethPeerIndex(info.macvlanBridge) if err != nil { return fmt.Errorf("error getting veth perr by index: %w", err) } - peerInterface, err := netlink.LinkByIndex(peerIdx) + peerInterface, err := n.toolkit.LinkByIndex(peerIdx) if err != nil { return fmt.Errorf("error getting link by index: %w", err) } @@ -203,10 +202,10 @@ func updateLink(info *Layer2Information, link netlink.Link) error { return nil } -func (n *NetlinkManager) ListL2() ([]Layer2Information, error) { +func (n *Manager) ListL2() ([]Layer2Information, error) { infos := []Layer2Information{} - links, err := netlink.LinkList() + links, err := n.toolkit.LinkList() if err != nil { return nil, fmt.Errorf("error listing links: %w", err) } @@ -231,7 +230,7 @@ func (n *NetlinkManager) ListL2() ([]Layer2Information, error) { info.VlanID = vlanID if info.bridge.MasterIndex > 0 { - vrf, err := netlink.LinkByIndex(info.bridge.MasterIndex) + vrf, err := n.toolkit.LinkByIndex(info.bridge.MasterIndex) if err != nil { return nil, fmt.Errorf("error getting link by index: %w", err) } @@ -251,7 +250,7 @@ func (n *NetlinkManager) ListL2() ([]Layer2Information, error) { return infos, nil } -func (*NetlinkManager) ListTaas() ([]TaasInformation, error) { +func (*Manager) ListTaas() ([]TaasInformation, error) { infos := []TaasInformation{} links, err := netlink.LinkList() diff --git a/pkg/nl/manager.go b/pkg/nl/manager.go index 42e566b9..7db7f9e0 100644 --- a/pkg/nl/manager.go +++ b/pkg/nl/manager.go @@ -26,10 +26,15 @@ const ( var macPrefix = []byte("\x02\x54") -type NetlinkManager struct { +type Manager struct { + toolkit ToolkitInterface } -func (*NetlinkManager) GetUnderlayIP() (net.IP, error) { - _, ip, err := getUnderlayInterfaceAndIP() +func NewManager(toolkit ToolkitInterface) *Manager { + return &Manager{toolkit: toolkit} +} + +func (n *Manager) GetUnderlayIP() (net.IP, error) { + _, ip, err := n.getUnderlayInterfaceAndIP() return ip, err } diff --git a/pkg/nl/mock/mock_nl.go b/pkg/nl/mock/mock_nl.go new file mode 100644 index 00000000..dcb71db3 --- /dev/null +++ b/pkg/nl/mock/mock_nl.go @@ -0,0 +1,411 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/telekom/das-schiff-network-operator/pkg/nl (interfaces: ToolkitInterface) + +// Package mock_nl is a generated GoMock package. +package mock_nl + +import ( + net "net" + reflect "reflect" + + netlink "github.com/vishvananda/netlink" + nl "github.com/vishvananda/netlink/nl" + gomock "go.uber.org/mock/gomock" +) + +// MockToolkitInterface is a mock of ToolkitInterface interface. +type MockToolkitInterface struct { + ctrl *gomock.Controller + recorder *MockToolkitInterfaceMockRecorder +} + +// MockToolkitInterfaceMockRecorder is the mock recorder for MockToolkitInterface. +type MockToolkitInterfaceMockRecorder struct { + mock *MockToolkitInterface +} + +// NewMockToolkitInterface creates a new mock instance. +func NewMockToolkitInterface(ctrl *gomock.Controller) *MockToolkitInterface { + mock := &MockToolkitInterface{ctrl: ctrl} + mock.recorder = &MockToolkitInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockToolkitInterface) EXPECT() *MockToolkitInterfaceMockRecorder { + return m.recorder +} + +// AddrAdd mocks base method. +func (m *MockToolkitInterface) AddrAdd(arg0 netlink.Link, arg1 *netlink.Addr) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddrAdd", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddrAdd indicates an expected call of AddrAdd. +func (mr *MockToolkitInterfaceMockRecorder) AddrAdd(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrAdd", reflect.TypeOf((*MockToolkitInterface)(nil).AddrAdd), arg0, arg1) +} + +// AddrDel mocks base method. +func (m *MockToolkitInterface) AddrDel(arg0 netlink.Link, arg1 *netlink.Addr) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddrDel", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddrDel indicates an expected call of AddrDel. +func (mr *MockToolkitInterfaceMockRecorder) AddrDel(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrDel", reflect.TypeOf((*MockToolkitInterface)(nil).AddrDel), arg0, arg1) +} + +// AddrList mocks base method. +func (m *MockToolkitInterface) AddrList(arg0 netlink.Link, arg1 int) ([]netlink.Addr, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddrList", arg0, arg1) + ret0, _ := ret[0].([]netlink.Addr) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AddrList indicates an expected call of AddrList. +func (mr *MockToolkitInterfaceMockRecorder) AddrList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddrList", reflect.TypeOf((*MockToolkitInterface)(nil).AddrList), arg0, arg1) +} + +// ExecuteNetlinkRequest mocks base method. +func (m *MockToolkitInterface) ExecuteNetlinkRequest(arg0 *nl.NetlinkRequest, arg1 int, arg2 uint16) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExecuteNetlinkRequest", arg0, arg1, arg2) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExecuteNetlinkRequest indicates an expected call of ExecuteNetlinkRequest. +func (mr *MockToolkitInterfaceMockRecorder) ExecuteNetlinkRequest(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteNetlinkRequest", reflect.TypeOf((*MockToolkitInterface)(nil).ExecuteNetlinkRequest), arg0, arg1, arg2) +} + +// LinkAdd mocks base method. +func (m *MockToolkitInterface) LinkAdd(arg0 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkAdd", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkAdd indicates an expected call of LinkAdd. +func (mr *MockToolkitInterfaceMockRecorder) LinkAdd(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkAdd", reflect.TypeOf((*MockToolkitInterface)(nil).LinkAdd), arg0) +} + +// LinkByIndex mocks base method. +func (m *MockToolkitInterface) LinkByIndex(arg0 int) (netlink.Link, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkByIndex", arg0) + ret0, _ := ret[0].(netlink.Link) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LinkByIndex indicates an expected call of LinkByIndex. +func (mr *MockToolkitInterfaceMockRecorder) LinkByIndex(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkByIndex", reflect.TypeOf((*MockToolkitInterface)(nil).LinkByIndex), arg0) +} + +// LinkByName mocks base method. +func (m *MockToolkitInterface) LinkByName(arg0 string) (netlink.Link, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkByName", arg0) + ret0, _ := ret[0].(netlink.Link) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LinkByName indicates an expected call of LinkByName. +func (mr *MockToolkitInterfaceMockRecorder) LinkByName(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkByName", reflect.TypeOf((*MockToolkitInterface)(nil).LinkByName), arg0) +} + +// LinkDel mocks base method. +func (m *MockToolkitInterface) LinkDel(arg0 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkDel", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkDel indicates an expected call of LinkDel. +func (mr *MockToolkitInterfaceMockRecorder) LinkDel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkDel", reflect.TypeOf((*MockToolkitInterface)(nil).LinkDel), arg0) +} + +// LinkGetProtinfo mocks base method. +func (m *MockToolkitInterface) LinkGetProtinfo(arg0 netlink.Link) (netlink.Protinfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkGetProtinfo", arg0) + ret0, _ := ret[0].(netlink.Protinfo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LinkGetProtinfo indicates an expected call of LinkGetProtinfo. +func (mr *MockToolkitInterfaceMockRecorder) LinkGetProtinfo(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkGetProtinfo", reflect.TypeOf((*MockToolkitInterface)(nil).LinkGetProtinfo), arg0) +} + +// LinkList mocks base method. +func (m *MockToolkitInterface) LinkList() ([]netlink.Link, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkList") + ret0, _ := ret[0].([]netlink.Link) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// LinkList indicates an expected call of LinkList. +func (mr *MockToolkitInterfaceMockRecorder) LinkList() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkList", reflect.TypeOf((*MockToolkitInterface)(nil).LinkList)) +} + +// LinkSetDown mocks base method. +func (m *MockToolkitInterface) LinkSetDown(arg0 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetDown", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetDown indicates an expected call of LinkSetDown. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetDown(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetDown", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetDown), arg0) +} + +// LinkSetHairpin mocks base method. +func (m *MockToolkitInterface) LinkSetHairpin(arg0 netlink.Link, arg1 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetHairpin", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetHairpin indicates an expected call of LinkSetHairpin. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetHairpin(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetHairpin", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetHairpin), arg0, arg1) +} + +// LinkSetHardwareAddr mocks base method. +func (m *MockToolkitInterface) LinkSetHardwareAddr(arg0 netlink.Link, arg1 net.HardwareAddr) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetHardwareAddr", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetHardwareAddr indicates an expected call of LinkSetHardwareAddr. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetHardwareAddr(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetHardwareAddr", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetHardwareAddr), arg0, arg1) +} + +// LinkSetLearning mocks base method. +func (m *MockToolkitInterface) LinkSetLearning(arg0 netlink.Link, arg1 bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetLearning", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetLearning indicates an expected call of LinkSetLearning. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetLearning(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetLearning", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetLearning), arg0, arg1) +} + +// LinkSetMTU mocks base method. +func (m *MockToolkitInterface) LinkSetMTU(arg0 netlink.Link, arg1 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetMTU", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetMTU indicates an expected call of LinkSetMTU. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetMTU(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetMTU", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetMTU), arg0, arg1) +} + +// LinkSetMaster mocks base method. +func (m *MockToolkitInterface) LinkSetMaster(arg0, arg1 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetMaster", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetMaster indicates an expected call of LinkSetMaster. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetMaster(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetMaster", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetMaster), arg0, arg1) +} + +// LinkSetMasterByIndex mocks base method. +func (m *MockToolkitInterface) LinkSetMasterByIndex(arg0 netlink.Link, arg1 int) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetMasterByIndex", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetMasterByIndex indicates an expected call of LinkSetMasterByIndex. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetMasterByIndex(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetMasterByIndex", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetMasterByIndex), arg0, arg1) +} + +// LinkSetNoMaster mocks base method. +func (m *MockToolkitInterface) LinkSetNoMaster(arg0 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetNoMaster", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetNoMaster indicates an expected call of LinkSetNoMaster. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetNoMaster(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetNoMaster", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetNoMaster), arg0) +} + +// LinkSetUp mocks base method. +func (m *MockToolkitInterface) LinkSetUp(arg0 netlink.Link) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LinkSetUp", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LinkSetUp indicates an expected call of LinkSetUp. +func (mr *MockToolkitInterfaceMockRecorder) LinkSetUp(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LinkSetUp", reflect.TypeOf((*MockToolkitInterface)(nil).LinkSetUp), arg0) +} + +// NeighList mocks base method. +func (m *MockToolkitInterface) NeighList(arg0, arg1 int) ([]netlink.Neigh, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NeighList", arg0, arg1) + ret0, _ := ret[0].([]netlink.Neigh) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NeighList indicates an expected call of NeighList. +func (mr *MockToolkitInterfaceMockRecorder) NeighList(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NeighList", reflect.TypeOf((*MockToolkitInterface)(nil).NeighList), arg0, arg1) +} + +// NewIPNet mocks base method. +func (m *MockToolkitInterface) NewIPNet(arg0 net.IP) *net.IPNet { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewIPNet", arg0) + ret0, _ := ret[0].(*net.IPNet) + return ret0 +} + +// NewIPNet indicates an expected call of NewIPNet. +func (mr *MockToolkitInterfaceMockRecorder) NewIPNet(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewIPNet", reflect.TypeOf((*MockToolkitInterface)(nil).NewIPNet), arg0) +} + +// ParseAddr mocks base method. +func (m *MockToolkitInterface) ParseAddr(arg0 string) (*netlink.Addr, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ParseAddr", arg0) + ret0, _ := ret[0].(*netlink.Addr) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ParseAddr indicates an expected call of ParseAddr. +func (mr *MockToolkitInterfaceMockRecorder) ParseAddr(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseAddr", reflect.TypeOf((*MockToolkitInterface)(nil).ParseAddr), arg0) +} + +// RouteAdd mocks base method. +func (m *MockToolkitInterface) RouteAdd(arg0 *netlink.Route) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RouteAdd", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RouteAdd indicates an expected call of RouteAdd. +func (mr *MockToolkitInterfaceMockRecorder) RouteAdd(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteAdd", reflect.TypeOf((*MockToolkitInterface)(nil).RouteAdd), arg0) +} + +// RouteDel mocks base method. +func (m *MockToolkitInterface) RouteDel(arg0 *netlink.Route) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RouteDel", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// RouteDel indicates an expected call of RouteDel. +func (mr *MockToolkitInterfaceMockRecorder) RouteDel(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteDel", reflect.TypeOf((*MockToolkitInterface)(nil).RouteDel), arg0) +} + +// RouteListFiltered mocks base method. +func (m *MockToolkitInterface) RouteListFiltered(arg0 int, arg1 *netlink.Route, arg2 uint64) ([]netlink.Route, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RouteListFiltered", arg0, arg1, arg2) + ret0, _ := ret[0].([]netlink.Route) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// RouteListFiltered indicates an expected call of RouteListFiltered. +func (mr *MockToolkitInterfaceMockRecorder) RouteListFiltered(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RouteListFiltered", reflect.TypeOf((*MockToolkitInterface)(nil).RouteListFiltered), arg0, arg1, arg2) +} + +// VethPeerIndex mocks base method. +func (m *MockToolkitInterface) VethPeerIndex(arg0 *netlink.Veth) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "VethPeerIndex", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// VethPeerIndex indicates an expected call of VethPeerIndex. +func (mr *MockToolkitInterfaceMockRecorder) VethPeerIndex(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "VethPeerIndex", reflect.TypeOf((*MockToolkitInterface)(nil).VethPeerIndex), arg0) +} diff --git a/pkg/nl/nl_test.go b/pkg/nl/nl_test.go new file mode 100644 index 00000000..491aa54d --- /dev/null +++ b/pkg/nl/nl_test.go @@ -0,0 +1,1400 @@ +package nl + +import ( + "errors" + "fmt" + "net" + "os" + "path/filepath" + "strconv" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + mock_nl "github.com/telekom/das-schiff-network-operator/pkg/nl/mock" + "github.com/vishvananda/netlink" + "go.uber.org/mock/gomock" + "golang.org/x/sys/unix" +) + +const ( + arpAccept = "arp_accept" + baseReachableTimeMs = "base_reachable_time" + addrGenMode = "addr_gen_mode" +) + +var ( + mockctrl *gomock.Controller + tmpDir string +) + +const dummyIntf = "dummy" + +var _ = BeforeSuite(func() { + var err error + tmpDir, err = os.MkdirTemp(".", "testdata") + Expect(err).ToNot(HaveOccurred()) + err = os.Chmod(tmpDir, 0o777) + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = AfterSuite(func() { + err := os.RemoveAll(tmpDir) + Expect(err).ToNot(HaveOccurred()) +}) + +func TestNL(t *testing.T) { + RegisterFailHandler(Fail) + mockctrl = gomock.NewController(t) + defer mockctrl.Finish() + RunSpecs(t, + "NL Suite") +} + +var _ = Describe("GetUnderlayIP()", func() { + It("returns error if cannot list addresses", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("cannot list addresses")) + _, err := nm.GetUnderlayIP() + Expect(err).To(HaveOccurred()) + }) + It("returns error if number of listed addresses is not equal to 1", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{}, {}}, nil) + _, err := nm.GetUnderlayIP() + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + _, err := nm.GetUnderlayIP() + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("ListL3()", func() { + It("returns error if cannot list links", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error listing links")) + _, err := nm.ListL3() + Expect(err).To(HaveOccurred()) + }) + It("returns empty slice if there are no vrf interfaces", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Bridge{}}, nil) + result, err := nm.ListL3() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + It("returns no error error if cannot get bridge, vxlan and vrf links by name", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("link not found")).Times(3) + // netlinkMock.EXPECT().LinkByName(bridgePrefix+dummyIntf).Return(nil, errors.New("link not found")) + // netlinkMock.EXPECT().LinkByName(vxlanPrefix+dummyIntf).Return(nil, errors.New("link not found")) + // netlinkMock.EXPECT().LinkByName(vrfToDefaultPrefix+dummyIntf).Return(nil, errors.New("link notfound")) + + _, err := nm.ListL3() + Expect(err).ToNot(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(bridgePrefix+dummyIntf).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(vxlanPrefix+dummyIntf).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(vrfToDefaultPrefix+dummyIntf).Return(&netlink.Vrf{}, nil) + _, err := nm.ListL3() + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("ListL2()", func() { + It("returns error if cannot list links", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error listing links")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns empty slice if there are no bridge interfaces", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{}}, nil) + result, err := nm.ListL2() + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeEmpty()) + }) + It("returns error if cannot get vlan ID as integer", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + dummyIntf}}}, nil) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot get bridge link by index", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3}}}, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(nil, errors.New("link not found")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot list addresses and not updating link", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3}}}, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed to list addresses")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if failed to list addresses and the link is vxlan", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed to list addresses")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if failed get veth peer index during update", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().VethPeerIndex(gomock.Any()).Return(-1, errors.New("cannot get veth peer index")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if failed to get link by index of veth peer", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().VethPeerIndex(gomock.Any()).Return(0, nil) + netlinkMock.EXPECT().LinkByIndex(0).Return(nil, errors.New("link not found")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if update succeeded but cannot list IPv4 addresses", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().VethPeerIndex(gomock.Any()).Return(0, nil) + netlinkMock.EXPECT().LinkByIndex(0).Return( + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, nil, + ) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed to list addresses")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns error if update succeeded but cannot list IPv6 addresses", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().VethPeerIndex(gomock.Any()).Return(0, nil) + netlinkMock.EXPECT().LinkByIndex(0).Return( + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, nil, + ) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{}, nil) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("failed to list addresses")) + _, err := nm.ListL2() + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{ + &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{Name: layer2Prefix + "33", MasterIndex: 3, Index: 3}}, + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, + }, nil) + netlinkMock.EXPECT().LinkByIndex(3).Return(&netlink.Vxlan{LinkAttrs: netlink.LinkAttrs{Name: vxlanPrefix + dummyIntf, Index: 3}}, nil) + netlinkMock.EXPECT().VethPeerIndex(gomock.Any()).Return(0, nil) + netlinkMock.EXPECT().LinkByIndex(0).Return( + &netlink.Veth{LinkAttrs: netlink.LinkAttrs{Name: vethL2Prefix + "33", MasterIndex: 3, Index: 3}}, nil, + ) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{ + {Scope: unix.RT_SCOPE_UNIVERSE}, + {Scope: unix.RT_SCOPE_HOST}, + }, nil).Times(2) + _, err := nm.ListL2() + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("ParseIPAddresses()", func() { + It("returns error if cannot parse address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().ParseAddr("10.0.0.1").Return(nil, errors.New("error parsing address")) + _, err := nm.ParseIPAddresses([]string{"10.0.0.1"}) + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().ParseAddr("10.0.0.1").Return(&netlink.Addr{}, nil) + _, err := nm.ParseIPAddresses([]string{"10.0.0.1"}) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("GetL3ByName()", func() { + It("returns error if cannot list L3", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error listing links")) + _, err := nm.GetL3ByName("name") + Expect(err).To(HaveOccurred()) + }) + It("returns error if L3 was not found", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: dummyIntf}}}, nil) + _, err := nm.GetL3ByName("name") + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(bridgePrefix+dummyIntf).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(vxlanPrefix+dummyIntf).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(vrfToDefaultPrefix+dummyIntf).Return(&netlink.Vrf{}, nil) + _, err := nm.GetL3ByName(dummyIntf) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("CleanupL3()", func() { + It("returns non empty error slice if any errors occurred", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkDel(gomock.Any()).Return(errors.New("error deleting link")).Times(4) + err := nm.CleanupL3("name") + Expect(err).ToNot(BeEmpty()) + }) + It("returns empty error slice if no errors occurred", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkDel(gomock.Any()).Return(nil).Times(4) + err := nm.CleanupL3("name") + Expect(err).To(BeEmpty()) + }) +}) + +var _ = Describe("UpL3()", func() { + It("returns error if cannot set link up", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(errors.New("failed to set link up")) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set up bridge", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("link not found")) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set up VRF to Default", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("link not found")) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set up Default to VRF", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil).Times(2) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("link not found")) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set up vxlan", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil).Times(3) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil).Times(3) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("link not found")) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).To(HaveOccurred()) + }) + It("returns error no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil).Times(4) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil).Times(4) + err := nm.UpL3(VRFInformation{Name: dummyIntf}) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("findFreeTableID()", func() { + It("returns error if cannot list L3", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error")) + v, err := nm.findFreeTableID() + Expect(v).To(Equal(-1)) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot find free table ID", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + links := []netlink.Link{} + for i := vrfTableStart; i <= vrfTableEnd+1; i++ { + links = append(links, &netlink.Vrf{Table: uint32(i), LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf + strconv.Itoa(i)}}) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + } + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return(links, nil) + + v, err := nm.findFreeTableID() + Expect(v).To(Equal(-1)) + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + v, err := nm.findFreeTableID() + Expect(v).To(Equal(vrfTableStart)) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("CleanupL2()", func() { + numOfInterfaces := 3 + info := &Layer2Information{ + vxlan: &netlink.Vxlan{}, + bridge: &netlink.Bridge{}, + CreateMACVLANInterface: true, + macvlanBridge: &netlink.Veth{}, + } + It("returns slice of 3 errors", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkDel(gomock.Any()).Return(errors.New("cannot delete link")).Times(numOfInterfaces) + errors := nm.CleanupL2(info) + Expect(errors).To(HaveLen(numOfInterfaces)) + }) + It("returns empty slice", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkDel(gomock.Any()).Return(nil).Times(numOfInterfaces) + errors := nm.CleanupL2(info) + Expect(errors).To(BeEmpty()) + }) +}) + +var _ = Describe("ReconcileL2()", func() { + It("returns error if anycast gateway is used but anycast MAC is not set", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{} + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: nil, + } + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to set MTU for bridge", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(errors.New("cannot set MTU")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to set MTU for vxlan", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(errors.New("cannot set MTU")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to set MTU for macvlanBridge", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(errors.New("cannot set MTU")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to set MTU for macvlanHost", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(3) + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(errors.New("cannot set MTU")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot get underlying interface and IP", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(nil, errors.New("error listing addresses")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if IPv6 was found", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.ParseIP("2001::"))}}, nil) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to set link down to change MAC address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(errors.New("unable to set link down")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable to change MAC address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(errors.New("unable to change MAC address")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable set link up after changing MAC address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(errors.New("unable to set link up")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if unable set vxlan MAC address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(errors.New("unable to change MAC address")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot get L3", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "desired", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error listing links")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set master by index and desired VRF", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "desired", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + desired.VRF}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkSetMasterByIndex(gomock.Any(), gomock.Any()).Return(errors.New("error setting master by index")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot set no master", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(errors.New("error setting no master")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot get bridge prot info", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{}, errors.New("error getting prot info")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reattach L2VNI - cannot set link down", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(errors.New("cannot set link down")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reattach L2VNI - cannot set link no master", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(errors.New("cannot set link no master")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reattach L2VNI - cannot set link master", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(errors.New("cannot set link master")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reattach L2VNI - cannot set link master", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(errors.New("cannot set link learning")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reattach L2VNI - cannot set link up", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(errors.New("cannot set link up")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot setup macvlan interface - error deleting interface", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: true, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(4) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkDel(gomock.Any()).Return(errors.New("cannot delete interface")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot setup macvlan interface - error creating macvlan interface", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: false, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + CreateMACVLANInterface: true, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(errors.New("cannot add link")) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot reconcile IPs - cannot add address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + + procSysNetPath = tmpDir + current := &Layer2Information{ + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: false, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + CreateMACVLANInterface: true, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().ExecuteNetlinkRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return([][]byte{}, nil) + netlinkMock.EXPECT().AddrAdd(gomock.Any(), gomock.Any()).Return(errors.New("cannot add address")) + + vlanName := fmt.Sprintf("%s%d", layer2Prefix, current.VlanID) + vethName := fmt.Sprintf("%s%d", vethL2Prefix, current.VlanID) + macVlanName := fmt.Sprintf("%s%d", macvlanPrefix, current.VlanID) + + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vethName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + addrGenModePathIPv6 = fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, macVlanName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + neighPathIPv4 := fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 := fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + neighPathIPv4 = fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 = fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + arpAcceptIPv4 := fmt.Sprintf("%s/ipv4/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(arpAcceptIPv4 + "/" + arpAccept) + + arpAcceptIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(arpAcceptIPv6 + "/" + arpAccept) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + + procSysNetPath = oldProcSysNetPath + }) + It("returns error if cannot reconcile IPs - cannot delete address", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + + procSysNetPath = tmpDir + current := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))}}, + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: false, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(2, 2, 2, 2))}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + CreateMACVLANInterface: true, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().ExecuteNetlinkRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return([][]byte{}, nil) + netlinkMock.EXPECT().AddrAdd(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() + netlinkMock.EXPECT().AddrDel(gomock.Any(), gomock.Any()).Return(errors.New("cannot delete address")) + + vethName := fmt.Sprintf("%s%d", vethL2Prefix, current.VlanID) + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vethName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + vlanName := fmt.Sprintf("%s%d", layer2Prefix, current.VlanID) + addrGenModePathvlanIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(addrGenModePathvlanIPv6 + "/" + addrGenMode) + + neighPathIPv4 := fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 := fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + neighPathIPv4 = fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 = fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + err := nm.ReconcileL2(current, desired) + Expect(err).To(HaveOccurred()) + + procSysNetPath = oldProcSysNetPath + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + + procSysNetPath = tmpDir + current := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(1, 1, 1, 1))}}, + bridge: &netlink.Bridge{LinkAttrs: netlink.LinkAttrs{HardwareAddr: net.HardwareAddr{1, 1, 1, 1, 1, 1}}}, + vxlan: &netlink.Vxlan{}, + macvlanBridge: &netlink.Veth{}, + macvlanHost: &netlink.Veth{}, + CreateMACVLANInterface: false, + VRF: "current", + } + desired := &Layer2Information{ + AnycastGateways: []*netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(2, 2, 2, 2))}}, + AnycastMAC: &net.HardwareAddr{0, 0, 0, 0, 0, 0}, + MTU: 1399, + VRF: "", + CreateMACVLANInterface: true, + } + + netlinkMock.EXPECT().LinkSetMTU(gomock.Any(), gomock.Any()).Return(nil).Times(2) + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return([]netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(0, 0, 0, 0))}}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetHardwareAddr(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkGetProtinfo(gomock.Any()).Return(netlink.Protinfo{Learning: true}, nil) + netlinkMock.EXPECT().LinkSetDown(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetNoMaster(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetMaster(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetLearning(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().ExecuteNetlinkRequest(gomock.Any(), gomock.Any(), gomock.Any()).Return([][]byte{}, nil) + netlinkMock.EXPECT().AddrAdd(gomock.Any(), gomock.Any()).Return(nil) + netlinkMock.EXPECT().AddrDel(gomock.Any(), gomock.Any()).Return(nil) + + vethName := fmt.Sprintf("%s%d", vethL2Prefix, current.VlanID) + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vethName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + vlanName := fmt.Sprintf("%s%d", layer2Prefix, current.VlanID) + addrGenModePathvlanIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(addrGenModePathvlanIPv6 + "/" + addrGenMode) + addrGenModePathvlanIPv4 := fmt.Sprintf("%s/ipv4/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(addrGenModePathvlanIPv4 + "/" + arpAccept) + + vlanName = fmt.Sprintf("%s%d", macvlanPrefix, current.VlanID) + addrGenModePathvlanIPv6 = fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vlanName) + createInterfaceFile(addrGenModePathvlanIPv6 + "/" + addrGenMode) + + neighPathIPv4 := fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 := fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vethName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + neighPathIPv4 = fmt.Sprintf("%s/ipv4/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv4 + "/" + baseReachableTimeMs) + + neighPathIPv6 = fmt.Sprintf("%s/ipv6/neigh/%s", procSysNetPath, vlanName) + createInterfaceFile(neighPathIPv6 + "/" + baseReachableTimeMs) + + err := nm.ReconcileL2(current, desired) + Expect(err).ToNot(HaveOccurred()) + + procSysNetPath = oldProcSysNetPath + }) +}) + +var _ = Describe("GetBridgeID()", func() { + It("returns error if cannot find link", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(nil, errors.New("error getting link by name")) + _, err := nm.GetBridgeID(&Layer2Information{}) + Expect(err).To(HaveOccurred()) + }) + It("returns no error", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + _, err := nm.GetBridgeID(&Layer2Information{}) + Expect(err).ToNot(HaveOccurred()) + }) +}) + +var _ = Describe("CreateL3()", func() { + It("returns error if VRF name is longer than 15 characters", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + vrfInfo := VRFInformation{ + Name: "reallyLongTestNameOver15Chars", + } + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot find free table ID", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + vrfInfo := VRFInformation{ + Name: vrfPrefix + dummyIntf, + } + + netlinkMock.EXPECT().LinkList().Return(nil, errors.New("error")) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot create VRF - failed to add link", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + vrfInfo := VRFInformation{ + Name: vrfPrefix + dummyIntf, + } + + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(errors.New("failed to add link")) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot create VRF - failed to disable EUI generation", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + vrfInfo := VRFInformation{ + Name: dummyIntf, + } + + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + }) + It("returns error if cannot create VRF - failed to set link up", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + procSysNetPath = tmpDir + vrfInfo := VRFInformation{ + Name: dummyIntf, + } + + vrfName := fmt.Sprintf("%s%s", vrfPrefix, dummyIntf) + + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(errors.New("failed to set link up")) + + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vrfName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + procSysNetPath = oldProcSysNetPath + }) + It("returns error if cannot create bridge - failed to add link", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + procSysNetPath = tmpDir + vrfInfo := VRFInformation{ + Name: dummyIntf, + } + + vrfName := fmt.Sprintf("%s%s", vrfPrefix, dummyIntf) + + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + v := []netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(127, 0, 0, 1))}} + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(v, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(errors.New("failed to add link")) + + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vrfName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + procSysNetPath = oldProcSysNetPath + }) + It("returns error if cannot create bridge - failed to disable EUI generation", func() { + netlinkMock := mock_nl.NewMockToolkitInterface(mockctrl) + nm := NewManager(netlinkMock) + oldProcSysNetPath := procSysNetPath + procSysNetPath = tmpDir + vrfInfo := VRFInformation{ + Name: dummyIntf, + } + + vrfName := fmt.Sprintf("%s%s", vrfPrefix, dummyIntf) + + netlinkMock.EXPECT().LinkList().Return([]netlink.Link{&netlink.Vrf{LinkAttrs: netlink.LinkAttrs{Name: vrfPrefix + dummyIntf}}}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Bridge{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vxlan{}, nil) + netlinkMock.EXPECT().LinkByName(gomock.Any()).Return(&netlink.Vrf{}, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + netlinkMock.EXPECT().LinkSetUp(gomock.Any()).Return(nil) + v := []netlink.Addr{{IPNet: netlink.NewIPNet(net.IPv4(127, 0, 0, 1))}} + netlinkMock.EXPECT().AddrList(gomock.Any(), gomock.Any()).Return(v, nil) + netlinkMock.EXPECT().LinkAdd(gomock.Any()).Return(nil) + + addrGenModePathIPv6 := fmt.Sprintf("%s/ipv6/conf/%s", procSysNetPath, vrfName) + createInterfaceFile(addrGenModePathIPv6 + "/" + addrGenMode) + + err := nm.CreateL3(vrfInfo) + Expect(err).To(HaveOccurred()) + procSysNetPath = oldProcSysNetPath + }) +}) + +func createInterfaceFile(path string) { + err := os.MkdirAll(filepath.Dir(path), 0o777) + Expect(err).ToNot(HaveOccurred()) + f, err := os.Create(path) + Expect(err).ToNot(HaveOccurred()) + err = f.Close() + Expect(err).ToNot(HaveOccurred()) + err = os.Chmod(path, 0o777) + Expect(err).ToNot(HaveOccurred()) +} diff --git a/pkg/nl/route.go b/pkg/nl/route.go index b2def78c..d9168585 100644 --- a/pkg/nl/route.go +++ b/pkg/nl/route.go @@ -138,7 +138,7 @@ func GetAddressFamily(addressFamily int) (string, error) { } } -func (*NetlinkManager) getVRFName(tableID int, vrfInterfaces map[int]VRFInformation) (string, error) { +func (*Manager) getVRFName(tableID int, vrfInterfaces map[int]VRFInformation) (string, error) { if tableID < 0 || tableID > 255 { return "", fmt.Errorf("table id %d out of range [0-255]", tableID) } @@ -160,7 +160,7 @@ func (*NetlinkManager) getVRFName(tableID int, vrfInterfaces map[int]VRFInformat } } -func (n *NetlinkManager) ListRouteInformation() ([]route.Information, error) { +func (n *Manager) ListRouteInformation() ([]route.Information, error) { netlinkRoutes, err := n.listRoutes() if err != nil { return nil, fmt.Errorf("error listing routes: %w", err) diff --git a/pkg/nl/taas.go b/pkg/nl/taas.go index 58049fe8..aca1af69 100644 --- a/pkg/nl/taas.go +++ b/pkg/nl/taas.go @@ -7,7 +7,7 @@ type TaasInformation struct { Table int } -func (n *NetlinkManager) CreateTaas(info TaasInformation) error { +func (n *Manager) CreateTaas(info TaasInformation) error { _, err := n.createVRF(info.Name, info.Table) if err != nil { return fmt.Errorf("error creating VRF for TaaS: %w", err) @@ -21,7 +21,7 @@ func (n *NetlinkManager) CreateTaas(info TaasInformation) error { return nil } -func (n *NetlinkManager) CleanupTaas(info TaasInformation) error { +func (n *Manager) CleanupTaas(info TaasInformation) error { err := n.deleteLink(info.Name) if err != nil { return fmt.Errorf("error deleting VRF for TaaS: %w", err) diff --git a/pkg/notrack/notrack.go b/pkg/notrack/notrack.go index 68a3550b..f9ce4cb6 100644 --- a/pkg/notrack/notrack.go +++ b/pkg/notrack/notrack.go @@ -88,14 +88,14 @@ func RunIPTablesSync() error { return fmt.Errorf("error connecting to ip6tables for notrack: %w", err) } - netlinkManager := &nl.NetlinkManager{} + netlinkManager := nl.NewManager(&nl.Toolkit{}) go syncIPTTables(netlinkManager, ipt4, ipt6) return nil } -func syncIPTTables(netlinkManager *nl.NetlinkManager, ipt4, ipt6 *iptables.IPTables) { +func syncIPTTables(netlinkManager *nl.Manager, ipt4, ipt6 *iptables.IPTables) { for { links, err := netlink.LinkList() if err != nil { diff --git a/pkg/reconciler/layer3.go b/pkg/reconciler/layer3.go index 4a0295e9..72508743 100644 --- a/pkg/reconciler/layer3.go +++ b/pkg/reconciler/layer3.go @@ -98,7 +98,7 @@ func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration, reloadTwice changed, err := r.frrManager.Configure(frr.Configuration{ VRFs: vrfConfigs, ASN: r.config.ServerASN, - }) + }, r.netlinkManager) if err != nil { r.Logger.Error(err, "error updating FRR configuration") return fmt.Errorf("error updating FRR configuration: %w", err) diff --git a/pkg/reconciler/layer3.go.orig b/pkg/reconciler/layer3.go.orig new file mode 100644 index 00000000..5d765655 --- /dev/null +++ b/pkg/reconciler/layer3.go.orig @@ -0,0 +1,421 @@ +package reconciler + +import ( + "context" + "fmt" + "net" + "sort" + "strconv" + "time" + + networkv1alpha1 "github.com/telekom/das-schiff-network-operator/api/v1alpha1" + "github.com/telekom/das-schiff-network-operator/pkg/config" + "github.com/telekom/das-schiff-network-operator/pkg/frr" + "github.com/telekom/das-schiff-network-operator/pkg/nl" +) + +const defaultSleep = 2 * time.Second + +func (r *reconcile) fetchLayer3(ctx context.Context) ([]networkv1alpha1.VRFRouteConfiguration, error) { + vrfs := &networkv1alpha1.VRFRouteConfigurationList{} + err := r.client.List(ctx, vrfs) + if err != nil { + r.Logger.Error(err, "error getting list of VRFs from Kubernetes") + return nil, fmt.Errorf("error getting list of VRFs from Kubernetes: %w", err) + } + + return vrfs.Items, nil +} + +func (r *reconcile) fetchTaas(ctx context.Context) ([]networkv1alpha1.RoutingTable, error) { + tables := &networkv1alpha1.RoutingTableList{} + err := r.client.List(ctx, tables) + if err != nil { + r.Logger.Error(err, "error getting list of TaaS from Kubernetes") + return nil, fmt.Errorf("error getting list of TaaS from Kubernetes: %w", err) + } + + return tables.Items, nil +} + +// nolint: contextcheck // context is not relevant +func (r *reconcile) reconcileLayer3(l3vnis []networkv1alpha1.VRFRouteConfiguration, taas []networkv1alpha1.RoutingTable) error { + vrfConfigMap, err := r.createVrfConfigMap(l3vnis) + if err != nil { + return err + } + + vrfFromTaas := createVrfFromTaaS(taas) + + allConfigs := []frr.VRFConfiguration{} + l3Configs := []frr.VRFConfiguration{} + taasConfigs := []frr.VRFConfiguration{} + for key := range vrfConfigMap { + allConfigs = append(allConfigs, vrfConfigMap[key]) + l3Configs = append(l3Configs, vrfConfigMap[key]) + } + for key := range vrfFromTaas { + allConfigs = append(allConfigs, vrfFromTaas[key]) + taasConfigs = append(taasConfigs, vrfFromTaas[key]) + } + + sort.SliceStable(allConfigs, func(i, j int) bool { + return allConfigs[i].VNI < allConfigs[j].VNI + }) + + created, deletedVRF, err := r.reconcileL3Netlink(l3Configs) + if err != nil { + r.Logger.Error(err, "error reconciling Netlink") + return err + } + + deletedTaas, err := r.reconcileTaasNetlink(taasConfigs) + if err != nil { + return err + } + reloadTwice := deletedVRF || deletedTaas + + // We wait here for two seconds to let FRR settle after updating netlink devices + time.Sleep(defaultSleep) + +<<<<<<< HEAD + err = r.configureFRR(allConfigs, reloadTwice) +======= + changed, err := r.frrManager.Configure(frr.Configuration{ + VRFs: vrfConfigs, + ASN: r.config.ServerASN, + }, r.netlinkManager) +>>>>>>> 9c7cb71 (Added unit tests) + if err != nil { + return err + } + + // Make sure that all created netlink VRFs are up after FRR reload + time.Sleep(defaultSleep) + for _, info := range created { + if err := r.netlinkManager.UpL3(info); err != nil { + r.Logger.Error(err, "error setting L3 to state UP") + return fmt.Errorf("error setting L3 to state UP: %w", err) + } + } + return nil +} + +func (r *reconcile) configureFRR(vrfConfigs []frr.VRFConfiguration, reloadTwice bool) error { + changed, err := r.frrManager.Configure(frr.Configuration{ + VRFs: vrfConfigs, + ASN: r.config.ServerASN, + }) + if err != nil { + r.Logger.Error(err, "error updating FRR configuration") + return fmt.Errorf("error updating FRR configuration: %w", err) + } + + if changed || r.dirtyFRRConfig { + err := r.reloadFRR() + if err != nil { + r.dirtyFRRConfig = true + return err + } + + // When a BGP VRF is deleted there is a leftover running configuration after reload + // A second reload fixes this. + if reloadTwice { + err := r.reloadFRR() + if err != nil { + r.dirtyFRRConfig = true + return err + } + } + r.dirtyFRRConfig = false + } + return nil +} + +func (r *reconcile) reloadFRR() error { + r.Logger.Info("trying to reload FRR config because it changed") + err := r.frrManager.ReloadFRR() + if err != nil { + r.Logger.Error(err, "error reloading FRR systemd unit, trying restart") + + err = r.frrManager.RestartFRR() + if err != nil { + r.Logger.Error(err, "error restarting FRR systemd unit") + return fmt.Errorf("error reloading / restarting FRR systemd unit: %w", err) + } + } + r.Logger.Info("reloaded FRR config") + return nil +} + +func (r *reconcile) createVrfConfigMap(l3vnis []networkv1alpha1.VRFRouteConfiguration) (map[string]frr.VRFConfiguration, error) { + vrfConfigMap := map[string]frr.VRFConfiguration{} + for i := range l3vnis { + spec := l3vnis[i].Spec + logger := r.Logger.WithValues("name", l3vnis[i].ObjectMeta.Name, "namespace", l3vnis[i].ObjectMeta.Namespace, "vrf", spec.VRF) + + var vni int + var rt string + + if val, ok := r.config.VRFConfig[spec.VRF]; ok { + vni = val.VNI + rt = val.RT + logger.Info("Configuring VRF from new VRFConfig", "vni", val.VNI, "rt", rt) + } else if val, ok := r.config.VRFToVNI[spec.VRF]; ok { + vni = val + logger.Info("Configuring VRF from old VRFToVNI", "vni", val) + } else if r.config.ShouldSkipVRFConfig(spec.VRF) { + vni = config.SkipVrfTemplateVni + } else { + err := fmt.Errorf("vrf not in vrf vni map") + r.Logger.Error(err, "VRF does not exist in VRF VNI config, ignoring", "vrf", spec.VRF, "name", l3vnis[i].ObjectMeta.Name, "namespace", l3vnis[i].ObjectMeta.Namespace) + continue + } + + cfg, err := createVrfConfig(vrfConfigMap, &spec, vni, rt) + if err != nil { + return nil, err + } + vrfConfigMap[spec.VRF] = *cfg + } + + return vrfConfigMap, nil +} + +func createVrfFromTaaS(taas []networkv1alpha1.RoutingTable) map[string]frr.VRFConfiguration { + vrfConfigMap := map[string]frr.VRFConfiguration{} + + for i := range taas { + spec := taas[i].Spec + + name := fmt.Sprintf("taas.%d", spec.TableID) + + vrfConfigMap[name] = frr.VRFConfiguration{ + Name: name, + VNI: spec.TableID, + IsTaaS: true, + } + } + + return vrfConfigMap +} + +func createVrfConfig(vrfConfigMap map[string]frr.VRFConfiguration, spec *networkv1alpha1.VRFRouteConfigurationSpec, vni int, rt string) (*frr.VRFConfiguration, error) { + // If VRF is not yet in dict, initialize it + if _, ok := vrfConfigMap[spec.VRF]; !ok { + vrfConfigMap[spec.VRF] = frr.VRFConfiguration{ + Name: spec.VRF, + VNI: vni, + RT: rt, + MTU: spec.MTU, + } + } + + cfg := vrfConfigMap[spec.VRF] + + if len(spec.Export) > 0 { + prefixList, err := handlePrefixItemList(spec.Export, spec.Seq, spec.Community) + if err != nil { + return nil, err + } + cfg.Export = append(cfg.Export, prefixList) + } + if len(spec.Import) > 0 { + prefixList, err := handlePrefixItemList(spec.Import, spec.Seq, nil) + if err != nil { + return nil, err + } + cfg.Import = append(cfg.Import, prefixList) + } + for _, aggregate := range spec.Aggregate { + _, network, err := net.ParseCIDR(aggregate) + if err != nil { + return nil, fmt.Errorf("error parsing CIDR %s: %w", aggregate, err) + } + if network.IP.To4() == nil { + cfg.AggregateIPv6 = append(cfg.AggregateIPv6, aggregate) + } else { + cfg.AggregateIPv4 = append(cfg.AggregateIPv4, aggregate) + } + } + return &cfg, nil +} + +func (r *reconcile) reconcileL3Netlink(vrfConfigs []frr.VRFConfiguration) ([]nl.VRFInformation, bool, error) { + existing, err := r.netlinkManager.ListL3() + if err != nil { + return nil, false, fmt.Errorf("error listing L3 VRF information: %w", err) + } + + // Check for VRFs that are configured on the host but no longer in Kubernetes + toDelete := []nl.VRFInformation{} + for i := range existing { + stillExists := false + for j := range vrfConfigs { + if vrfConfigs[j].Name == existing[i].Name && vrfConfigs[j].VNI == existing[i].VNI { + stillExists = true + existing[i].MTU = vrfConfigs[j].MTU + break + } + } + if !stillExists || existing[i].MarkForDelete { + toDelete = append(toDelete, existing[i]) + } else if err := r.reconcileExisting(existing[i]); err != nil { + r.Logger.Error(err, "error reconciling existing VRF", "vrf", existing[i].Name, "vni", strconv.Itoa(existing[i].VNI)) + } + } + + // Check for VRFs that are in Kubernetes but not yet configured on the host + toCreate := prepareVRFsToCreate(vrfConfigs, existing) + + // Delete / Cleanup VRFs + for _, info := range toDelete { + r.Logger.Info("Deleting VRF because it is no longer configured in Kubernetes", "vrf", info.Name, "vni", info.VNI) + errs := r.netlinkManager.CleanupL3(info.Name) + for _, err := range errs { + r.Logger.Error(err, "Error deleting VRF", "vrf", info.Name, "vni", strconv.Itoa(info.VNI)) + } + } + // Create VRFs + for _, info := range toCreate { + r.Logger.Info("Creating VRF to match Kubernetes", "vrf", info.Name, "vni", info.VNI) + err := r.netlinkManager.CreateL3(info) + if err != nil { + return nil, false, fmt.Errorf("error creating VRF %s, VNI %d: %w", info.Name, info.VNI, err) + } + } + + return toCreate, len(toDelete) > 0, nil +} + +func (r *reconcile) reconcileTaasNetlink(vrfConfigs []frr.VRFConfiguration) (bool, error) { + existing, err := r.netlinkManager.ListTaas() + if err != nil { + return false, fmt.Errorf("error listing TaaS VRF information: %w", err) + } + + deletedInterface, err := r.cleanupTaasNetlink(existing, vrfConfigs) + if err != nil { + return false, err + } + + err = r.createTaasNetlink(existing, vrfConfigs) + if err != nil { + return false, err + } + + return deletedInterface, nil +} + +func (r *reconcile) cleanupTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) (bool, error) { + deletedInterface := false + for _, cfg := range existing { + stillExists := false + for i := range intended { + if intended[i].Name == cfg.Name && intended[i].VNI == cfg.Table { + stillExists = true + } + } + if !stillExists { + deletedInterface = true + err := r.netlinkManager.CleanupTaas(cfg) + if err != nil { + return false, fmt.Errorf("error deleting TaaS %s, table %d: %w", cfg.Name, cfg.Table, err) + } + } + } + return deletedInterface, nil +} + +func (r *reconcile) createTaasNetlink(existing []nl.TaasInformation, intended []frr.VRFConfiguration) error { + for i := range intended { + alreadyExists := false + for _, cfg := range existing { + if intended[i].Name == cfg.Name && intended[i].VNI == cfg.Table { + alreadyExists = true + break + } + } + if !alreadyExists { + info := nl.TaasInformation{ + Name: intended[i].Name, + Table: intended[i].VNI, + } + err := r.netlinkManager.CreateTaas(info) + if err != nil { + return fmt.Errorf("error creating Taas %s, table %d: %w", info.Name, info.Table, err) + } + } + } + return nil +} + +func (r *reconcile) reconcileExisting(cfg nl.VRFInformation) error { + if err := r.netlinkManager.EnsureBPFProgram(cfg); err != nil { + return fmt.Errorf("error ensuring BPF program on VRF") + } + if err := r.netlinkManager.EnsureMTU(cfg); err != nil { + return fmt.Errorf("error setting VRF veth link MTU: %d", cfg.MTU) + } + return nil +} + +func prepareVRFsToCreate(vrfConfigs []frr.VRFConfiguration, existing []nl.VRFInformation) []nl.VRFInformation { + create := []nl.VRFInformation{} + for i := range vrfConfigs { + // Skip VRF with VNI SKIP_VRF_TEMPLATE_VNI + if vrfConfigs[i].VNI == config.SkipVrfTemplateVni { + continue + } + alreadyExists := false + for _, cfg := range existing { + if vrfConfigs[i].Name == cfg.Name && vrfConfigs[i].VNI == cfg.VNI && !cfg.MarkForDelete { + alreadyExists = true + break + } + } + if !alreadyExists { + create = append(create, nl.VRFInformation{ + Name: vrfConfigs[i].Name, + VNI: vrfConfigs[i].VNI, + MTU: vrfConfigs[i].MTU, + }) + } + } + return create +} + +func handlePrefixItemList(input []networkv1alpha1.VrfRouteConfigurationPrefixItem, seq int, community *string) (frr.PrefixList, error) { + prefixList := frr.PrefixList{ + Seq: seq + 1, + Community: community, + } + for i, item := range input { + frrItem, err := copyPrefixItemToFRRItem(i, item) + if err != nil { + return frr.PrefixList{}, err + } + prefixList.Items = append(prefixList.Items, frrItem) + } + return prefixList, nil +} + +func copyPrefixItemToFRRItem(n int, item networkv1alpha1.VrfRouteConfigurationPrefixItem) (frr.PrefixedRouteItem, error) { + _, network, err := net.ParseCIDR(item.CIDR) + if err != nil { + return frr.PrefixedRouteItem{}, fmt.Errorf("error parsing CIDR :%s: %w", item.CIDR, err) + } + + seq := item.Seq + if seq <= 0 { + seq = n + 1 + } + return frr.PrefixedRouteItem{ + CIDR: *network, + IPv6: network.IP.To4() == nil, + Seq: seq, + Action: item.Action, + GE: item.GE, + LE: item.LE, + }, nil +} diff --git a/pkg/reconciler/reconciler.go b/pkg/reconciler/reconciler.go index 99530f6d..040d90c8 100644 --- a/pkg/reconciler/reconciler.go +++ b/pkg/reconciler/reconciler.go @@ -20,7 +20,7 @@ const defaultDebounceTime = 20 * time.Second type Reconciler struct { client client.Client - netlinkManager *nl.NetlinkManager + netlinkManager *nl.Manager frrManager *frr.Manager anycastTracker *anycast.Tracker config *config.Config @@ -40,7 +40,7 @@ type reconcile struct { func NewReconciler(clusterClient client.Client, anycastTracker *anycast.Tracker, logger logr.Logger) (*Reconciler, error) { reconciler := &Reconciler{ client: clusterClient, - netlinkManager: &nl.NetlinkManager{}, + netlinkManager: nl.NewManager(&nl.Toolkit{}), frrManager: frr.NewFRRManager(), anycastTracker: anycastTracker, logger: logger,