@@ -21,8 +21,9 @@ import (
2121 "errors"
2222 "fmt"
2323 "io"
24- "regexp "
24+ "net "
2525 "strconv"
26+ "strings"
2627)
2728
2829var (
@@ -74,10 +75,67 @@ type flowDirective struct {
7475// PortMapping contains the ofport number and MAC address for an interface.
7576type PortMapping struct {
7677 // OfPort specifies the OpenFlow port number.
77- OfPort int
78+ OfPort int `json:"ofPort"`
7879
7980 // MACAddress specifies the MAC address of the interface.
80- MACAddress string
81+ MACAddress string `json:"macAddress"`
82+ }
83+
84+ // UnmarshalText unmarshals a PortMapping from textual form as output by
85+ // 'ovs-ofctl show':
86+ //
87+ // 7(interface1): addr:fe:4f:76:09:88:2b
88+ func (p * PortMapping ) UnmarshalText (b []byte ) error {
89+ // Make a copy per documentation for encoding.TextUnmarshaler.
90+ s := strings .TrimSpace (string (b ))
91+
92+ // Expected format: " 7(interface1): addr:fe:4f:76:09:88:2b"
93+ // Find the opening parenthesis
94+ openParen := strings .IndexByte (s , '(' )
95+ if openParen == - 1 {
96+ return fmt .Errorf ("invalid port mapping format: missing opening parenthesis" )
97+ }
98+
99+ // Find the closing parenthesis
100+ closeParen := strings .IndexByte (s [openParen :], ')' )
101+ if closeParen == - 1 {
102+ return fmt .Errorf ("invalid port mapping format: missing closing parenthesis" )
103+ }
104+ closeParen += openParen
105+
106+ // Extract ofport number (before opening parenthesis)
107+ ofportStr := strings .TrimSpace (s [:openParen ])
108+ ofport , err := strconv .Atoi (ofportStr )
109+ if err != nil {
110+ return fmt .Errorf ("invalid ofport number: %w" , err )
111+ }
112+
113+ // Validate ofport is in valid OpenFlow port range [0, 65535]
114+ if ofport < 0 || ofport > 65535 {
115+ return fmt .Errorf ("ofport %d out of valid range [0, 65535]" , ofport )
116+ }
117+
118+ // Find "addr:" after the closing parenthesis
119+ addrPrefix := ": addr:"
120+ addrIdx := strings .Index (s [closeParen :], addrPrefix )
121+ if addrIdx == - 1 {
122+ return fmt .Errorf ("invalid port mapping format: missing addr prefix" )
123+ }
124+ addrIdx += closeParen + len (addrPrefix )
125+
126+ // Extract MAC address (after "addr:")
127+ macAddress := strings .TrimSpace (s [addrIdx :])
128+
129+ // Validate MAC address format
130+ if _ , err := net .ParseMAC (macAddress ); err != nil {
131+ return fmt .Errorf ("invalid MAC address %q: %w" , macAddress , err )
132+ }
133+
134+ p .OfPort = ofport
135+ // Note: interface name is not stored in PortMapping, it's used as map key
136+ p .MACAddress = macAddress
137+
138+ return nil
81139}
82140
83141// Possible flowDirective directive values.
@@ -335,6 +393,22 @@ func (o *OpenFlowService) DumpAggregate(bridge string, flow *MatchFlow) (*FlowSt
335393 return stats , nil
336394}
337395
396+ // DumpPortMapping retrieves port mapping (ofport and MAC address) for a
397+ // specific interface from the specified bridge.
398+ func (o * OpenFlowService ) DumpPortMapping (bridge string , interfaceName string ) (* PortMapping , error ) {
399+ mappings , err := o .DumpPortMappings (bridge )
400+ if err != nil {
401+ return nil , err
402+ }
403+
404+ mapping , ok := mappings [interfaceName ]
405+ if ! ok {
406+ return nil , fmt .Errorf ("interface %q not found" , interfaceName )
407+ }
408+
409+ return mapping , nil
410+ }
411+
338412// DumpPortMappings retrieves port mappings (ofport and MAC address) for all
339413// interfaces from the specified bridge. The output is parsed from 'ovs-ofctl show' command.
340414func (o * OpenFlowService ) DumpPortMappings (bridge string ) (map [string ]* PortMapping , error ) {
@@ -347,30 +421,37 @@ func (o *OpenFlowService) DumpPortMappings(bridge string) (map[string]*PortMappi
347421 }
348422
349423 mappings := make (map [string ]* PortMapping )
350- scanner := bufio .NewScanner (bytes .NewReader (out ))
351-
352- for scanner .Scan () {
353- line := scanner .Bytes ()
354- matches := portMappingPattern .FindSubmatch (line )
355- if len (matches ) != 4 {
356- // Skip lines that don't match the pattern
357- continue
424+ err = parseEachPort (out , showPrefix , func (line []byte ) error {
425+ // Check if line matches port format: " 7(interface1): addr:..."
426+ if ! bytes .Contains (line , []byte ("(" )) || ! bytes .Contains (line , []byte ("): addr:" )) {
427+ // Skip lines that don't match the port format
428+ return nil
358429 }
359430
360- interfaceName := string (matches [2 ])
361- ofport , err := strconv .Atoi (string (matches [1 ]))
362- if err != nil {
363- // Skip malformed ofport numbers
364- continue
431+ // Extract interface name from the line for map key (do this once)
432+ openParen := bytes .IndexByte (line , '(' )
433+ if openParen == - 1 {
434+ return nil
435+ }
436+ closeParen := bytes .IndexByte (line [openParen :], ')' )
437+ if closeParen == - 1 {
438+ return nil
365439 }
440+ closeParen += openParen
441+ interfaceName := string (line [openParen + 1 : closeParen ])
366442
367- mappings [interfaceName ] = & PortMapping {
368- OfPort : ofport ,
369- MACAddress : string (matches [3 ]),
443+ // Parse the PortMapping
444+ pm := new (PortMapping )
445+ if err := pm .UnmarshalText (line ); err != nil {
446+ // Skip malformed lines
447+ return nil
370448 }
371- }
372449
373- if err := scanner .Err (); err != nil {
450+ mappings [interfaceName ] = pm
451+ return nil
452+ })
453+
454+ if err != nil {
374455 return nil , fmt .Errorf ("failed to parse port mappings: %w" , err )
375456 }
376457
@@ -397,10 +478,9 @@ var (
397478 // the output from "ovs-ofctl dump-aggregate"
398479 //dumpAggregatePrefix = []byte("NXST_AGGREGATE reply")
399480
400- // portMappingPattern matches port lines from 'ovs-ofctl show' output.
401- // Pattern matches: " 7(interface1): addr:fe:4f:76:09:88:2b"
402- // or " 65534(LOCAL): addr:fe:4f:76:09:88:2b"
403- portMappingPattern = regexp .MustCompile (`^\s*(\d+)\(([^)]+)\):\s*addr:([a-fA-F0-9:]+)` )
481+ // showPrefix is a sentinel value returned at the beginning of
482+ // the output from 'ovs-ofctl show'.
483+ showPrefix = []byte ("OFPT_FEATURES_REPLY" )
404484)
405485
406486// dumpPorts calls 'ovs-ofctl dump-ports' with the specified arguments and
@@ -555,6 +635,39 @@ func parseEach(in []byte, prefix []byte, fn func(b []byte) error) error {
555635 return scanner .Err ()
556636}
557637
638+ // parseEachPort parses ovs-ofctl show output from the input buffer, ensuring it has the
639+ // specified prefix, and invoking the input function on each line scanned.
640+ func parseEachPort (in []byte , prefix []byte , fn func (b []byte ) error ) error {
641+ // Handle empty input - return nil to allow empty mappings (matches test expectations)
642+ if len (in ) == 0 {
643+ return nil
644+ }
645+
646+ // First line must not be empty
647+ scanner := bufio .NewScanner (bytes .NewReader (in ))
648+ scanner .Split (bufio .ScanLines )
649+ if ! scanner .Scan () {
650+ return io .ErrUnexpectedEOF
651+ }
652+
653+ // First line must contain the prefix returned by OVS
654+ if ! bytes .Contains (scanner .Bytes (), prefix ) {
655+ return io .ErrUnexpectedEOF
656+ }
657+
658+ // Scan every line to retrieve information needed to unmarshal
659+ // a single PortMapping struct.
660+ for scanner .Scan () {
661+ b := make ([]byte , len (scanner .Bytes ()))
662+ copy (b , scanner .Bytes ())
663+ if err := fn (b ); err != nil {
664+ return err
665+ }
666+ }
667+
668+ return scanner .Err ()
669+ }
670+
558671// exec executes an ExecFunc using 'ovs-ofctl'.
559672func (o * OpenFlowService ) exec (args ... string ) ([]byte , error ) {
560673 return o .c .exec ("ovs-ofctl" , args ... )
0 commit comments