Skip to content

Commit e61e803

Browse files
committed
Fixes:
- Added JSON tags (ofPort, macAddress) to PortMapping struct for consistency - Refactored to use parseEachPort() semantics instead of regex, following parseEach() pattern - Removed unused regexp import and portMappingPattern variable - Updated test to be generic table-driven instead of LOCAL-specific - Added DumpPortMapping (singular) function for single interface lookup - Fixed some error handling consistency, add MAC/ofport validation, and eliminate code duplication
1 parent 7a2a409 commit e61e803

File tree

2 files changed

+294
-55
lines changed

2 files changed

+294
-55
lines changed

ovs/openflow.go

Lines changed: 138 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24-
"regexp"
24+
"net"
2525
"strconv"
26+
"strings"
2627
)
2728

2829
var (
@@ -74,10 +75,67 @@ type flowDirective struct {
7475
// PortMapping contains the ofport number and MAC address for an interface.
7576
type 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.
340414
func (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'.
559672
func (o *OpenFlowService) exec(args ...string) ([]byte, error) {
560673
return o.c.exec("ovs-ofctl", args...)

0 commit comments

Comments
 (0)