diff --git a/src/ifmib/ifmib.go b/src/ifmib/ifmib.go index fc836b6..01d5fd4 100644 --- a/src/ifmib/ifmib.go +++ b/src/ifmib/ifmib.go @@ -13,9 +13,9 @@ import ( "github.com/posteo/go-agentx/value" "go.fd.io/govpp/api" "gopkg.in/yaml.v3" - + "govpp-snmp-agentx/logger" - "govpp-snmp-agentx/vppstats" + "govpp-snmp-agentx/vpp" ) // IF-MIB OID bases: @@ -72,8 +72,8 @@ type VPPConfig struct { } type VPPInterface struct { - Description string `yaml:"description"` - SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"` + Description string `yaml:"description"` + SubInterfaces map[string]VPPInterface `yaml:"sub-interfaces"` } type InterfaceMIB struct { @@ -172,7 +172,7 @@ func (m *InterfaceMIB) UpdateStats(interfaceStats *api.InterfaceStats) { } func (m *InterfaceMIB) addInterfaceToMIB(iface *api.InterfaceCounters) { - idx := int(iface.InterfaceIndex) + *vppstats.IfIndexOffset + idx := int(iface.InterfaceIndex) + *vpp.IfIndexOffset // Add ifEntry (classic interface table) entries m.addIfEntry(iface, idx) diff --git a/src/ifmib/ifmib_test.go b/src/ifmib/ifmib_test.go index 3ca53d7..a46cc33 100644 --- a/src/ifmib/ifmib_test.go +++ b/src/ifmib/ifmib_test.go @@ -11,27 +11,27 @@ import ( func TestNewInterfaceMIB(t *testing.T) { mib := NewInterfaceMIB() - + if mib == nil { t.Fatal("NewInterfaceMIB returned nil") } - + if mib.handler == nil { t.Error("Expected handler to be initialized") } - + if mib.stats == nil { t.Error("Expected stats map to be initialized") } - + if mib.descriptions == nil { t.Error("Expected descriptions map to be initialized") } - + if len(mib.stats) != 0 { t.Errorf("Expected stats map to be empty, got %d entries", len(mib.stats)) } - + if len(mib.descriptions) != 0 { t.Errorf("Expected descriptions map to be empty, got %d entries", len(mib.descriptions)) } @@ -40,11 +40,11 @@ func TestNewInterfaceMIB(t *testing.T) { func TestGetHandler(t *testing.T) { mib := NewInterfaceMIB() handler := mib.GetHandler() - + if handler == nil { t.Error("GetHandler returned nil") } - + if handler != mib.handler { t.Error("GetHandler returned different handler than expected") } @@ -52,7 +52,7 @@ func TestGetHandler(t *testing.T) { func TestLoadVPPConfigValidYAML(t *testing.T) { mib := NewInterfaceMIB() - + // Create a temporary YAML file yamlContent := `interfaces: GigabitEthernet0/0/0: @@ -64,39 +64,39 @@ loopbacks: loop0: description: 'Test: Loopback' ` - + tmpfile, err := os.CreateTemp("", "test_*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) - + if _, err := tmpfile.Write([]byte(yamlContent)); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } - + // Test loading the config err = mib.LoadVPPConfig(tmpfile.Name()) if err != nil { t.Fatalf("LoadVPPConfig failed: %v", err) } - + // Check that descriptions were loaded if len(mib.descriptions) != 3 { t.Errorf("Expected 3 descriptions, got %d", len(mib.descriptions)) } - + if mib.descriptions["GigabitEthernet0/0/0"] != "Test: Interface" { t.Errorf("Unexpected interface description: %s", mib.descriptions["GigabitEthernet0/0/0"]) } - + if mib.descriptions["GigabitEthernet0/0/0.100"] != "Test: Sub-interface" { t.Errorf("Unexpected sub-interface description: %s", mib.descriptions["GigabitEthernet0/0/0.100"]) } - + if mib.descriptions["loop0"] != "Test: Loopback" { t.Errorf("Unexpected loopback description: %s", mib.descriptions["loop0"]) } @@ -104,7 +104,7 @@ loopbacks: func TestLoadVPPConfigNonExistentFile(t *testing.T) { mib := NewInterfaceMIB() - + err := mib.LoadVPPConfig("/nonexistent/file.yaml") if err == nil { t.Error("Expected error for non-existent file") @@ -113,25 +113,25 @@ func TestLoadVPPConfigNonExistentFile(t *testing.T) { func TestLoadVPPConfigInvalidYAML(t *testing.T) { mib := NewInterfaceMIB() - + // Create a temporary file with invalid YAML invalidYAML := `interfaces: test: [ ` - + tmpfile, err := os.CreateTemp("", "invalid_*.yaml") if err != nil { t.Fatal(err) } defer os.Remove(tmpfile.Name()) - + if _, err := tmpfile.Write([]byte(invalidYAML)); err != nil { t.Fatal(err) } if err := tmpfile.Close(); err != nil { t.Fatal(err) } - + err = mib.LoadVPPConfig(tmpfile.Name()) if err == nil { t.Error("Expected error for invalid YAML") @@ -140,7 +140,7 @@ func TestLoadVPPConfigInvalidYAML(t *testing.T) { func TestUpdateStatsBasic(t *testing.T) { mib := NewInterfaceMIB() - + // Create mock interface stats stats := &api.InterfaceStats{ Interfaces: []api.InterfaceCounters{ @@ -158,15 +158,15 @@ func TestUpdateStatsBasic(t *testing.T) { }, }, } - + // Call UpdateStats (this will test the basic flow without AgentX sessions) mib.UpdateStats(stats) - + // Check that stats were stored if len(mib.stats) != 1 { t.Errorf("Expected 1 interface in stats, got %d", len(mib.stats)) } - + if storedStats, exists := mib.stats[0]; !exists { t.Error("Expected interface 0 to be stored in stats") } else { @@ -177,4 +177,4 @@ func TestUpdateStatsBasic(t *testing.T) { t.Errorf("Expected RX packets 100, got %d", storedStats.Rx.Packets) } } -} \ No newline at end of file +} diff --git a/src/main.go b/src/main.go index cac929c..b431f45 100644 --- a/src/main.go +++ b/src/main.go @@ -13,7 +13,7 @@ import ( "govpp-snmp-agentx/config" "govpp-snmp-agentx/ifmib" "govpp-snmp-agentx/logger" - "govpp-snmp-agentx/vppstats" + "govpp-snmp-agentx/vpp" ) func main() { @@ -41,7 +41,7 @@ func main() { } // Start VPP stats routine with callback to update MIB - vppstats.StartStatsRoutine(interfaceMIB.UpdateStats) + vpp.StartStatsRoutine(interfaceMIB.UpdateStats) // Set up signal handling for graceful shutdown sigChan := make(chan os.Signal, 1) @@ -50,7 +50,7 @@ func main() { // Wait for shutdown signal <-sigChan logger.Printf("Shutting down...") - + // Flush any buffered log entries logger.Sync() } diff --git a/src/main_test.go b/src/main_test.go index a49c56a..4657a0e 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -14,7 +14,7 @@ func TestMainCompiles(t *testing.T) { // This would run main(), but we skip it in tests return } - + // Just test that we can access main package t.Log("Main package compiles successfully") -} \ No newline at end of file +} diff --git a/src/vpp/vpp_iface.go b/src/vpp/vpp_iface.go new file mode 100644 index 0000000..c110cf5 --- /dev/null +++ b/src/vpp/vpp_iface.go @@ -0,0 +1,72 @@ +// Copyright 2025, IPng Networks GmbH, Pim van Pelt + +package vpp + +import ( + "os" + + "go.fd.io/govpp/api" + interfaces "go.fd.io/govpp/binapi/interface" + + "govpp-snmp-agentx/logger" +) + +func WatchInterfaceEvents(ch api.Channel) error { + logger.Debugf("WatchInterfaceEvents() called - starting interface event monitoring") + + notifChan := make(chan api.Message, 100) + + // subscribe for specific event message + logger.Debugf("Subscribing to interface events...") + sub, err := ch.SubscribeNotification(notifChan, &interfaces.SwInterfaceEvent{}) + if err != nil { + logger.Debugf("error subscribing to interface events: %v", err) + return err + } + logger.Debugf("Successfully subscribed to interface events") + + // enable interface events in VPP + logger.Debugf("Enabling interface events in VPP...") + err = ch.SendRequest(&interfaces.WantInterfaceEvents{ + PID: uint32(os.Getpid()), + EnableDisable: 1, + }).ReceiveReply(&interfaces.WantInterfaceEventsReply{}) + if err != nil { + logger.Debugf("error enabling interface events: %v", err) + return err + } + + logger.Debugf("Interface events enabled in VPP, starting event listener goroutine") + + // receive notifications + go func() { + logger.Debugf("Interface event listener goroutine started") + defer func() { + logger.Debugf("Interface event listener goroutine shutting down") + // disable interface events in VPP + err = ch.SendRequest(&interfaces.WantInterfaceEvents{ + PID: uint32(os.Getpid()), + EnableDisable: 0, + }).ReceiveReply(&interfaces.WantInterfaceEventsReply{}) + if err != nil { + logger.Debugf("error disabling interface events: %v", err) + } + + // unsubscribe from receiving events + err = sub.Unsubscribe() + if err != nil { + logger.Debugf("error unsubscribing from interface events: %v", err) + } + }() + + logger.Debugf("Interface event listener waiting for events...") + for notif := range notifChan { + e := notif.(*interfaces.SwInterfaceEvent) + logger.Debugf("interface event: SwIfIndex=%d, Flags=%d, Deleted=%t", + e.SwIfIndex, e.Flags, e.Deleted) + } + logger.Debugf("Interface event listener goroutine ended") + }() + + return nil +} diff --git a/src/vppstats/stats.go b/src/vpp/vpp_stats.go similarity index 91% rename from src/vppstats/stats.go rename to src/vpp/vpp_stats.go index ba5433c..d6c3062 100644 --- a/src/vppstats/stats.go +++ b/src/vpp/vpp_stats.go @@ -1,6 +1,6 @@ // Copyright 2025, IPng Networks GmbH, Pim van Pelt -package vppstats +package vpp import ( "flag" @@ -131,6 +131,21 @@ func statsRoutine(period time.Duration, callback StatsCallback) { logger.Printf("Connected to VPP (API: %s, Stats: %s)", *ApiAddr, *StatsAddr) connected = true wasConnected = true + + // Start watching interface events + logger.Debugf("Creating API channel for interface events...") + ch, err := conn.NewAPIChannel() + if err != nil { + logger.Debugf("Failed to create API channel for interface events: %v", err) + } else { + logger.Debugf("API channel created successfully, calling WatchInterfaceEvents...") + if err := WatchInterfaceEvents(ch); err != nil { + logger.Debugf("Failed to start interface event watching: %v", err) + ch.Close() + } else { + logger.Printf("Interface event watching started successfully") + } + } } // Query stats if connected @@ -231,4 +246,3 @@ func checkVPPLiveness(conn *core.Connection) bool { logger.Debugf("VPP liveness check passed (version: %s)", string(reply.Version)) return true } - diff --git a/src/vppstats/stats_test.go b/src/vppstats/stats_test.go deleted file mode 100644 index d72dee2..0000000 --- a/src/vppstats/stats_test.go +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright 2025, IPng Networks GmbH, Pim van Pelt - -package vppstats - -import ( - "flag" - "fmt" - "testing" - "time" - - "go.fd.io/govpp/api" -) - -func TestVPPStatsFlags(t *testing.T) { - // Test default values - if *ApiAddr != "/var/run/vpp/api.sock" { - t.Errorf("Expected default API address to be '/var/run/vpp/api.sock', got '%s'", *ApiAddr) - } - - if *StatsAddr != "/var/run/vpp/stats.sock" { - t.Errorf("Expected default stats address to be '/var/run/vpp/stats.sock', got '%s'", *StatsAddr) - } - - if *IfIndexOffset != 1000 { - t.Errorf("Expected default interface index offset to be 1000, got %d", *IfIndexOffset) - } - - if *Period != 10 { - t.Errorf("Expected default period to be 10, got %d", *Period) - } -} - -func TestFlagRegistrations(t *testing.T) { - tests := []struct { - name string - flagName string - defValue string - }{ - {"API address", "vppstats.api.addr", "/var/run/vpp/api.sock"}, - {"Stats address", "vppstats.stats.addr", "/var/run/vpp/stats.sock"}, - {"Index offset", "vppstats.ifindex-offset", "1000"}, - {"Period", "vppstats.period", "10"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - f := flag.Lookup(tt.flagName) - if f == nil { - t.Errorf("Expected %s flag to be registered", tt.flagName) - return - } - - if f.DefValue != tt.defValue { - t.Errorf("Expected %s flag default value to be '%s', got '%s'", - tt.flagName, tt.defValue, f.DefValue) - } - }) - } -} - -func TestStatsCallbackType(t *testing.T) { - // Test that we can create a valid callback function - var called bool - var receivedStats *api.InterfaceStats - - callback := func(stats *api.InterfaceStats) { - called = true - receivedStats = stats - } - - // Create mock stats - mockStats := &api.InterfaceStats{ - Interfaces: []api.InterfaceCounters{ - { - InterfaceIndex: 1, - InterfaceName: "test", - }, - }, - } - - // Call the callback - callback(mockStats) - - if !called { - t.Error("Expected callback to be called") - } - - if receivedStats != mockStats { - t.Error("Expected callback to receive the same stats object") - } - - if len(receivedStats.Interfaces) != 1 { - t.Errorf("Expected 1 interface, got %d", len(receivedStats.Interfaces)) - } - - if receivedStats.Interfaces[0].InterfaceName != "test" { - t.Errorf("Expected interface name 'test', got '%s'", receivedStats.Interfaces[0].InterfaceName) - } -} - -func TestPeriodConversion(t *testing.T) { - // Test that period conversion works correctly - originalPeriod := *Period - defer func() { *Period = originalPeriod }() - - testPeriods := []struct { - input int - expected time.Duration - }{ - {1, time.Second}, - {5, 5 * time.Second}, - {10, 10 * time.Second}, - {60, time.Minute}, - } - - for _, tt := range testPeriods { - t.Run(fmt.Sprintf("period_%d", tt.input), func(t *testing.T) { - *Period = tt.input - result := time.Duration(*Period) * time.Second - - if result != tt.expected { - t.Errorf("Expected period %v, got %v", tt.expected, result) - } - }) - } -} - -func TestFlagValues(t *testing.T) { - // Save original flag values - originalApiAddr := *ApiAddr - originalStatsAddr := *StatsAddr - originalOffset := *IfIndexOffset - originalPeriod := *Period - - defer func() { - *ApiAddr = originalApiAddr - *StatsAddr = originalStatsAddr - *IfIndexOffset = originalOffset - *Period = originalPeriod - }() - - // Test setting flag values - *ApiAddr = "/custom/api.sock" - *StatsAddr = "/custom/stats.sock" - *IfIndexOffset = 2000 - *Period = 30 - - if *ApiAddr != "/custom/api.sock" { - t.Errorf("Expected API address to be '/custom/api.sock', got '%s'", *ApiAddr) - } - - if *StatsAddr != "/custom/stats.sock" { - t.Errorf("Expected stats address to be '/custom/stats.sock', got '%s'", *StatsAddr) - } - - if *IfIndexOffset != 2000 { - t.Errorf("Expected interface index offset to be 2000, got %d", *IfIndexOffset) - } - - if *Period != 30 { - t.Errorf("Expected period to be 30, got %d", *Period) - } -} \ No newline at end of file