diff --git a/internal/smdclient/SMDclient.go b/internal/smdclient/SMDclient.go index 3e0e65ff..eaa30dd0 100644 --- a/internal/smdclient/SMDclient.go +++ b/internal/smdclient/SMDclient.go @@ -46,10 +46,14 @@ type SMDClient struct { tokenEndpoint string accessToken string nodes map[string]NodeMapping - nodesMutex *sync.Mutex + nodesMutex *sync.RWMutex nodes_last_update time.Time stopCacheRefresh chan struct{} stopOnce sync.Once + // Reverse indexes for O(1) lookups + ipToXname map[string]string + macToXname map[string]string + wgipToXname map[string]string } type NodeInterface struct { @@ -62,6 +66,7 @@ type NodeInterface struct { type NodeMapping struct { Xname string `json:"xname" yaml:"xname"` Interfaces []NodeInterface `json:"interfaces" yaml:"interfaces"` + Groups []string `json:"groups" yaml:"groups"` } // NewSMDClient creates a new SMDClient which connects to the SMD server at baseurl @@ -105,10 +110,13 @@ func NewSMDClient(clusterName, baseurl, jwtURL, accessToken, certPath string, in smdBaseURL: baseurl, tokenEndpoint: jwtURL, accessToken: accessToken, - nodesMutex: &sync.Mutex{}, + nodesMutex: &sync.RWMutex{}, nodes_last_update: time.Now(), nodes: make(map[string]NodeMapping), stopCacheRefresh: make(chan struct{}), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), } // Populate the cache initially @@ -219,7 +227,7 @@ func (s *SMDClient) getSMD(ep string, smd interface{}) error { } // PopulateNodes fetches the Ethernet interface data from the SMD server and populates the nodes map -// with the corresponding node information, including MAC addresses, IP addresses, and descriptions. +// with the corresponding node information, including MAC addresses, IP addresses, descriptions, and group membership. func (s *SMDClient) PopulateNodes() { s.nodesMutex.Lock() defer s.nodesMutex.Unlock() @@ -273,44 +281,77 @@ func (s *SMDClient) PopulateNodes() { s.nodes[ep.CompID] = newNode } } + + // Populate group membership for all nodes + log.Debug().Msg("Fetching group membership for all nodes") + for xname, node := range s.nodes { + ml := new(sm.Membership) + membershipEp := "/hsm/v2/memberships/" + xname + if err := s.getSMD(membershipEp, ml); err != nil { + log.Debug().Err(err).Msgf("Failed to get group membership for %s", xname) + node.Groups = []string{} // Empty groups if fetch fails + } else { + node.Groups = ml.GroupLabels + } + s.nodes[xname] = node + } + + // Build reverse indexes for O(1) lookups + log.Debug().Msg("Building reverse indexes") + s.ipToXname = make(map[string]string) + s.macToXname = make(map[string]string) + s.wgipToXname = make(map[string]string) + + for xname, node := range s.nodes { + for _, iface := range node.Interfaces { + if iface.IP != "" { + s.ipToXname[strings.ToLower(iface.IP)] = xname + } + if iface.MAC != "" { + s.macToXname[strings.ToLower(iface.MAC)] = xname + } + if iface.WGIP != "" { + s.wgipToXname[strings.ToLower(iface.WGIP)] = xname + } + } + } + s.nodes_last_update = time.Now() - log.Debug().Msg("Nodes map populated") + log.Debug().Msgf("Nodes map populated with %d nodes, %d IP mappings, %d MAC mappings", + len(s.nodes), len(s.ipToXname), len(s.macToXname)) } // IDfromMAC returns the ID of the xname that has the MAC address func (s *SMDClient) IDfromMAC(mac string) (string, error) { - s.nodesMutex.Lock() - defer s.nodesMutex.Unlock() + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() - for _, node := range s.nodes { - for _, iface := range node.Interfaces { - if strings.EqualFold(mac, iface.MAC) { - return node.Xname, nil - } - } + key := strings.ToLower(mac) + if xname, found := s.macToXname[key]; found { + return xname, nil } - return "", errors.New("MAC " + mac + " not found for an xname in nodes") + return "", fmt.Errorf("MAC %s not found for an xname in nodes", mac) } // IDfromIP returns the ID of the xname that has the IP address func (s *SMDClient) IDfromIP(ipaddr string) (string, error) { - s.nodesMutex.Lock() - defer s.nodesMutex.Unlock() + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() - for _, node := range s.nodes { - for _, iface := range node.Interfaces { - if strings.EqualFold(ipaddr, iface.IP) || strings.EqualFold(ipaddr, iface.WGIP) { - return node.Xname, nil - } - } + key := strings.ToLower(ipaddr) + if xname, found := s.ipToXname[key]; found { + return xname, nil } - return "", errors.New("IP address " + ipaddr + " not found for an xname in nodes") + if xname, found := s.wgipToXname[key]; found { + return xname, nil + } + return "", fmt.Errorf("IP address %s not found for an xname in nodes", ipaddr) } // IPfromID returns the IP address of the xname with the given ID func (s *SMDClient) IPfromID(id string) (string, error) { - s.nodesMutex.Lock() - defer s.nodesMutex.Unlock() + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() if node, found := s.nodes[id]; found { if node.Interfaces != nil { if len(node.Interfaces) > 0 { @@ -323,8 +364,8 @@ func (s *SMDClient) IPfromID(id string) (string, error) { } func (s *SMDClient) MACfromID(id string) (string, error) { - s.nodesMutex.Lock() - defer s.nodesMutex.Unlock() + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() if node, found := s.nodes[id]; found { if node.Interfaces != nil { if len(node.Interfaces) > 0 { @@ -343,13 +384,15 @@ func (s *SMDClient) GroupMembership(id string) ([]string, error) { log.Err(err).Msg("failed to get group membership") return []string{}, err } - ml := new(sm.Membership) - ep := "/hsm/v2/memberships/" + id - err := s.getSMD(ep, ml) - if err != nil { - return nil, err + + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() + + if node, found := s.nodes[id]; found { + return node.Groups, nil } - return ml.GroupLabels, nil + + return []string{}, fmt.Errorf("node %s not found in cache", id) } func (s *SMDClient) ComponentInformation(id string) (base.Component, error) { @@ -372,6 +415,9 @@ func (s *SMDClient) AddWGIP(id string, wgip string) error { if node.Interfaces != nil { if len(node.Interfaces) > 0 { node.Interfaces[0].WGIP = wgip + s.nodes[id] = node + // Update reverse index + s.wgipToXname[strings.ToLower(wgip)] = id return nil } return errors.New("no interfaces found for ID " + id) @@ -381,8 +427,8 @@ func (s *SMDClient) AddWGIP(id string, wgip string) error { } func (s *SMDClient) WGIPfromID(id string) (string, error) { - s.nodesMutex.Lock() - defer s.nodesMutex.Unlock() + s.nodesMutex.RLock() + defer s.nodesMutex.RUnlock() if node, found := s.nodes[id]; found { if node.Interfaces != nil { if len(node.Interfaces) > 0 { diff --git a/internal/smdclient/SMDclient_performance_test.go b/internal/smdclient/SMDclient_performance_test.go new file mode 100644 index 00000000..c0a03372 --- /dev/null +++ b/internal/smdclient/SMDclient_performance_test.go @@ -0,0 +1,449 @@ +package smdclient + +import ( + "fmt" + "net/http" + "net/http/httptest" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestGroupMembershipCached verifies that Bug #1 is fixed: +// GroupMembership should use the cache instead of making HTTP requests +func TestGroupMembershipCached(t *testing.T) { + requestCount := 0 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestCount++ + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ + { + "ComponentID": "x1000", + "MACAddress": "00:11:22:33:44:55", + "IPAddresses": [{"IPAddress": "192.168.1.1"}], + "Description": "Test Node 1" + } + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute", "cabinet1"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + // Populate cache - should make 2 requests (interfaces + membership) + initialRequests := requestCount + client.PopulateNodes() + populateRequests := requestCount - initialRequests + + // Verify group membership was cached + groups, err := client.GroupMembership("x1000") + require.NoError(t, err) + assert.Equal(t, []string{"compute", "cabinet1"}, groups) + + // Verify no additional HTTP requests were made + assert.Equal(t, populateRequests, requestCount-initialRequests, + "GroupMembership should not make HTTP requests after cache is populated") + + // Call GroupMembership 100 times - should use cache every time + for i := 0; i < 100; i++ { + groups, err := client.GroupMembership("x1000") + require.NoError(t, err) + assert.Equal(t, []string{"compute", "cabinet1"}, groups) + } + + // Verify still no additional requests + assert.Equal(t, populateRequests, requestCount-initialRequests, + "GroupMembership made %d HTTP requests when it should have used cache", + requestCount-initialRequests-populateRequests) +} + +// TestConcurrentReads verifies that Bug #2 is fixed: +// Read operations should use RLock instead of Lock to allow concurrent access +func TestConcurrentReads(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ + { + "ComponentID": "x1000", + "MACAddress": "00:11:22:33:44:55", + "IPAddresses": [{"IPAddress": "192.168.1.1"}], + "Description": "Test Node 1" + }, + { + "ComponentID": "x1001", + "MACAddress": "00:11:22:33:44:66", + "IPAddresses": [{"IPAddress": "192.168.1.2"}], + "Description": "Test Node 2" + } + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1001": + _, _ = w.Write([]byte(`{"GroupLabels": ["io"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + // Test concurrent reads - with RLock these should all run in parallel + // With Lock (the bug), they would serialize + const concurrency = 100 + start := time.Now() + + var wg sync.WaitGroup + wg.Add(concurrency) + + for i := 0; i < concurrency; i++ { + go func(id int) { + defer wg.Done() + + // Each goroutine does multiple read operations + _, _ = client.IDfromIP("192.168.1.1") + _, _ = client.IDfromMAC("00:11:22:33:44:55") + _, _ = client.IPfromID("x1000") + _, _ = client.MACfromID("x1000") + _, _ = client.GroupMembership("x1000") + + // Add small delay to ensure overlap + time.Sleep(1 * time.Millisecond) + }(i) + } + + wg.Wait() + elapsed := time.Since(start) + + // With RLock, 100 goroutines should complete in ~100-200ms + // With Lock (serialized), it would take ~10-20s (100 * 100ms) + assert.Less(t, elapsed, 2*time.Second, + "Concurrent reads took %v - this suggests Lock instead of RLock is being used", elapsed) + + t.Logf("100 concurrent readers completed in %v", elapsed) +} + +// TestReverseIndexPerformance verifies that Bug #3 is fixed: +// IP/MAC lookups should be O(1) using reverse indexes, not O(n) linear search +func TestReverseIndexPerformance(t *testing.T) { + // Generate a large number of nodes to test performance + nodeCount := 1000 + + ethInterfaces := "[" + for i := 0; i < nodeCount; i++ { + if i > 0 { + ethInterfaces += "," + } + ethInterfaces += fmt.Sprintf(`{ + "ComponentID": "x%d", + "MACAddress": "00:11:22:33:%02x:%02x", + "IPAddresses": [{"IPAddress": "192.168.%d.%d"}], + "Description": "Node %d" + }`, i, (i>>8)&0xFF, i&0xFF, (i>>8)&0xFF, i&0xFF, i) + } + ethInterfaces += "]" + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if r.URL.Path == "/hsm/v2/Inventory/EthernetInterfaces/" { + _, _ = w.Write([]byte(ethInterfaces)) + } else if len(r.URL.Path) >= len("/hsm/v2/memberships/") && r.URL.Path[:len("/hsm/v2/memberships/")] == "/hsm/v2/memberships/" { + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + // Verify reverse indexes were built + assert.Equal(t, nodeCount, len(client.ipToXname), "IP reverse index should have %d entries", nodeCount) + assert.Equal(t, nodeCount, len(client.macToXname), "MAC reverse index should have %d entries", nodeCount) + + // Test lookup performance - should be O(1) + // With O(n) linear search, 1000 lookups on 1000 nodes = 1M comparisons + // With O(1) hash map, 1000 lookups = 1000 lookups + + lookupCount := 1000 + start := time.Now() + + for i := 0; i < lookupCount; i++ { + ip := fmt.Sprintf("192.168.%d.%d", (i>>8)&0xFF, i&0xFF) + xname, err := client.IDfromIP(ip) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("x%d", i), xname) + } + + elapsed := time.Since(start) + + // O(1) lookups: 1000 lookups should take <10ms + // O(n) lookups: 1000 lookups on 1000 nodes would take >100ms + assert.Less(t, elapsed, 50*time.Millisecond, + "1000 lookups on 1000 nodes took %v - this suggests O(n) linear search instead of O(1) hash map", elapsed) + + t.Logf("1000 IP lookups on 1000 nodes completed in %v (avg %v per lookup)", + elapsed, elapsed/time.Duration(lookupCount)) + + // Test MAC lookups + start = time.Now() + for i := 0; i < lookupCount; i++ { + mac := fmt.Sprintf("00:11:22:33:%02x:%02x", (i>>8)&0xFF, i&0xFF) + xname, err := client.IDfromMAC(mac) + require.NoError(t, err) + assert.Equal(t, fmt.Sprintf("x%d", i), xname) + } + elapsed = time.Since(start) + + assert.Less(t, elapsed, 50*time.Millisecond, + "1000 MAC lookups on 1000 nodes took %v - this suggests O(n) linear search", elapsed) + + t.Logf("1000 MAC lookups on 1000 nodes completed in %v (avg %v per lookup)", + elapsed, elapsed/time.Duration(lookupCount)) +} + +// TestCaseInsensitiveLookup verifies that IP/MAC lookups are case-insensitive +func TestCaseInsensitiveLookup(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ + { + "ComponentID": "x1000", + "MACAddress": "AA:BB:CC:DD:EE:FF", + "IPAddresses": [{"IPAddress": "192.168.1.1"}], + "Description": "Test Node" + } + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + // Test case variations for MAC + testMACs := []string{ + "AA:BB:CC:DD:EE:FF", + "aa:bb:cc:dd:ee:ff", + "Aa:Bb:Cc:Dd:Ee:Ff", + } + + for _, mac := range testMACs { + xname, err := client.IDfromMAC(mac) + require.NoError(t, err, "Failed to lookup MAC %s", mac) + assert.Equal(t, "x1000", xname, "Case-insensitive MAC lookup failed for %s", mac) + } + + // Test case variations for IP (though IPs are typically lowercase) + testIPs := []string{ + "192.168.1.1", + "192.168.1.1", // IPs don't have case, but test anyway + } + + for _, ip := range testIPs { + xname, err := client.IDfromIP(ip) + require.NoError(t, err, "Failed to lookup IP %s", ip) + assert.Equal(t, "x1000", xname, "IP lookup failed for %s", ip) + } +} + +// TestAddWGIPUpdatesReverseIndex verifies that AddWGIP updates the reverse index +func TestAddWGIPUpdatesReverseIndex(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ + { + "ComponentID": "x1000", + "MACAddress": "00:11:22:33:44:55", + "IPAddresses": [{"IPAddress": "192.168.1.1"}], + "Description": "Test Node" + } + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + // Initially no WireGuard IP + _, err := client.IDfromIP("10.99.0.1") + assert.Error(t, err, "WireGuard IP should not be found before AddWGIP") + + // Add WireGuard IP + err = client.AddWGIP("x1000", "10.99.0.1") + require.NoError(t, err) + + // Now it should be findable + xname, err := client.IDfromIP("10.99.0.1") + require.NoError(t, err) + assert.Equal(t, "x1000", xname, "WireGuard IP should be findable after AddWGIP") + + // Verify WGIPfromID works + wgip, err := client.WGIPfromID("x1000") + require.NoError(t, err) + assert.Equal(t, "10.99.0.1", wgip) +} + +// BenchmarkIDfromIP benchmarks the IP lookup performance +func BenchmarkIDfromIP(b *testing.B) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + if r.URL.Path == "/hsm/v2/Inventory/EthernetInterfaces/" { + // Generate 1000 nodes + ethInterfaces := "[" + for i := 0; i < 1000; i++ { + if i > 0 { + ethInterfaces += "," + } + ethInterfaces += fmt.Sprintf(`{ + "ComponentID": "x%d", + "MACAddress": "00:11:22:33:%02x:%02x", + "IPAddresses": [{"IPAddress": "192.168.%d.%d"}], + "Description": "Node %d" + }`, i, (i>>8)&0xFF, i&0xFF, (i>>8)&0xFF, i&0xFF, i) + } + ethInterfaces += "]" + _, _ = w.Write([]byte(ethInterfaces)) + } else if len(r.URL.Path) >= len("/hsm/v2/memberships/") && r.URL.Path[:len("/hsm/v2/memberships/")] == "/hsm/v2/memberships/" { + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + ip := fmt.Sprintf("192.168.%d.%d", (i%1000>>8)&0xFF, (i%1000)&0xFF) + _, _ = client.IDfromIP(ip) + } +} + +// BenchmarkGroupMembership benchmarks the group membership lookup performance +func BenchmarkGroupMembership(b *testing.B) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ + { + "ComponentID": "x1000", + "MACAddress": "00:11:22:33:44:55", + "IPAddresses": [{"IPAddress": "192.168.1.1"}], + "Description": "Test Node" + } + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute", "cabinet1", "rack1"]}`)) + } + }) + server := httptest.NewServer(handler) + defer server.Close() + + client := &SMDClient{ + smdClient: server.Client(), + smdBaseURL: server.URL, + nodesMutex: &sync.RWMutex{}, + nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), + } + + client.PopulateNodes() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = client.GroupMembership("x1000") + } +} diff --git a/internal/smdclient/SMDclient_test.go b/internal/smdclient/SMDclient_test.go index 9fe04617..ddfe4f80 100644 --- a/internal/smdclient/SMDclient_test.go +++ b/internal/smdclient/SMDclient_test.go @@ -14,10 +14,12 @@ import ( func TestPopulateNodes(t *testing.T) { // Mock SMD server handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`[ + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -48,9 +50,15 @@ func TestPopulateNodes(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)); err != nil { - // If an error occurs here, something is very wrong. - t.Errorf("Write(): unexpected error: %v", err) + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1001": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute", "io"]}`)) + case "/hsm/v2/memberships/x1002": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1003": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute", "cabinet1"]}`)) } }) server := httptest.NewServer(handler) @@ -60,18 +68,21 @@ func TestPopulateNodes(t *testing.T) { client := &SMDClient{ smdClient: server.Client(), smdBaseURL: server.URL, - nodesMutex: &sync.Mutex{}, + nodesMutex: &sync.RWMutex{}, nodes_last_update: time.Now(), nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), } // Call PopulateNodes client.PopulateNodes() // Verify nodes map - client.nodesMutex.Lock() + client.nodesMutex.RLock() t.Log(client.nodes) - defer client.nodesMutex.Unlock() + defer client.nodesMutex.RUnlock() assert.Equal(t, 4, len(client.nodes)) @@ -104,10 +115,12 @@ func TestPopulateNodes(t *testing.T) { func TestIPfromID(t *testing.T) { // Mock SMD server handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`[ + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -138,9 +151,15 @@ func TestIPfromID(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)); err != nil { - // If an error occurs here, something is very wrong. - t.Errorf("Write(): unexpected error: %v", err) + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1001": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1002": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1003": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) } }) server := httptest.NewServer(handler) @@ -150,9 +169,12 @@ func TestIPfromID(t *testing.T) { client := &SMDClient{ smdClient: server.Client(), smdBaseURL: server.URL, - nodesMutex: &sync.Mutex{}, + nodesMutex: &sync.RWMutex{}, nodes_last_update: time.Now(), nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), } // Call PopulateNodes to populate the nodes map @@ -186,10 +208,12 @@ func TestIPfromID(t *testing.T) { func TestIDfromIP(t *testing.T) { // Mock SMD server handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`[ + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -220,9 +244,15 @@ func TestIDfromIP(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)); err != nil { - // If an error occurs here, something is very wrong. - t.Errorf("Write(): unexpected error: %v", err) + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1001": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1002": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1003": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) } }) server := httptest.NewServer(handler) @@ -232,9 +262,12 @@ func TestIDfromIP(t *testing.T) { client := &SMDClient{ smdClient: server.Client(), smdBaseURL: server.URL, - nodesMutex: &sync.Mutex{}, + nodesMutex: &sync.RWMutex{}, nodes_last_update: time.Now(), nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), } // Call PopulateNodes to populate the nodes map @@ -268,10 +301,12 @@ func TestIDfromIP(t *testing.T) { func TestIDfromMAC(t *testing.T) { // Mock SMD server handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/hsm/v2/Inventory/EthernetInterfaces/", r.URL.Path) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write([]byte(`[ + + switch r.URL.Path { + case "/hsm/v2/Inventory/EthernetInterfaces/": + _, _ = w.Write([]byte(`[ { "ComponentID": "x1000", "MACAddress": "00:11:22:33:44:55", @@ -302,9 +337,15 @@ func TestIDfromMAC(t *testing.T) { "IPAddresses": [{"IPAddr": "192.168.1.6"}], "Description": "Test Node 4 Interface 2" } - ]`)); err != nil { - // If an error occurs here, something is very wrong. - t.Errorf("Write(): unexpected error: %v", err) + ]`)) + case "/hsm/v2/memberships/x1000": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1001": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1002": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) + case "/hsm/v2/memberships/x1003": + _, _ = w.Write([]byte(`{"GroupLabels": ["compute"]}`)) } }) server := httptest.NewServer(handler) @@ -314,9 +355,12 @@ func TestIDfromMAC(t *testing.T) { client := &SMDClient{ smdClient: server.Client(), smdBaseURL: server.URL, - nodesMutex: &sync.Mutex{}, + nodesMutex: &sync.RWMutex{}, nodes_last_update: time.Now(), nodes: make(map[string]NodeMapping), + ipToXname: make(map[string]string), + macToXname: make(map[string]string), + wgipToXname: make(map[string]string), } // Call PopulateNodes to populate the nodes map