Skip to content

Commit b64fc3e

Browse files
committed
Implement stats fetch using dump-flows to provide an option to get statistics
for all matching flows in one shot. Also, provide an option to report interface names instead of port numbers, if requested.
1 parent c0f7d42 commit b64fc3e

File tree

5 files changed

+449
-2
lines changed

5 files changed

+449
-2
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ Neal Shrader <[email protected]>
1515
Sangeetha Srikanth <[email protected]>
1616
Franck Rupin <[email protected]>
1717
Adam Simeth <[email protected]>
18+
Manmeet Singh <[email protected]>

ovs/flow.go

+124
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ type LearnedFlow struct {
8282
Limit int
8383
}
8484

85+
// A PerFlowStats is meant for fetching FlowStats per flow
86+
type PerFlowStats struct {
87+
Protocol Protocol
88+
InPort int
89+
Table int
90+
Cookie uint64
91+
IfName string
92+
Stats FlowStats
93+
}
94+
8595
var _ error = &FlowError{}
8696

8797
// A FlowError is an error encountered while marshaling or unmarshaling
@@ -439,6 +449,120 @@ func (f *Flow) UnmarshalText(b []byte) error {
439449
return nil
440450
}
441451

452+
// UnmarshalText unmarshals flows text into a PerFlowStats.
453+
func (f *PerFlowStats) UnmarshalText(b []byte) error {
454+
// Make a copy per documentation for encoding.TextUnmarshaler.
455+
// A string is easier to work with in this case.
456+
s := string(b)
457+
458+
// Must have one and only one actions=... field in the flow.
459+
ss := strings.Split(s, keyActions+"=")
460+
if len(ss) != 2 || ss[1] == "" {
461+
return &FlowError{
462+
Err: errNoActions,
463+
}
464+
}
465+
if len(ss) < 2 {
466+
return &FlowError{
467+
Err: errNotEnoughElements,
468+
}
469+
}
470+
matchers := strings.TrimSpace(ss[0])
471+
472+
// Handle matchers first.
473+
ss = strings.Split(matchers, ",")
474+
for i := 0; i < len(ss); i++ {
475+
if !strings.Contains(ss[i], "=") {
476+
// that means this will be a protocol field.
477+
if ss[i] != "" {
478+
f.Protocol = Protocol(ss[i])
479+
}
480+
continue
481+
}
482+
483+
// All remaining comma-separated values should be in key=value format
484+
kv := strings.Split(ss[i], "=")
485+
if len(kv) != 2 {
486+
continue
487+
}
488+
kv[1] = strings.TrimSpace(kv[1])
489+
490+
switch strings.TrimSpace(kv[0]) {
491+
case cookie:
492+
// Parse cookie into struct field.
493+
cookie, err := strconv.ParseUint(kv[1], 0, 64)
494+
if err != nil {
495+
return &FlowError{
496+
Str: kv[1],
497+
Err: err,
498+
}
499+
}
500+
f.Cookie = cookie
501+
continue
502+
case inPort:
503+
// Parse in_port into struct field.
504+
s := kv[1]
505+
if strings.TrimSpace(s) == portLOCAL {
506+
f.InPort = PortLOCAL
507+
continue
508+
}
509+
// Try to read as integer port numbers first
510+
port, err := strconv.ParseInt(s, 10, 0)
511+
if err != nil {
512+
f.IfName = s
513+
} else {
514+
f.InPort = int(port)
515+
}
516+
continue
517+
case table:
518+
// Parse table into struct field.
519+
table, err := strconv.ParseInt(kv[1], 10, 0)
520+
if err != nil {
521+
return &FlowError{
522+
Str: kv[1],
523+
Err: err,
524+
}
525+
}
526+
f.Table = int(table)
527+
continue
528+
case nPackets:
529+
// Parse nPackets into struct field.
530+
pktCount, err := strconv.ParseUint(kv[1], 0, 64)
531+
if err != nil {
532+
return &FlowError{
533+
Str: kv[1],
534+
Err: err,
535+
}
536+
}
537+
f.Stats.PacketCount = uint64(pktCount)
538+
continue
539+
case nBytes:
540+
// Parse nBytes into struct field.
541+
byteCount, err := strconv.ParseUint(kv[1], 0, 64)
542+
if err != nil {
543+
return &FlowError{
544+
Str: kv[1],
545+
Err: err,
546+
}
547+
}
548+
f.Stats.ByteCount = uint64(byteCount)
549+
continue
550+
case duration, hardAge, idleAge, priority, idleTimeout, keyActions:
551+
// ignore those fields.
552+
continue
553+
}
554+
555+
// All arbitrary key/value pairs that
556+
// don't match the case above.
557+
_, err := parseMatch(kv[0], kv[1])
558+
if err != nil {
559+
return err
560+
}
561+
}
562+
563+
return nil
564+
}
565+
442566
// MatchFlow converts Flow into MatchFlow.
443567
func (f *Flow) MatchFlow() *MatchFlow {
444568
return &MatchFlow{

ovs/flow_test.go

+70
Original file line numberDiff line numberDiff line change
@@ -1313,3 +1313,73 @@ func flowErrorEqual(a error, b error) bool {
13131313

13141314
return reflect.DeepEqual(fa, fb)
13151315
}
1316+
1317+
// perflowstatsEqual determines if two possible PerFlowStats are equal.
1318+
func perflowstatsEqual(a *PerFlowStats, b *PerFlowStats) bool {
1319+
// Special case: both nil is OK
1320+
if a == nil && b == nil {
1321+
return true
1322+
}
1323+
1324+
return reflect.DeepEqual(a, b)
1325+
}
1326+
1327+
func TestPerFlowStatsUnmarshalText(t *testing.T) {
1328+
var tests = []struct {
1329+
desc string
1330+
s string
1331+
f *PerFlowStats
1332+
err error
1333+
}{
1334+
{
1335+
desc: "empty Flow string, need actions fields",
1336+
err: &FlowError{
1337+
Err: errNoActions,
1338+
},
1339+
},
1340+
{
1341+
desc: "Flow string with interface name",
1342+
s: "priority=10,in_port=eth0,table=0,actions=drop",
1343+
f: &PerFlowStats{
1344+
InPort: 0,
1345+
IfName: "eth0",
1346+
Table: 0,
1347+
},
1348+
},
1349+
{
1350+
desc: "Flow string with flow stats",
1351+
s: "n_packets=13256, n_bytes=1287188, priority=10,in_port=eth0,table=0,actions=drop",
1352+
f: &PerFlowStats{
1353+
InPort: 0,
1354+
IfName: "eth0",
1355+
Table: 0,
1356+
Stats: FlowStats{
1357+
PacketCount: 13256,
1358+
ByteCount: 1287188,
1359+
},
1360+
},
1361+
},
1362+
}
1363+
1364+
for _, tt := range tests {
1365+
t.Run(tt.desc, func(t *testing.T) {
1366+
f := new(PerFlowStats)
1367+
err := f.UnmarshalText([]byte(tt.s))
1368+
1369+
// Need temporary strings to avoid nil pointer dereference
1370+
// panics when checking Error method.
1371+
if want, got := tt.err, err; !flowErrorEqual(want, got) {
1372+
t.Fatalf("unexpected error:\n- want: %v\n- got: %v",
1373+
want, got)
1374+
}
1375+
if err != nil {
1376+
return
1377+
}
1378+
1379+
if want, got := tt.f, f; !perflowstatsEqual(want, got) {
1380+
t.Fatalf("unexpected Flow:\n- want: %#v\n- got: %#v",
1381+
want, got)
1382+
}
1383+
})
1384+
}
1385+
}

ovs/openflow.go

+58-2
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ const (
7575
dirDelete = "delete"
7676
)
7777

78+
// Interface names option
79+
const (
80+
interfaceNamesOption = "--names"
81+
)
82+
7883
// Add pushes zero or more Flows on to the transaction, to be added by
7984
// Open vSwitch. If any of the flows are invalid, Add becomes a no-op
8085
// and the error will be surfaced when Commit is called.
@@ -267,7 +272,7 @@ func (o *OpenFlowService) DumpTables(bridge string) ([]*Table, error) {
267272
return tables, err
268273
}
269274

270-
// DumpFlowsWithFlowArgs retrieves statistics about all flows for the specified bridge,
275+
// DumpFlowsWithFlowArgs retrieves details about all flows for the specified bridge,
271276
// filtering on the specified flow(s), if provided.
272277
// If a table has no active flows and has not been used for a lookup or matched
273278
// by an incoming packet, it is filtered from the output.
@@ -306,13 +311,64 @@ func (o *OpenFlowService) DumpFlowsWithFlowArgs(bridge string, flow *MatchFlow)
306311
return flows, err
307312
}
308313

309-
// DumpFlows retrieves statistics about all flows for the specified bridge.
314+
// DumpFlows retrieves details about all flows for the specified bridge.
310315
// If a table has no active flows and has not been used for a lookup or matched
311316
// by an incoming packet, it is filtered from the output.
312317
func (o *OpenFlowService) DumpFlows(bridge string) ([]*Flow, error) {
313318
return o.DumpFlowsWithFlowArgs(bridge, nil)
314319
}
315320

321+
// DumpFlowStatsWithFlowArgs retrieves statistics about all flows for the specified bridge,
322+
// filtering on the specified flow(s), if provided.
323+
// If a table has no active flows and has not been used for a lookup or matched
324+
// by an incoming packet, it is filtered from the output.
325+
// We neeed to add a Matchflow to filter the dumpflow results. For example filter based on table, cookie.
326+
// Report with interface names if useInterfaceNames is set. Port numbers otherwise
327+
func (o *OpenFlowService) DumpFlowStatsWithFlowArgs(bridge string, flow *MatchFlow, useInterfaceNames bool) ([]*PerFlowStats, error) {
328+
args := []string{"dump-flows", bridge}
329+
if useInterfaceNames {
330+
args = append(args, interfaceNamesOption)
331+
}
332+
args = append(args, o.c.ofctlFlags...)
333+
if flow != nil {
334+
fb, err := flow.MarshalText()
335+
if err != nil {
336+
return nil, err
337+
}
338+
args = append(args, string(fb))
339+
}
340+
out, err := o.exec(args...)
341+
if err != nil {
342+
return nil, err
343+
}
344+
345+
var flows []*PerFlowStats
346+
err = parseEachLine(out, dumpFlowsPrefix, func(b []byte) error {
347+
// Do not attempt to parse ST_FLOW messages.
348+
if bytes.Contains(b, dumpFlowsPrefix) {
349+
return nil
350+
}
351+
352+
f := new(PerFlowStats)
353+
if err := f.UnmarshalText(b); err != nil {
354+
return err
355+
}
356+
357+
flows = append(flows, f)
358+
return nil
359+
})
360+
361+
return flows, err
362+
}
363+
364+
// DumpFlowStats retrieves statistics about all matching flows for the specified bridge.
365+
// If a table has no active flows and has not been used for a lookup or matched
366+
// by an incoming packet, it is filtered from the output.
367+
// Use nil MatchFlow if no filtering is desired.
368+
func (o *OpenFlowService) DumpFlowStats(bridge string, flow *MatchFlow, useInterfaceNames bool) ([]*PerFlowStats, error) {
369+
return o.DumpFlowStatsWithFlowArgs(bridge, flow, useInterfaceNames)
370+
}
371+
316372
// DumpAggregate retrieves statistics about the specified flow attached to the
317373
// specified bridge.
318374
func (o *OpenFlowService) DumpAggregate(bridge string, flow *MatchFlow) (*FlowStats, error) {

0 commit comments

Comments
 (0)