Skip to content

Commit f2dc1f6

Browse files
committed
Add DumpPortMappings API to retrieve ofport and MAC address for tap interfaces
1 parent 045aa1d commit f2dc1f6

File tree

2 files changed

+270
-0
lines changed

2 files changed

+270
-0
lines changed

ovs/openflow.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ import (
2121
"errors"
2222
"fmt"
2323
"io"
24+
"regexp"
25+
"strconv"
26+
"strings"
2427
)
2528

2629
var (
@@ -69,6 +72,15 @@ type flowDirective struct {
6972
flow string
7073
}
7174

75+
// PortMapping contains the ofport number and MAC address for an interface.
76+
type PortMapping struct {
77+
// OfPort specifies the OpenFlow port number.
78+
OfPort int
79+
80+
// MACAddress specifies the MAC address of the interface.
81+
MACAddress string
82+
}
83+
7284
// Possible flowDirective directive values.
7385
const (
7486
dirAdd = "add"
@@ -324,6 +336,54 @@ func (o *OpenFlowService) DumpAggregate(bridge string, flow *MatchFlow) (*FlowSt
324336
return stats, nil
325337
}
326338

339+
// DumpPortMappings retrieves port mappings (ofport and MAC address) for all
340+
// tap interfaces and the LOCAL port from the specified bridge. The output is
341+
// parsed from 'ovs-ofctl show' command.
342+
func (o *OpenFlowService) DumpPortMappings(bridge string) (map[string]*PortMapping, error) {
343+
args := []string{"show", bridge}
344+
args = append(args, o.c.ofctlFlags...)
345+
346+
out, err := o.exec(args...)
347+
if err != nil {
348+
return nil, err
349+
}
350+
351+
mappings := make(map[string]*PortMapping)
352+
scanner := bufio.NewScanner(bytes.NewReader(out))
353+
354+
for scanner.Scan() {
355+
line := scanner.Bytes()
356+
matches := portMappingPattern.FindSubmatch(line)
357+
if len(matches) != 4 {
358+
// Skip lines that don't match the pattern
359+
continue
360+
}
361+
362+
interfaceName := string(matches[2])
363+
// Filter: only include interfaces starting with "tap" or "LOCAL"
364+
if !strings.HasPrefix(interfaceName, "tap") && interfaceName != "LOCAL" {
365+
continue
366+
}
367+
368+
ofport, err := strconv.Atoi(string(matches[1]))
369+
if err != nil {
370+
// Skip malformed ofport numbers
371+
continue
372+
}
373+
374+
mappings[interfaceName] = &PortMapping{
375+
OfPort: ofport,
376+
MACAddress: string(matches[3]),
377+
}
378+
}
379+
380+
if err := scanner.Err(); err != nil {
381+
return nil, fmt.Errorf("failed to parse port mappings: %w", err)
382+
}
383+
384+
return mappings, nil
385+
}
386+
327387
var (
328388
// dumpPortsPrefix is a sentinel value returned at the beginning of
329389
// the output from 'ovs-ofctl dump-ports'.
@@ -343,6 +403,11 @@ var (
343403
// dumpAggregatePrefix is a sentinel value returned at the beginning of
344404
// the output from "ovs-ofctl dump-aggregate"
345405
//dumpAggregatePrefix = []byte("NXST_AGGREGATE reply")
406+
407+
// portMappingPattern matches port lines from 'ovs-ofctl show' output.
408+
// Pattern matches: " 7(tapext10396233): addr:fe:4f:76:09:88:2b"
409+
// or " 65534(LOCAL): addr:fe:4f:76:09:88:2b"
410+
portMappingPattern = regexp.MustCompile(`^\s*(\d+)\(([^)]+)\):\s*addr:([a-fA-F0-9:]+)`)
346411
)
347412

348413
// dumpPorts calls 'ovs-ofctl dump-ports' with the specified arguments and

ovs/openflow_test.go

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,3 +1415,208 @@ func mustVerifyFlowBundle(t *testing.T, stdin io.Reader, flows []*Flow, matchFlo
14151415
}
14161416
}
14171417
}
1418+
1419+
func TestClientOpenFlowDumpPortMappingsOK(t *testing.T) {
1420+
want := map[string]*PortMapping{
1421+
"tapext10396233": {
1422+
OfPort: 7,
1423+
MACAddress: "fe:4f:76:09:88:2b",
1424+
},
1425+
"tapext12716181": {
1426+
OfPort: 8,
1427+
MACAddress: "fe:be:7b:0d:53:d8",
1428+
},
1429+
"tapext10864673": {
1430+
OfPort: 9,
1431+
MACAddress: "fe:b6:4c:d5:40:79",
1432+
},
1433+
"tapint10396233": {
1434+
OfPort: 20,
1435+
MACAddress: "fe:cf:a6:90:30:29",
1436+
},
1437+
"LOCAL": {
1438+
OfPort: 65534,
1439+
MACAddress: "fe:74:0f:80:cf:9a",
1440+
},
1441+
}
1442+
1443+
bridge := "br0"
1444+
1445+
c := testClient([]OptionFunc{Timeout(1)}, func(cmd string, args ...string) ([]byte, error) {
1446+
// Verify correct command and arguments passed, including option flags
1447+
if want, got := "ovs-ofctl", cmd; want != got {
1448+
t.Fatalf("incorrect command:\n- want: %v\n- got: %v",
1449+
want, got)
1450+
}
1451+
1452+
wantArgs := []string{"--timeout=1", "show", bridge}
1453+
if want, got := wantArgs, args; !reflect.DeepEqual(want, got) {
1454+
t.Fatalf("incorrect arguments\n- want: %v\n- got: %v",
1455+
want, got)
1456+
}
1457+
1458+
return []byte(`
1459+
OFPT_FEATURES_REPLY (xid=0x2): dpid:0000000000000001
1460+
n_tables:254, n_buffers:256
1461+
capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP
1462+
actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_nw_tos mod_tp_src mod_tp_dst
1463+
7(tapext10396233): addr:fe:4f:76:09:88:2b
1464+
config: 0
1465+
state: 0
1466+
current: 10GB-FD COPPER
1467+
speed: 10000 Mbps now, 0 Mbps max
1468+
8(tapext12716181): addr:fe:be:7b:0d:53:d8
1469+
config: 0
1470+
state: 0
1471+
current: 10GB-FD COPPER
1472+
speed: 10000 Mbps now, 0 Mbps max
1473+
9(tapext10864673): addr:fe:b6:4c:d5:40:79
1474+
config: 0
1475+
state: 0
1476+
current: 10GB-FD COPPER
1477+
speed: 10000 Mbps now, 0 Mbps max
1478+
20(tapint10396233): addr:fe:cf:a6:90:30:29
1479+
config: 0
1480+
state: 0
1481+
current: 10GB-FD COPPER
1482+
speed: 10000 Mbps now, 0 Mbps max
1483+
65534(LOCAL): addr:fe:74:0f:80:cf:9a
1484+
config: 0
1485+
state: 0
1486+
current: 10GB-FD COPPER
1487+
speed: 10000 Mbps now, 0 Mbps max
1488+
1(non-tap-interface): addr:aa:bb:cc:dd:ee:ff
1489+
config: 0
1490+
state: 0
1491+
`), nil
1492+
})
1493+
1494+
got, err := c.OpenFlow.DumpPortMappings(bridge)
1495+
if err != nil {
1496+
t.Fatalf("unexpected error: %v", err)
1497+
}
1498+
1499+
if len(want) != len(got) {
1500+
t.Fatalf("unexpected number of mappings:\n- want: %d\n- got: %d",
1501+
len(want), len(got))
1502+
}
1503+
1504+
for name, wantMapping := range want {
1505+
gotMapping, ok := got[name]
1506+
if !ok {
1507+
t.Fatalf("missing mapping for interface %q", name)
1508+
}
1509+
1510+
if wantMapping.OfPort != gotMapping.OfPort {
1511+
t.Fatalf("unexpected OfPort for %q:\n- want: %d\n- got: %d",
1512+
name, wantMapping.OfPort, gotMapping.OfPort)
1513+
}
1514+
1515+
if wantMapping.MACAddress != gotMapping.MACAddress {
1516+
t.Fatalf("unexpected MACAddress for %q:\n- want: %q\n- got: %q",
1517+
name, wantMapping.MACAddress, gotMapping.MACAddress)
1518+
}
1519+
}
1520+
1521+
// Verify non-tap interfaces are filtered out
1522+
if _, ok := got["non-tap-interface"]; ok {
1523+
t.Fatalf("non-tap interface should be filtered out")
1524+
}
1525+
}
1526+
1527+
func TestClientOpenFlowDumpPortMappingsEmptyOutput(t *testing.T) {
1528+
bridge := "br0"
1529+
1530+
c := testClient(nil, func(cmd string, args ...string) ([]byte, error) {
1531+
return []byte(""), nil
1532+
})
1533+
1534+
got, err := c.OpenFlow.DumpPortMappings(bridge)
1535+
if err != nil {
1536+
t.Fatalf("unexpected error: %v", err)
1537+
}
1538+
1539+
if len(got) != 0 {
1540+
t.Fatalf("unexpected mappings for empty output:\n- want: 0\n- got: %d", len(got))
1541+
}
1542+
}
1543+
1544+
func TestClientOpenFlowDumpPortMappingsNoTapInterfaces(t *testing.T) {
1545+
bridge := "br0"
1546+
1547+
c := testClient(nil, func(cmd string, args ...string) ([]byte, error) {
1548+
return []byte(`
1549+
OFPT_FEATURES_REPLY (xid=0x2): dpid:0000000000000001
1550+
1(eth0): addr:aa:bb:cc:dd:ee:ff
1551+
2(eth1): addr:11:22:33:44:55:66
1552+
`), nil
1553+
})
1554+
1555+
got, err := c.OpenFlow.DumpPortMappings(bridge)
1556+
if err != nil {
1557+
t.Fatalf("unexpected error: %v", err)
1558+
}
1559+
1560+
if len(got) != 0 {
1561+
t.Fatalf("unexpected mappings (should be empty):\n- want: 0\n- got: %d", len(got))
1562+
}
1563+
}
1564+
1565+
func TestClientOpenFlowDumpPortMappingsOnlyLOCAL(t *testing.T) {
1566+
want := map[string]*PortMapping{
1567+
"LOCAL": {
1568+
OfPort: 65534,
1569+
MACAddress: "fe:74:0f:80:cf:9a",
1570+
},
1571+
}
1572+
1573+
bridge := "br0"
1574+
1575+
c := testClient(nil, func(cmd string, args ...string) ([]byte, error) {
1576+
return []byte(`
1577+
OFPT_FEATURES_REPLY (xid=0x2): dpid:0000000000000001
1578+
65534(LOCAL): addr:fe:74:0f:80:cf:9a
1579+
`), nil
1580+
})
1581+
1582+
got, err := c.OpenFlow.DumpPortMappings(bridge)
1583+
if err != nil {
1584+
t.Fatalf("unexpected error: %v", err)
1585+
}
1586+
1587+
if len(want) != len(got) {
1588+
t.Fatalf("unexpected number of mappings:\n- want: %d\n- got: %d",
1589+
len(want), len(got))
1590+
}
1591+
1592+
for name, wantMapping := range want {
1593+
gotMapping, ok := got[name]
1594+
if !ok {
1595+
t.Fatalf("missing mapping for interface %q", name)
1596+
}
1597+
1598+
if wantMapping.OfPort != gotMapping.OfPort {
1599+
t.Fatalf("unexpected OfPort for %q:\n- want: %d\n- got: %d",
1600+
name, wantMapping.OfPort, gotMapping.OfPort)
1601+
}
1602+
1603+
if wantMapping.MACAddress != gotMapping.MACAddress {
1604+
t.Fatalf("unexpected MACAddress for %q:\n- want: %q\n- got: %q",
1605+
name, wantMapping.MACAddress, gotMapping.MACAddress)
1606+
}
1607+
}
1608+
}
1609+
1610+
func TestClientOpenFlowDumpPortMappingsCommandError(t *testing.T) {
1611+
bridge := "br0"
1612+
wantErr := errors.New("command failed")
1613+
1614+
c := testClient(nil, func(cmd string, args ...string) ([]byte, error) {
1615+
return nil, wantErr
1616+
})
1617+
1618+
_, err := c.OpenFlow.DumpPortMappings(bridge)
1619+
if err == nil {
1620+
t.Fatalf("expected error, got nil")
1621+
}
1622+
}

0 commit comments

Comments
 (0)