stats: golang vpp_if_stats_client
see README for details
Change-Id: Ida603ccaee21dabc903512699b5b355cebb70320
Signed-off-by: Koren Lev <korenlev@gmail.com>
diff --git a/extras/vpp_if_stats/README.md b/extras/vpp_if_stats/README.md
new file mode 100755
index 0000000..7185d6c
--- /dev/null
+++ b/extras/vpp_if_stats/README.md
@@ -0,0 +1,35 @@
+# VPP interface stats client
+
+This is a source code and a binary of a 'thin client' to collect,
+aggregate and expose VPP interface stats through VPP stats socket API.
+It also provides some information about the installed VPP version.
+
+This can be used by monitoring systems that needs to grab those details
+through a simple executable client with no dependencies.
+
+example use case: where VPP runs in a container that can't expose the socket API to the host level
+
+
+## Prerequisites (for building)
+
+**GoVPP** library (compatible with VPP 18.10)
+vpp, vpp-api, vpp-lib
+
+## Building
+
+```bash
+go get git.fd.io/govpp.git
+go build
+```
+
+## Using (post-build for example on linux 64bit)
+
+```bash
+./bin/vpp_if_stats_linux_amd64
+```
+
+## Output examples
+
+[JSON schema](./response_schema.json)
+[Example](./response_example.json)
+
diff --git a/extras/vpp_if_stats/apimock.go b/extras/vpp_if_stats/apimock.go
new file mode 100755
index 0000000..7736334
--- /dev/null
+++ b/extras/vpp_if_stats/apimock.go
@@ -0,0 +1,92 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.fd.io/govpp.git/api (Interfaces: Channel)
+
+// Package mock_api is a generated GoMock package.
+package main
+
+import (
+ api "git.fd.io/govpp.git/api"
+ gomock "github.com/golang/mock/gomock"
+ reflect "reflect"
+ time "time"
+)
+
+// MockChannel is a mock of Channel interface
+type MockChannel struct {
+ ctrl *gomock.Controller
+ recorder *MockChannelMockRecorder
+}
+
+// MockChannelMockRecorder is the mock recorder for MockChannel
+type MockChannelMockRecorder struct {
+ mock *MockChannel
+}
+
+// NewMockChannel creates a new mock instance
+func NewMockChannel(ctrl *gomock.Controller) *MockChannel {
+ mock := &MockChannel{ctrl: ctrl}
+ mock.recorder = &MockChannelMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockChannel) EXPECT() *MockChannelMockRecorder {
+ return m.recorder
+}
+
+// Close mocks base method
+func (m *MockChannel) Close() {
+ m.ctrl.Call(m, "Close")
+}
+
+// Close indicates an expected call of Close
+func (mr *MockChannelMockRecorder) Close() *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockChannel)(nil).Close))
+}
+
+// SendMultiRequest mocks base method
+func (m *MockChannel) SendMultiRequest(arg0 api.Message) api.MultiRequestCtx {
+ ret := m.ctrl.Call(m, "SendMultiRequest", arg0)
+ ret0, _ := ret[0].(api.MultiRequestCtx)
+ return ret0
+}
+
+// SendMultiRequest indicates an expected call of SendMultiRequest
+func (mr *MockChannelMockRecorder) SendMultiRequest(arg0 interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendMultiRequest", reflect.TypeOf((*MockChannel)(nil).SendMultiRequest), arg0)
+}
+
+// SendRequest mocks base method
+func (m *MockChannel) SendRequest(arg0 api.Message) api.RequestCtx {
+ ret := m.ctrl.Call(m, "SendRequest", arg0)
+ ret0, _ := ret[0].(api.RequestCtx)
+ return ret0
+}
+
+// SendRequest indicates an expected call of SendRequest
+func (mr *MockChannelMockRecorder) SendRequest(arg0 interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SendRequest", reflect.TypeOf((*MockChannel)(nil).SendRequest), arg0)
+}
+
+// SetReplyTimeout mocks base method
+func (m *MockChannel) SetReplyTimeout(arg0 time.Duration) {
+ m.ctrl.Call(m, "SetReplyTimeout", arg0)
+}
+
+// SetReplyTimeout indicates an expected call of SetReplyTimeout
+func (mr *MockChannelMockRecorder) SetReplyTimeout(arg0 interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReplyTimeout", reflect.TypeOf((*MockChannel)(nil).SetReplyTimeout), arg0)
+}
+
+// SubscribeNotification mocks base method
+func (m *MockChannel) SubscribeNotification(arg0 chan api.Message, arg1 api.Message) (api.SubscriptionCtx, error) {
+ ret := m.ctrl.Call(m, "SubscribeNotification", arg0, arg1)
+ ret0, _ := ret[0].(api.SubscriptionCtx)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// SubscribeNotification indicates an expected call of SubscribeNotification
+func (mr *MockChannelMockRecorder) SubscribeNotification(arg0, arg1 interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubscribeNotification", reflect.TypeOf((*MockChannel)(nil).SubscribeNotification), arg0, arg1)
+}
diff --git a/extras/vpp_if_stats/json_structs.go b/extras/vpp_if_stats/json_structs.go
new file mode 100755
index 0000000..a42b6d8
--- /dev/null
+++ b/extras/vpp_if_stats/json_structs.go
@@ -0,0 +1,93 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "git.fd.io/govpp.git/examples/bin_api/vpe"
+)
+
+type jsonVppDetails struct {
+ Program string `json:"program"`
+ Version string `json:"version"`
+ BuildDate string `json:"build_date"`
+ BuildDirectory string `json:"build_directory"`
+}
+
+type jsonVppInterface struct {
+ Index uint32 `json:"if_index"`
+ Name string `json:"if_name"`
+ Tag string `json:"if_tag"`
+ MacAddress string `json:"if_mac"`
+ AdminState uint8 `json:"if_admin_state"`
+ LinkState uint8 `json:"if_link_state"`
+ LinkMTU uint16 `json:"if_link_mtu"`
+ SubDot1ad uint8 `json:"if_sub_dot1ad"`
+ SubID uint32 `json:"if_sub_id"`
+
+ TxBytes uint64 `json:"if_tx_bytes"`
+ TxPackets uint64 `json:"if_tx_packets"`
+ TxErrors uint64 `json:"if_tx_errors"`
+ RxBytes uint64 `json:"if_rx_bytes"`
+ RxPackets uint64 `json:"if_rx_packets"`
+ RxErrors uint64 `json:"if_rx_errors"`
+ Drops uint64 `json:"if_drops"`
+ Punts uint64 `json:"if_punts"`
+}
+
+type jsonVppPayload struct {
+ *jsonVppDetails `json:"vpp_details"`
+ Interfaces []*jsonVppInterface `json:"interfaces"`
+}
+
+func bytesToString(b []byte) string {
+ return string(bytes.Split(b, []byte{0})[0])
+}
+
+func toJSONVppDetails(svReply *vpe.ShowVersionReply) *jsonVppDetails {
+ return &jsonVppDetails{
+ Program: bytesToString(svReply.Program),
+ Version: bytesToString(svReply.Version),
+ BuildDate: bytesToString(svReply.BuildDate),
+ BuildDirectory: bytesToString(svReply.BuildDirectory),
+ }
+}
+
+func toJSONVppInterface(vppIf *vppInterface) *jsonVppInterface {
+ return &jsonVppInterface{
+ Index: vppIf.SwIfIndex,
+ Name: bytesToString(vppIf.InterfaceName),
+ Tag: bytesToString(vppIf.Tag),
+ MacAddress: parseMacAddress(vppIf.L2Address, vppIf.L2AddressLength),
+ AdminState: vppIf.AdminUpDown,
+ LinkState: vppIf.LinkUpDown,
+ LinkMTU: vppIf.LinkMtu,
+ SubDot1ad: vppIf.SubDot1ad,
+ SubID: vppIf.SubID,
+ TxBytes: vppIf.Stats.TxBytes,
+ TxPackets: vppIf.Stats.TxPackets,
+ TxErrors: vppIf.Stats.TxErrors,
+ RxBytes: vppIf.Stats.RxBytes,
+ RxPackets: vppIf.Stats.RxPackets,
+ RxErrors: vppIf.Stats.RxErrors,
+ Drops: vppIf.Stats.Drops,
+ Punts: vppIf.Stats.Punts,
+ }
+}
+
+func toJSONVppPayload(svReply *vpe.ShowVersionReply, vppIfs []*vppInterface) *jsonVppPayload {
+ p := &jsonVppPayload{jsonVppDetails: toJSONVppDetails(svReply), Interfaces: make([]*jsonVppInterface, len(vppIfs))}
+ for index, vppIf := range vppIfs {
+ p.Interfaces[index] = toJSONVppInterface(vppIf)
+ }
+ return p
+}
+
+func dumpToJSONString(v *vppConnector) (string, error) {
+ payload := toJSONVppPayload(&v.VppDetails, v.Interfaces)
+ jsonBytes, err := json.Marshal(payload)
+ if err != nil {
+ return "", fmt.Errorf("failed to dump to json: %v", err)
+ }
+ return string(jsonBytes), nil
+}
diff --git a/extras/vpp_if_stats/response_example.json b/extras/vpp_if_stats/response_example.json
new file mode 100755
index 0000000..ed9f933
--- /dev/null
+++ b/extras/vpp_if_stats/response_example.json
@@ -0,0 +1,200 @@
+{
+ "vpp_details": {
+ "program": "vpe",
+ "version": "18.10-release",
+ "build_date": "Tue Oct 23 07:03:38 UTC 2018",
+ "build_directory": "/w/workspace/vpp-merge-1810-centos7"
+ },
+ "interfaces": [
+ {
+ "if_index": 0,
+ "if_name": "local0",
+ "if_tag": "",
+ "if_mac": "",
+ "if_admin_state": 0,
+ "if_link_state": 0,
+ "if_link_mtu": 0,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ },
+ {
+ "if_index": 1,
+ "if_name": "TenGigabitEthernet5e/0/2",
+ "if_tag": "",
+ "if_mac": "11:33:55:77:99:aa",
+ "if_admin_state": 1,
+ "if_link_state": 1,
+ "if_link_mtu": 9202,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 5024976,
+ "if_tx_packets": 40524,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 200094228,
+ "if_rx_packets": 1685702,
+ "if_rx_errors": 0,
+ "if_drops": 1214356,
+ "if_punts": 0
+ },
+ {
+ "if_index": 2,
+ "if_name": "TenGigabitEthernet5e/0/3",
+ "if_tag": "",
+ "if_mac": "11:33:55:77:99:aa",
+ "if_admin_state": 1,
+ "if_link_state": 1,
+ "if_link_mtu": 9202,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 5024976,
+ "if_tx_packets": 40524,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 233044788,
+ "if_rx_packets": 2257762,
+ "if_rx_errors": 0,
+ "if_drops": 1214348,
+ "if_punts": 0
+ },
+ {
+ "if_index": 3,
+ "if_name": "BondEthernet0",
+ "if_tag": "net-vpp.physnet:physnet1",
+ "if_mac": "11:33:55:77:99:bb",
+ "if_admin_state": 1,
+ "if_link_state": 1,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 1514852,
+ "if_punts": 0
+ },
+ {
+ "if_index": 4,
+ "if_name": "BondEthernet0.549",
+ "if_tag": "net-vpp.uplink:physnet1.vlan.549",
+ "if_mac": "",
+ "if_admin_state": 1,
+ "if_link_state": 1,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 549,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 1968,
+ "if_rx_packets": 26,
+ "if_rx_errors": 0,
+ "if_drops": 78,
+ "if_punts": 0
+ },
+ {
+ "if_index": 5,
+ "if_name": "VirtualEthernet0/0/0",
+ "if_tag": "net-vpp.port:fb9b1ce8-f643-45be-9298-ccd18f9018c8",
+ "if_mac": "dd:ff:11:33:55:77",
+ "if_admin_state": 1,
+ "if_link_state": 0,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 26,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ },
+ {
+ "if_index": 6,
+ "if_name": "BondEthernet0.529",
+ "if_tag": "net-vpp.uplink:physnet1.vlan.529",
+ "if_mac": "",
+ "if_admin_state": 1,
+ "if_link_state": 1,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 529,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ },
+ {
+ "if_index": 7,
+ "if_name": "VirtualEthernet0/0/1",
+ "if_tag": "net-vpp.port:bc726b1c-526e-4a8d-9f9a-c19b5dfe2b28",
+ "if_mac": "22:44:66:88:aa:cc",
+ "if_admin_state": 1,
+ "if_link_state": 0,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 0,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ },
+ {
+ "if_index": 8,
+ "if_name": "VirtualEthernet0/0/2",
+ "if_tag": "net-vpp.port:aaabbbcc-a86d-4eb5-a3bc-aaabbbcccddd",
+ "if_mac": "12:34:56:78:9a:bc",
+ "if_admin_state": 1,
+ "if_link_state": 0,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 26,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ },
+ {
+ "if_index": 9,
+ "if_name": "VirtualEthernet0/0/3",
+ "if_tag": "net-vpp.port:dddeeeff-838e-4995-9bd2-eeefff000111",
+ "if_mac": "fe:dc:ba:98:76:54",
+ "if_admin_state": 1,
+ "if_link_state": 0,
+ "if_link_mtu": 9216,
+ "if_sub_dot1ad": 0,
+ "if_sub_id": 0,
+ "if_tx_bytes": 0,
+ "if_tx_packets": 0,
+ "if_tx_errors": 26,
+ "if_rx_bytes": 0,
+ "if_rx_packets": 0,
+ "if_rx_errors": 0,
+ "if_drops": 0,
+ "if_punts": 0
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extras/vpp_if_stats/response_schema.json b/extras/vpp_if_stats/response_schema.json
new file mode 100755
index 0000000..aa5f948
--- /dev/null
+++ b/extras/vpp_if_stats/response_schema.json
@@ -0,0 +1,253 @@
+{
+ "definitions": {},
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "$id": "http://example.com/root.json",
+ "type": "object",
+ "title": "The Root Schema",
+ "required": [
+ "vpp_details",
+ "interfaces"
+ ],
+ "properties": {
+ "vpp_details": {
+ "$id": "#/properties/vpp_details",
+ "type": "object",
+ "title": "The Vpp_details Schema",
+ "required": [
+ "program",
+ "version",
+ "build_date",
+ "build_directory"
+ ],
+ "properties": {
+ "program": {
+ "$id": "#/properties/vpp_details/properties/program",
+ "type": "string",
+ "title": "The Program Schema",
+ "default": "",
+ "examples": [
+ "vpe"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "version": {
+ "$id": "#/properties/vpp_details/properties/version",
+ "type": "string",
+ "title": "The Version Schema",
+ "default": "",
+ "examples": [
+ "18.10-release"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "build_date": {
+ "$id": "#/properties/vpp_details/properties/build_date",
+ "type": "string",
+ "title": "The Build_date Schema",
+ "default": "",
+ "examples": [
+ "Tue Oct 23 07:03:38 UTC 2018"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "build_directory": {
+ "$id": "#/properties/vpp_details/properties/build_directory",
+ "type": "string",
+ "title": "The Build_directory Schema",
+ "default": "",
+ "examples": [
+ "/w/workspace/vpp-merge-1810-centos7"
+ ],
+ "pattern": "^(.*)$"
+ }
+ }
+ },
+ "interfaces": {
+ "$id": "#/properties/interfaces",
+ "type": "array",
+ "title": "The Interfaces Schema",
+ "items": {
+ "$id": "#/properties/interfaces/items",
+ "type": "object",
+ "title": "The Items Schema",
+ "required": [
+ "if_index",
+ "if_name",
+ "if_tag",
+ "if_mac",
+ "if_admin_state",
+ "if_link_state",
+ "if_link_mtu",
+ "if_sub_dot1ad",
+ "if_sub_id",
+ "if_tx_bytes",
+ "if_tx_packets",
+ "if_tx_errors",
+ "if_rx_bytes",
+ "if_rx_packets",
+ "if_rx_errors",
+ "if_drops",
+ "if_punts"
+ ],
+ "properties": {
+ "if_index": {
+ "$id": "#/properties/interfaces/items/properties/if_index",
+ "type": "integer",
+ "title": "The If_index Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_name": {
+ "$id": "#/properties/interfaces/items/properties/if_name",
+ "type": "string",
+ "title": "The If_name Schema",
+ "default": "",
+ "examples": [
+ "local0"
+ ],
+ "pattern": "^(.*)$"
+ },
+ "if_tag": {
+ "$id": "#/properties/interfaces/items/properties/if_tag",
+ "type": "string",
+ "title": "The If_tag Schema",
+ "default": "",
+ "examples": [
+ ""
+ ],
+ "pattern": "^(.*)$"
+ },
+ "if_mac": {
+ "$id": "#/properties/interfaces/items/properties/if_mac",
+ "type": "string",
+ "title": "The If_mac Schema",
+ "default": "",
+ "examples": [
+ ""
+ ],
+ "pattern": "^(.*)$"
+ },
+ "if_admin_state": {
+ "$id": "#/properties/interfaces/items/properties/if_admin_state",
+ "type": "integer",
+ "title": "The If_admin_state Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_link_state": {
+ "$id": "#/properties/interfaces/items/properties/if_link_state",
+ "type": "integer",
+ "title": "The If_link_state Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_link_mtu": {
+ "$id": "#/properties/interfaces/items/properties/if_link_mtu",
+ "type": "integer",
+ "title": "The If_link_mtu Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_sub_dot1ad": {
+ "$id": "#/properties/interfaces/items/properties/if_sub_dot1ad",
+ "type": "integer",
+ "title": "The If_sub_dot1ad Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_sub_id": {
+ "$id": "#/properties/interfaces/items/properties/if_sub_id",
+ "type": "integer",
+ "title": "The If_sub_id Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_tx_bytes": {
+ "$id": "#/properties/interfaces/items/properties/if_tx_bytes",
+ "type": "integer",
+ "title": "The If_tx_bytes Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_tx_packets": {
+ "$id": "#/properties/interfaces/items/properties/if_tx_packets",
+ "type": "integer",
+ "title": "The If_tx_packets Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_tx_errors": {
+ "$id": "#/properties/interfaces/items/properties/if_tx_errors",
+ "type": "integer",
+ "title": "The If_tx_errors Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_rx_bytes": {
+ "$id": "#/properties/interfaces/items/properties/if_rx_bytes",
+ "type": "integer",
+ "title": "The If_rx_bytes Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_rx_packets": {
+ "$id": "#/properties/interfaces/items/properties/if_rx_packets",
+ "type": "integer",
+ "title": "The If_rx_packets Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_rx_errors": {
+ "$id": "#/properties/interfaces/items/properties/if_rx_errors",
+ "type": "integer",
+ "title": "The If_rx_errors Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_drops": {
+ "$id": "#/properties/interfaces/items/properties/if_drops",
+ "type": "integer",
+ "title": "The If_drops Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ },
+ "if_punts": {
+ "$id": "#/properties/interfaces/items/properties/if_punts",
+ "type": "integer",
+ "title": "The If_punts Schema",
+ "default": 0,
+ "examples": [
+ 0
+ ]
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/extras/vpp_if_stats/statsmock.go b/extras/vpp_if_stats/statsmock.go
new file mode 100755
index 0000000..72528f5
--- /dev/null
+++ b/extras/vpp_if_stats/statsmock.go
@@ -0,0 +1,92 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: git.fd.io/govpp.git/adapter (interfaces: StatsAPI)
+
+// Package mock_adapter is a generated GoMock package.
+package main
+
+import (
+ adapter "git.fd.io/govpp.git/adapter"
+ gomock "github.com/golang/mock/gomock"
+ reflect "reflect"
+)
+
+// MockStatsAPI is a mock of StatsAPI interface
+type MockStatsAPI struct {
+ ctrl *gomock.Controller
+ recorder *MockStatsAPIMockRecorder
+}
+
+// MockStatsAPIMockRecorder is the mock recorder for MockStatsAPI
+type MockStatsAPIMockRecorder struct {
+ mock *MockStatsAPI
+}
+
+// NewMockStatsAPI creates a new mock instance
+func NewMockStatsAPI(ctrl *gomock.Controller) *MockStatsAPI {
+ mock := &MockStatsAPI{ctrl: ctrl}
+ mock.recorder = &MockStatsAPIMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use
+func (m *MockStatsAPI) EXPECT() *MockStatsAPIMockRecorder {
+ return m.recorder
+}
+
+// Connect mocks base method
+func (m *MockStatsAPI) Connect() error {
+ ret := m.ctrl.Call(m, "Connect")
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Connect indicates an expected call of Connect
+func (mr *MockStatsAPIMockRecorder) Connect() *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockStatsAPI)(nil).Connect))
+}
+
+// Disconnect mocks base method
+func (m *MockStatsAPI) Disconnect() error {
+ ret := m.ctrl.Call(m, "Disconnect")
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// Disconnect indicates an expected call of Disconnect
+func (mr *MockStatsAPIMockRecorder) Disconnect() *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Disconnect", reflect.TypeOf((*MockStatsAPI)(nil).Disconnect))
+}
+
+// DumpStats mocks base method
+func (m *MockStatsAPI) DumpStats(arg0 ...string) ([]*adapter.StatEntry, error) {
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "DumpStats", varargs...)
+ ret0, _ := ret[0].([]*adapter.StatEntry)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// DumpStats indicates an expected call of DumpStats
+func (mr *MockStatsAPIMockRecorder) DumpStats(arg0 ...interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DumpStats", reflect.TypeOf((*MockStatsAPI)(nil).DumpStats), arg0...)
+}
+
+// ListStats mocks base method
+func (m *MockStatsAPI) ListStats(arg0 ...string) ([]string, error) {
+ varargs := []interface{}{}
+ for _, a := range arg0 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ListStats", varargs...)
+ ret0, _ := ret[0].([]string)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ListStats indicates an expected call of ListStats
+func (mr *MockStatsAPIMockRecorder) ListStats(arg0 ...interface{}) *gomock.Call {
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListStats", reflect.TypeOf((*MockStatsAPI)(nil).ListStats), arg0...)
+}
diff --git a/extras/vpp_if_stats/vpp_if_stats.go b/extras/vpp_if_stats/vpp_if_stats.go
new file mode 100755
index 0000000..48ce085
--- /dev/null
+++ b/extras/vpp_if_stats/vpp_if_stats.go
@@ -0,0 +1,223 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "git.fd.io/govpp.git"
+ "git.fd.io/govpp.git/adapter"
+ "git.fd.io/govpp.git/adapter/vppapiclient"
+ "git.fd.io/govpp.git/api"
+ "git.fd.io/govpp.git/core"
+ "git.fd.io/govpp.git/examples/bin_api/interfaces"
+ "git.fd.io/govpp.git/examples/bin_api/vpe"
+ "log"
+)
+
+//////////////////////////////////////
+///////// Data structs ///////////
+//////////////////////////////////////
+
+const defaultStatsSocketPath = "/run/vpp/stats.sock"
+const defaultShmPrefix = ""
+
+func parseMacAddress(l2Address []byte, l2AddressLength uint32) string {
+ var mac string
+ for i := uint32(0); i < l2AddressLength; i++ {
+ mac += fmt.Sprintf("%02x", l2Address[i])
+ if i < l2AddressLength-1 {
+ mac += ":"
+ }
+ }
+ return mac
+}
+
+type interfaceStats struct {
+ TxBytes uint64
+ TxPackets uint64
+ TxErrors uint64
+ RxBytes uint64
+ RxPackets uint64
+ RxErrors uint64
+ Drops uint64
+ Punts uint64
+}
+
+type vppInterface struct {
+ interfaces.SwInterfaceDetails
+ Stats interfaceStats
+}
+
+type vppConnector struct {
+ statsSocketPath string
+ shmPrefix string
+
+ conn *core.Connection
+ api api.Channel
+ stats adapter.StatsAPI
+
+ VppDetails vpe.ShowVersionReply
+ Interfaces []*vppInterface
+}
+
+//////////////////////////////////////
+///////// VPP workflow ///////////
+//////////////////////////////////////
+
+func (v *vppConnector) getVppVersion() error {
+ if err := v.api.SendRequest(&vpe.ShowVersion{}).ReceiveReply(&v.VppDetails); err != nil {
+ return fmt.Errorf("failed to fetch vpp version: %v", err)
+ }
+ return nil
+}
+
+func (v *vppConnector) getInterfaces() error {
+ ifCtx := v.api.SendMultiRequest(&interfaces.SwInterfaceDump{})
+ for {
+ ifDetails := interfaces.SwInterfaceDetails{}
+ stop, err := ifCtx.ReceiveReply(&ifDetails)
+ if err != nil {
+ return fmt.Errorf("failed to fetch vpp interface: %v", err)
+ }
+ if stop {
+ break
+ }
+
+ v.Interfaces = append(v.Interfaces, &vppInterface{SwInterfaceDetails: ifDetails})
+ }
+ return nil
+}
+
+func (v *vppConnector) connect() (err error) {
+ if v.conn, err = govpp.Connect(v.shmPrefix); err != nil {
+ return fmt.Errorf("failed to connect to vpp: %v", err)
+ }
+
+ if v.api, err = v.conn.NewAPIChannel(); err != nil {
+ return fmt.Errorf("failed to create api channel: %v", err)
+ }
+
+ v.stats = vppapiclient.NewStatClient(v.statsSocketPath)
+ if err = v.stats.Connect(); err != nil {
+ return fmt.Errorf("failed to connect to Stats adapter: %v", err)
+ }
+
+ return
+}
+
+func (v *vppConnector) disconnect() {
+ if v.stats != nil {
+ v.stats.Disconnect()
+ }
+ if v.conn != nil {
+ v.conn.Disconnect()
+ }
+}
+
+func (v *vppConnector) reduceCombinedCounters(stat *adapter.StatEntry) *[]adapter.CombinedCounter {
+ counters := stat.Data.(adapter.CombinedCounterStat)
+ stats := make([]adapter.CombinedCounter, len(v.Interfaces))
+ for _, workerStats := range counters {
+ for i, interfaceStats := range workerStats {
+ stats[i].Bytes += interfaceStats.Bytes
+ stats[i].Packets += interfaceStats.Packets
+ }
+ }
+ return &stats
+}
+
+func (v *vppConnector) reduceSimpleCounters(stat *adapter.StatEntry) *[]adapter.Counter {
+ counters := stat.Data.(adapter.SimpleCounterStat)
+ stats := make([]adapter.Counter, len(v.Interfaces))
+ for _, workerStats := range counters {
+ for i, interfaceStats := range workerStats {
+ stats[i] += interfaceStats
+ }
+ }
+ return &stats
+}
+
+func (v *vppConnector) getStatsForAllInterfaces() error {
+ statsDump, err := v.stats.DumpStats("/if")
+ if err != nil {
+ return fmt.Errorf("failed to dump vpp Stats: %v", err)
+ }
+
+ stats := func(i int) *interfaceStats { return &v.Interfaces[uint32(i)].Stats }
+
+ for _, stat := range statsDump {
+ switch stat.Name {
+ case "/if/tx":
+ {
+ for i, counter := range *v.reduceCombinedCounters(stat) {
+ stats(i).TxBytes = uint64(counter.Bytes)
+ stats(i).TxPackets = uint64(counter.Packets)
+ }
+ }
+ case "/if/rx":
+ {
+ for i, counter := range *v.reduceCombinedCounters(stat) {
+ stats(i).RxBytes = uint64(counter.Bytes)
+ stats(i).RxPackets = uint64(counter.Packets)
+ }
+ }
+ case "/if/tx-error":
+ {
+ for i, counter := range *v.reduceSimpleCounters(stat) {
+ stats(i).TxErrors = uint64(counter)
+ }
+ }
+ case "/if/rx-error":
+ {
+ for i, counter := range *v.reduceSimpleCounters(stat) {
+ stats(i).RxErrors = uint64(counter)
+ }
+ }
+ case "/if/drops":
+ {
+ for i, counter := range *v.reduceSimpleCounters(stat) {
+ stats(i).Drops = uint64(counter)
+ }
+ }
+ case "/if/punt":
+ {
+ for i, counter := range *v.reduceSimpleCounters(stat) {
+ stats(i).Punts = uint64(counter)
+ }
+ }
+ }
+ }
+ return nil
+}
+
+//////////////////////////////////////
+
+func main() {
+ statsSocketPathPtr := flag.String("stats_socket_path", defaultStatsSocketPath, "Path to vpp stats socket")
+ shmPrefixPtr := flag.String("shm_prefix", defaultShmPrefix, "Shared memory prefix (advanced)")
+ flag.Parse()
+
+ vppConn := &vppConnector{statsSocketPath: *statsSocketPathPtr, shmPrefix: *shmPrefixPtr}
+ defer vppConn.disconnect()
+
+ if err := vppConn.connect(); err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := vppConn.getVppVersion(); err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := vppConn.getInterfaces(); err != nil {
+ log.Fatalln(err)
+ }
+
+ if err := vppConn.getStatsForAllInterfaces(); err != nil {
+ log.Fatalln(err)
+ }
+
+ jsonString, err := dumpToJSONString(vppConn)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ fmt.Println(jsonString)
+}
diff --git a/extras/vpp_if_stats/vpp_if_stats_test.go b/extras/vpp_if_stats/vpp_if_stats_test.go
new file mode 100755
index 0000000..07a5983
--- /dev/null
+++ b/extras/vpp_if_stats/vpp_if_stats_test.go
@@ -0,0 +1,192 @@
+package main
+
+import (
+ "git.fd.io/govpp.git/adapter"
+ "git.fd.io/govpp.git/api"
+ "git.fd.io/govpp.git/examples/bin_api/interfaces"
+ "git.fd.io/govpp.git/examples/bin_api/vpe"
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/assert"
+ "math/rand"
+ "testing"
+ "time"
+)
+
+var (
+ vppDetails = vpe.ShowVersionReply{
+ Program: []byte("vpe"),
+ Version: []byte("18.10"),
+ }
+
+ testSwIfIndex = uint32(0)
+ testInterface = func() *vppInterface {
+ return &vppInterface{
+ SwInterfaceDetails: interfaces.SwInterfaceDetails{SwIfIndex: testSwIfIndex}, // TODO
+ Stats: interfaceStats{}, // TODO
+ }
+ }
+ testInterfaces = func() *map[uint32]*vppInterface {
+ return &map[uint32]*vppInterface{
+ testSwIfIndex: testInterface(),
+ }
+ }
+
+ r = rand.New(rand.NewSource(time.Now().UnixNano()))
+ testCombinedStats = interfaceStats{
+ TxBytes: r.Uint64(),
+ TxPackets: r.Uint64(),
+ RxBytes: r.Uint64(),
+ RxPackets: r.Uint64(),
+ }
+ testCombinedStatsDump = []*adapter.StatEntry{
+ {
+ Name: "/if/tx",
+ Type: adapter.CombinedCounterVector,
+ Data: adapter.CombinedCounterStat{
+ []adapter.CombinedCounter{
+ {
+ Bytes: adapter.Counter(testCombinedStats.TxBytes),
+ Packets: adapter.Counter(testCombinedStats.TxPackets),
+ },
+ },
+ },
+ },
+ {
+ Name: "/if/rx",
+ Type: adapter.CombinedCounterVector,
+ Data: adapter.CombinedCounterStat{
+ []adapter.CombinedCounter{
+ {
+ Bytes: adapter.Counter(testCombinedStats.RxBytes),
+ Packets: adapter.Counter(testCombinedStats.RxPackets),
+ },
+ },
+ },
+ },
+ }
+
+ testSimpleStats = interfaceStats{
+ TxErrors: r.Uint64(),
+ RxErrors: r.Uint64(),
+ Drops: r.Uint64(),
+ Punts: r.Uint64(),
+ }
+ testSimpleStatsDump = []*adapter.StatEntry{
+ {
+ Name: "/if/tx-error",
+ Type: adapter.SimpleCounterVector,
+ Data: adapter.SimpleCounterStat{
+ []adapter.Counter{adapter.Counter(testSimpleStats.TxErrors)},
+ },
+ },
+ {
+ Name: "/if/rx-error",
+ Type: adapter.SimpleCounterVector,
+ Data: adapter.SimpleCounterStat{
+ []adapter.Counter{adapter.Counter(testSimpleStats.RxErrors)},
+ },
+ },
+ {
+ Name: "/if/drops",
+ Type: adapter.SimpleCounterVector,
+ Data: adapter.SimpleCounterStat{
+ []adapter.Counter{adapter.Counter(testSimpleStats.Drops)},
+ },
+ },
+ {
+ Name: "/if/punt",
+ Type: adapter.SimpleCounterVector,
+ Data: adapter.SimpleCounterStat{
+ []adapter.Counter{adapter.Counter(testSimpleStats.Punts)},
+ },
+ },
+ }
+)
+
+type showDetailsContext struct {
+ details vpe.ShowVersionReply
+}
+
+func (ctx *showDetailsContext) ReceiveReply(msg api.Message) (err error) {
+ *(msg.(*vpe.ShowVersionReply)) = vppDetails
+ return nil
+}
+
+type interfaceDumpContext struct {
+ interfaces []interfaces.SwInterfaceDetails
+ currentIndex int
+}
+
+func (ctx *interfaceDumpContext) ReceiveReply(msg api.Message) (lastReplyReceived bool, err error) {
+ stop := ctx.currentIndex >= len(ctx.interfaces)
+ if !stop {
+ *(msg.(*interfaces.SwInterfaceDetails)) = ctx.interfaces[ctx.currentIndex]
+ ctx.currentIndex++
+ }
+ return stop, nil
+}
+
+func TestVppIfStats_GetVppVersion(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockChannel := NewMockChannel(mockCtrl)
+ mockChannel.EXPECT().SendRequest(&vpe.ShowVersion{}).Return(&showDetailsContext{details: vppDetails})
+
+ v := vppConnector{api: mockChannel}
+ err := v.getVppVersion()
+ assert.NoError(t, err, "GetVppVersion should not return an error")
+ assert.Equal(t, vppDetails, v.VppDetails, "VPP details should be saved")
+}
+
+func TestVppIfStats_GetInterfaces(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ testContext := interfaceDumpContext{interfaces: []interfaces.SwInterfaceDetails{testInterface().SwInterfaceDetails}}
+ mockChannel := NewMockChannel(mockCtrl)
+ mockChannel.EXPECT().SendMultiRequest(&interfaces.SwInterfaceDump{}).Return(&testContext)
+
+ v := vppConnector{api: mockChannel}
+ err := v.getInterfaces()
+ assert.NoError(t, err, "GetInterfaces should not return an error")
+ assert.Len(t, v.Interfaces, len(testContext.interfaces), "All dumped interfaces should be saved")
+ if len(testContext.interfaces) > 0 {
+ assert.Equal(t, testContext.interfaces[0], v.Interfaces[testInterface().SwIfIndex].SwInterfaceDetails,
+ "All dumped interface info should be saved")
+ }
+}
+
+func TestVppIfStats_GetStatsForAllInterfacesNoStats(t *testing.T) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockStatsAPI := NewMockStatsAPI(mockCtrl)
+ mockStatsAPI.EXPECT().DumpStats("/if").Return([]*adapter.StatEntry{}, nil)
+
+ v := vppConnector{stats: mockStatsAPI, Interfaces: *testInterfaces()}
+ err := v.getStatsForAllInterfaces()
+ assert.NoError(t, err, "GetStatsForAllInterfaces should not return an error")
+ assert.Equal(t, interfaceStats{}, v.Interfaces[testSwIfIndex].Stats, "Stats should be empty")
+}
+
+func testStats(t *testing.T, statsDump *[]*adapter.StatEntry, expectedStats *interfaceStats) {
+ mockCtrl := gomock.NewController(t)
+ defer mockCtrl.Finish()
+
+ mockStatsAPI := NewMockStatsAPI(mockCtrl)
+ mockStatsAPI.EXPECT().DumpStats("/if").Return(*statsDump, nil)
+
+ v := vppConnector{stats: mockStatsAPI, Interfaces: *testInterfaces()}
+ err := v.getStatsForAllInterfaces()
+ assert.NoError(t, err, "GetStatsForAllInterfaces should not return an error")
+ assert.Equal(t, *expectedStats, v.Interfaces[testSwIfIndex].Stats, "Collected and saved stats should match")
+}
+
+func TestVppIfStats_GetStatsForAllInterfacesCombinedStats(t *testing.T) {
+ testStats(t, &testCombinedStatsDump, &testCombinedStats)
+}
+
+func TestVppIfStats_GetStatsForAllInterfacesSimpleStats(t *testing.T) {
+ testStats(t, &testSimpleStatsDump, &testSimpleStats)
+}