77 "fmt"
88 "io"
99 "os"
10+ "regexp"
1011 "strings"
1112 "testing"
1213
@@ -18,6 +19,7 @@ import (
1819 kubevirtv1 "kubevirt.io/api/core/v1"
1920
2021 "github.com/k8snetworkplumbingwg/kubemacpool/pkg/names"
22+ "github.com/k8snetworkplumbingwg/kubemacpool/tests/kubectl"
2123 "github.com/k8snetworkplumbingwg/kubemacpool/tests/reporter"
2224)
2325
@@ -144,6 +146,11 @@ func dumpKubemacpoolLogs(failureCount int) {
144146 if err := logNodes (failureCount ); err != nil {
145147 fmt .Println (err )
146148 }
149+
150+ // Dump collision gauge values to help debug alert flakiness / lingering collisions.
151+ if err := logMACCollisionGauge (failureCount ); err != nil {
152+ fmt .Println (err )
153+ }
147154}
148155
149156func logPodContainersLogs (podName string , containers []corev1.Container , failureCount int ) error {
@@ -588,3 +595,130 @@ func logNodes(failureCount int) error {
588595
589596 return nil
590597}
598+
599+ func logMACCollisionGauge (failureCount int ) error {
600+ report , err := macCollisionGaugeReport ()
601+ if strings .TrimSpace (report ) == "" {
602+ report = "(no kmp_mac_collisions metrics found)"
603+ }
604+
605+ if err != nil {
606+ report = fmt .Sprintf ("failed to build kmp mac collision gauge report: %v\n \n %s" , err , report )
607+ }
608+
609+ if logErr := reporter .LogToFile ("kmp_mac_collisions_gauge" , report , artifactDir , failureCount ); logErr != nil {
610+ return fmt .Errorf ("failed to log mac collision gauge report: %w" , logErr )
611+ }
612+
613+ return err
614+ }
615+
616+ func macCollisionGaugeReport () (string , error ) {
617+ var b strings.Builder
618+
619+ token , stderr , err := getPrometheusToken ()
620+ if err != nil {
621+ return "" , fmt .Errorf ("failed to get prometheus token: %s: %w" , stderr , err )
622+ }
623+
624+ metrics , err := getMetrics (token )
625+ if err != nil {
626+ return "" , fmt .Errorf ("failed to scrape /metrics: %w" , err )
627+ }
628+
629+ vmiByMAC , err := getVMIByMAC ()
630+ if err != nil {
631+ // Keep whatever collision lines we can so the output is still useful.
632+ appendMACCollisionLines (& b , metrics , nil )
633+ return strings .TrimSpace (b .String ()), err
634+ }
635+
636+ appendMACCollisionLines (& b , metrics , vmiByMAC )
637+ return strings .TrimSpace (b .String ()), nil
638+ }
639+
640+ func getVMIByMAC () (map [string ][]string , error ) {
641+ const vmiJSONPath = `{range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\t"}{.status.interfaces[*].mac}{"\n"}{end}`
642+ allVMIOutput , vmiStderr , vmiErr := kubectl .Kubectl (
643+ "get" , "vmi" , "-A" ,
644+ "-o" , "jsonpath=" + vmiJSONPath ,
645+ )
646+ if vmiErr != nil {
647+ return nil , fmt .Errorf ("failed to run kubectl get vmi: %s: %w" , vmiStderr , vmiErr )
648+ }
649+
650+ vmiByMAC := map [string ][]string {}
651+ for _ , line := range strings .Split (allVMIOutput , "\n " ) {
652+ line = strings .TrimSpace (line )
653+ if line == "" {
654+ continue
655+ }
656+
657+ parts := strings .Split (line , "\t " )
658+ // Expected: <namespace>\t<name>\t<macs...>
659+ if len (parts ) < 3 {
660+ continue
661+ }
662+
663+ namespace := strings .TrimSpace (parts [0 ])
664+ name := strings .TrimSpace (parts [1 ])
665+ macsField := strings .TrimSpace (strings .Join (parts [2 :], "\t " ))
666+ if namespace == "" || name == "" || macsField == "" {
667+ continue
668+ }
669+
670+ vmiID := namespace + "/" + name
671+ for _ , mac := range strings .Fields (macsField ) {
672+ mac = strings .ToLower (strings .TrimSpace (mac ))
673+ if mac == "" {
674+ continue
675+ }
676+ vmiByMAC [mac ] = append (vmiByMAC [mac ], vmiID )
677+ }
678+ }
679+
680+ return vmiByMAC , nil
681+ }
682+
683+ func appendMACCollisionLines (b * strings.Builder , metrics string , vmiByMAC map [string ][]string ) {
684+ for _ , line := range strings .Split (metrics , "\n " ) {
685+ if ! strings .HasPrefix (line , macCollisionMetric + "{" ) {
686+ continue
687+ }
688+
689+ b .WriteString (line )
690+ b .WriteString ("\n " )
691+
692+ mac , ok := parseMACLabelValue (line )
693+ if ! ok {
694+ continue
695+ }
696+
697+ mac = strings .ToLower (mac )
698+ b .WriteString (fmt .Sprintf ("MAC %s VMI matches:\n " , mac ))
699+
700+ if vmiByMAC == nil {
701+ b .WriteString ("(skipped: failed listing VMIs)\n " )
702+ continue
703+ }
704+
705+ matches := vmiByMAC [mac ]
706+ if len (matches ) == 0 {
707+ b .WriteString ("(no matching VMIs found)\n " )
708+ continue
709+ }
710+
711+ b .WriteString (strings .Join (matches , "\n " ))
712+ b .WriteString ("\n " )
713+ }
714+ }
715+
716+ func parseMACLabelValue (line string ) (string , bool ) {
717+ var macLabelValueRE = regexp .MustCompile (`\bmac="([^"]+)"` )
718+
719+ m := macLabelValueRE .FindStringSubmatch (line )
720+ if len (m ) != 2 {
721+ return "" , false
722+ }
723+ return m [1 ], true
724+ }
0 commit comments