From 80b6e5b3c01734dd251b8ed005065368187be301 Mon Sep 17 00:00:00 2001 From: kwanhur Date: Thu, 24 Feb 2022 20:10:08 +0800 Subject: [PATCH] feature: ovs vport retrieve operation like: List GetByID GetByName solve #110 Signed-off-by: kwanhur --- AUTHORS | 1 + ovsnl/client.go | 140 +++++++++++++ ovsnl/vport.go | 368 ++++++++++++++++++++++++++++++++++ ovsnl/vport_linux_test.go | 410 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 919 insertions(+) create mode 100644 ovsnl/vport.go create mode 100644 ovsnl/vport_linux_test.go diff --git a/AUTHORS b/AUTHORS index efb22b9..4d335cc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,3 +14,4 @@ Kei Nohguchi Neal Shrader Sangeetha Srikanth Franck Rupin +Kwanhur Huang diff --git a/ovsnl/client.go b/ovsnl/client.go index 204af0e..47dace5 100644 --- a/ovsnl/client.go +++ b/ovsnl/client.go @@ -22,6 +22,8 @@ import ( "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" "github.com/mdlayher/genetlink" + "github.com/mdlayher/netlink" + "github.com/mdlayher/netlink/nlenc" ) // Sizes of various structures, used in unsafe casts. @@ -30,12 +32,15 @@ const ( sizeofDPStats = int(unsafe.Sizeof(ovsh.DPStats{})) sizeofDPMegaflowStats = int(unsafe.Sizeof(ovsh.DPMegaflowStats{})) + sizeofVportStats = int(unsafe.Sizeof(ovsh.VportStats{})) ) // A Client is a Linux Open vSwitch generic netlink client. type Client struct { // Datapath provides access to DatapathService methods. Datapath *DatapathService + // Vport provides access to VportService methods. + Vport *VportService c *genetlink.Conn } @@ -111,6 +116,12 @@ func (c *Client) initFamily(f genetlink.Family) error { c: c, } return nil + case ovsh.VportFamily: + c.Vport = &VportService{ + c: c, + f: f, + } + return nil default: // Unknown OVS netlink family, nothing we can do. return fmt.Errorf("unknown OVS generic netlink family: %q", f.Name) @@ -133,3 +144,132 @@ func parseHeader(b []byte) (ovsh.Header, error) { h := *(*ovsh.Header)(unsafe.Pointer(&b[:sizeofHeader][0])) return h, nil } + +// NlMsgBuilder to build genetlink message +type NlMsgBuilder struct { + msg *genetlink.Message +} + +// NewNlMsgBuilder construct a netlink message builder with genetlink.Message +func NewNlMsgBuilder() *NlMsgBuilder { + return &NlMsgBuilder{msg: &genetlink.Message{}} +} + +// PutGenlMsgHdr set msg header with genetlink.Header +func (nlmsg *NlMsgBuilder) PutGenlMsgHdr(command, version uint8) { + nlmsg.msg.Header = genetlink.Header{ + Command: command, + Version: version, + } +} + +// PutOvsHeader set ovs header with ovsh.Header +func (nlmsg *NlMsgBuilder) PutOvsHeader(dpID int32) { + nlmsg.msg.Data = headerBytes(ovsh.Header{Ifindex: dpID}) +} + +// PutStringAttr put attribute with string value +func (nlmsg *NlMsgBuilder) PutStringAttr(typ uint16, value string) error { + attrs := []netlink.Attribute{ + { + Type: typ, + Data: nlenc.Bytes(value), + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal string attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// PutUint8Attr put attribute with uint8 value +func (nlmsg *NlMsgBuilder) PutUint8Attr(typ uint16, value uint8) error { + attrs := []netlink.Attribute{ + { + Type: typ, + Data: nlenc.Uint8Bytes(value), + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal uint8 attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// PutUint16Attr put attribute with uint16 value +func (nlmsg *NlMsgBuilder) PutUint16Attr(typ uint16, value uint16) error { + attrs := []netlink.Attribute{ + { + Type: typ, + Data: nlenc.Uint16Bytes(value), + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal uint16 attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// PutUint32Attr put attribute with uint32 value +func (nlmsg *NlMsgBuilder) PutUint32Attr(typ uint16, value uint32) error { + attrs := []netlink.Attribute{ + { + Type: typ, + Data: nlenc.Uint32Bytes(value), + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal uint32 attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// PutSliceAttr put attribute with slice byte value +func (nlmsg *NlMsgBuilder) PutSliceAttr(typ uint16, value []byte) error { + attrs := []netlink.Attribute{ + { + Type: typ, + Data: value, + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal slice byte attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// PutEmptyAttr put attribute with empty value +func (nlmsg *NlMsgBuilder) PutEmptyAttr(typ uint16) error { + attrs := []netlink.Attribute{ + { + Type: typ, + }, + } + attr, err := netlink.MarshalAttributes(attrs) + if err != nil { + return fmt.Errorf("marshal empty attributes failed:%s", err) + } + + nlmsg.msg.Data = append(nlmsg.msg.Data[:], attr...) + return nil +} + +// Message generic netlink message +func (nlmsg *NlMsgBuilder) Message() genetlink.Message { + return *nlmsg.msg +} diff --git a/ovsnl/vport.go b/ovsnl/vport.go new file mode 100644 index 0000000..abefd0d --- /dev/null +++ b/ovsnl/vport.go @@ -0,0 +1,368 @@ +// Copyright 2022 DigitalOcean. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ovsnl + +import ( + "fmt" + "unsafe" + + "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" + "github.com/mdlayher/genetlink" + "github.com/mdlayher/netlink" + "github.com/mdlayher/netlink/nlenc" +) + +// VportService provides access to methods which interact with the +// "ovs_vport" generic netlink family. +type VportService struct { + c *Client + f genetlink.Family +} + +// VportID numbers are scoped to a particular datapath +type VportID uint32 + +// VportSpec vport spec +type VportSpec interface { + TypeName() string + Name() string + typeID() uint32 + optionNlAttrs() +} + +// VportSpecBase vport spec basic structure +type VportSpecBase struct { + name string +} + +// Name vport spec name +func (v VportSpecBase) Name() string { + return v.name +} + +// SimpleVportSpec simple vport spec +type SimpleVportSpec struct { + VportSpecBase + typ uint32 + typeName string +} + +// TypeName vport spec type name +func (s SimpleVportSpec) TypeName() string { + return s.typeName +} + +func (s SimpleVportSpec) typeID() uint32 { + return s.typ +} + +func (s SimpleVportSpec) optionNlAttrs() { + +} + +// NewNetDevVportSpec creates netdev vport spec +func NewNetDevVportSpec(name string) VportSpec { + return SimpleVportSpec{ + VportSpecBase: VportSpecBase{name: name}, + typ: ovsh.VportTypeNetdev, + typeName: "netdev", + } +} + +// NewInternalVportSepc creates internal vport spec +func NewInternalVportSepc(name string) VportSpec { + return SimpleVportSpec{ + VportSpecBase: VportSpecBase{name: name}, + typ: ovsh.VportTypeInternal, + typeName: "internal", + } +} + +// GreVportSpec GRE vport spec +type GreVportSpec struct { + VportSpecBase +} + +// TypeName returns type name +func (s GreVportSpec) TypeName() string { + return "gre" +} + +func (s GreVportSpec) typeID() uint32 { + return ovsh.VportTypeGre +} + +func (s GreVportSpec) optionNlAttrs() { + +} + +// NewGreVportSpec creates GRE vport spec +func NewGreVportSpec(name string) VportSpec { + return GreVportSpec{VportSpecBase{name: name}} +} + +type udpVportSpec struct { + VportSpecBase + Port uint16 +} + +func (s udpVportSpec) optionNlAttrs() { + +} + +// VxLanVportSpec vxlan port spec +type VxLanVportSpec struct { + udpVportSpec +} + +// TypeName returns type name +func (s VxLanVportSpec) TypeName() string { + return "vxlan" +} + +func (s VxLanVportSpec) typeID() uint32 { + return ovsh.VportTypeVxlan +} + +// NewVxLanVportSpec creates vxlan vport spec +func NewVxLanVportSpec(name string, port uint16) VportSpec { + return VxLanVportSpec{udpVportSpec{VportSpecBase{name: name}, port}} +} + +// GeneveVportSpec geneve port spec +type GeneveVportSpec struct { + udpVportSpec +} + +// TypeName returns type name +func (s GeneveVportSpec) TypeName() string { + return "geneve" +} + +func (s GeneveVportSpec) typeID() uint32 { + return ovsh.VportTypeGeneve +} + +// NewGeneveVportSpec creates geneve vport spec +func NewGeneveVportSpec(name string, port uint16) VportSpec { + return GeneveVportSpec{udpVportSpec{VportSpecBase{name: name}, port}} +} + +// Vport is an Open vSwitch in-kernel vport. +type Vport struct { + DatapathID int // datapath id + ID VportID + Spec VportSpec + Stats VportStats + IfIndex uint32 + NetNsID uint32 +} + +func (p *Vport) String() string { + return fmt.Sprintf("port %d: %s (%s) IfIndex:%d netnsid:%d", p.ID, p.Spec.Name(), p.Spec.TypeName(), + p.IfIndex, p.NetNsID) +} + +// VportStats contatins statistics about packets that have passed through a vport. +type VportStats struct { + // total packets received + RxPackets uint64 + // total packets transmitted + TxPackets uint64 + // total bytes received + RxBytes uint64 + // total bytes transmitted + TxBytes uint64 + // total bad packets received + RxErrors uint64 + // total bad packets transmitted + TxErrors uint64 + // total dropped packets, no space in linux buffers + RxDropped uint64 + // total dropped packets, no space available in linux + TxDropped uint64 +} + +func (s *VportStats) String() string { + return fmt.Sprintf("RX packets:%d errors:%d dropped:%d\nTX packets:%d errors:%d dropped:%d\n"+ + "RX bytes:%d TX bytes:%d", s.RxPackets, s.RxErrors, s.RxDropped, s.TxPackets, s.TxErrors, s.TxDropped, + s.RxBytes, s.TxBytes) +} + +// parseVportStats parses a slice of byte into VportStats +func parseVportStats(b []byte) (VportStats, error) { + // Verfiy that the byte slice is the correct length before doing + // unsafe casts. + if want, got := sizeofVportStats, len(b); want != got { + return VportStats{}, fmt.Errorf("unexpected vport stats structure size, want %d, got %d", want, got) + } + + s := *(*ovsh.VportStats)(unsafe.Pointer(&b[0])) + return VportStats{ + RxPackets: s.Rx_packets, + TxPackets: s.Tx_packets, + RxBytes: s.Rx_bytes, + TxBytes: s.Tx_bytes, + RxErrors: s.Rx_errors, + TxErrors: s.Tx_errors, + RxDropped: s.Rx_dropped, + TxDropped: s.Tx_dropped, + }, nil +} + +// parseVport parses a Vport from a generic netlink message +func parseVport(msg genetlink.Message) (Vport, error) { + // Fetch the header at the beginning of the message + h, err := parseHeader(msg.Data) + if err != nil { + return Vport{}, err + } + + vport := Vport{ + DatapathID: int(h.Ifindex), + } + + var ( + spec VportSpec + typ uint32 + name string + ) + + // Skip the header to parse attributes. + attrs, err := netlink.UnmarshalAttributes(msg.Data[sizeofHeader:]) + if err != nil { + return Vport{}, err + } + + for _, a := range attrs { + switch a.Type { + case ovsh.VportAttrPortNo: + vport.ID = VportID(nlenc.Uint32(a.Data)) + case ovsh.VportAttrType: + typ = nlenc.Uint32(a.Data) + case ovsh.VportAttrName: + name = nlenc.String(a.Data) + case ovsh.VportAttrIfindex: + vport.IfIndex = nlenc.Uint32(a.Data) + case ovsh.VportAttrNetnsid: + vport.NetNsID = nlenc.Uint32(a.Data) + case ovsh.VportAttrStats: + vport.Stats, err = parseVportStats(a.Data) + if err != nil { + return Vport{}, err + } + } + } + + switch typ { + case ovsh.VportTypeNetdev: + spec = NewNetDevVportSpec(name) + case ovsh.VportTypeInternal: + spec = NewInternalVportSepc(name) + case ovsh.VportTypeGre: + spec = NewGreVportSpec(name) + case ovsh.VportTypeVxlan: + spec = NewVxLanVportSpec(name, 0) + case ovsh.VportTypeGeneve: + spec = NewGeneveVportSpec(name, 0) + default: + err = fmt.Errorf("unsupported vport type %d", typ) + } + + if err != nil { + return Vport{}, err + } + + vport.Spec = spec + return vport, nil +} + +// parseVports parses a slice of Vport from a slice of generic netlink messages. +func parseVports(msgs []genetlink.Message) ([]Vport, error) { + vports := make([]Vport, 0, len(msgs)) + + for _, m := range msgs { + vport, err := parseVport(m) + if err != nil { + return nil, err + } + vports = append(vports, vport) + } + + return vports, nil +} + +// GetByID get a Vport with specified id in the kernel. +func (s *VportService) GetByID(dpID int, vportID VportID) (Vport, error) { + req := NewNlMsgBuilder() + req.PutGenlMsgHdr(ovsh.VportCmdGet, s.f.Version) + req.PutOvsHeader(int32(dpID)) + if err := req.PutUint32Attr(ovsh.VportAttrPortNo, uint32(vportID)); err != nil { + return Vport{}, err + } + + flags := netlink.Request | netlink.Echo + msgs, err := s.c.c.Execute(req.Message(), s.f.ID, flags) + if err != nil { + return Vport{}, err + } + + if len(msgs) == 0 { + return Vport{}, nil + } + + return parseVport(msgs[0]) +} + +// GetByName get a Vport with specified name in the kernel. +func (s *VportService) GetByName(dpID int, name string) (Vport, error) { + req := NewNlMsgBuilder() + req.PutGenlMsgHdr(ovsh.VportCmdGet, s.f.Version) + req.PutOvsHeader(int32(dpID)) + if err := req.PutStringAttr(ovsh.VportAttrPortNo, name); err != nil { + return Vport{}, err + } + + flags := netlink.Request | netlink.Echo + msgs, err := s.c.c.Execute(req.Message(), s.f.ID, flags) + if err != nil { + return Vport{}, err + } + + if len(msgs) == 0 { + return Vport{}, nil + } + + return parseVport(msgs[0]) +} + +// List lists all Vport in the kernel. +func (s *VportService) List(dpID int) ([]Vport, error) { + req := NewNlMsgBuilder() + req.PutGenlMsgHdr(ovsh.VportCmdGet, s.f.Version) + req.PutOvsHeader(int32(dpID)) + + flags := netlink.Request | netlink.Echo + msgs, err := s.c.c.Execute(req.Message(), s.f.ID, flags) + if err != nil { + return nil, err + } + + if len(msgs) == 0 { + return nil, nil + } + + return parseVports(msgs) +} diff --git a/ovsnl/vport_linux_test.go b/ovsnl/vport_linux_test.go new file mode 100644 index 0000000..abafbed --- /dev/null +++ b/ovsnl/vport_linux_test.go @@ -0,0 +1,410 @@ +// Copyright 2022 DigitalOcean. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build linux +// +build linux + +package ovsnl + +import ( + "fmt" + "testing" + "unsafe" + + "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" + "github.com/google/go-cmp/cmp" + "github.com/mdlayher/genetlink" + "github.com/mdlayher/genetlink/genltest" + "github.com/mdlayher/netlink" + "github.com/mdlayher/netlink/nlenc" +) + +var ( + optUnexportSpecBase = cmp.AllowUnexported(VportSpecBase{}) + optUnexportSimpleVport = cmp.AllowUnexported(SimpleVportSpec{}) +) + +func TestClientVportListOK(t *testing.T) { + tap0 := Vport{ + DatapathID: 1, + ID: 1, + Spec: NewInternalVportSepc("tap0"), + Stats: VportStats{ + RxPackets: 100, + TxPackets: 200, + RxBytes: 1000, + TxBytes: 500, + RxDropped: 0, + TxDropped: 5, + RxErrors: 0, + TxErrors: 10, + }, + IfIndex: 10, + NetNsID: 9, + } + + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Ensure we are querying the "ovs_vport" family with the + // correct parameters. + if diff := cmp.Diff(ovsh.VportCmdGet, int(greq.Header.Command)); diff != "" { + t.Fatalf("unexpected generic netlink command (-want +got):\n%s", diff) + } + + h, err := parseHeader(greq.Data) + if err != nil { + t.Fatalf("failed to parse OvS generic netlink header: %v", err) + } + + if diff := cmp.Diff(1, int(h.Ifindex)); diff != "" { + t.Fatalf("unexpected datapath ID (-want +got):\n%s", diff) + } + + return []genetlink.Message{ + { + Data: mustMarshalVport(tap0), + }, + }, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + vports, err := c.Vport.List(1) + if err != nil { + t.Fatalf("failed to list vports: %v", err) + } + + if diff := cmp.Diff(1, len(vports)); diff != "" { + t.Fatalf("unexpected number of vports (-want +got):\n%s", diff) + } + + if diff := cmp.Diff(tap0, vports[0], optUnexportSpecBase, optUnexportSimpleVport); diff != "" { + t.Fatalf("unexpected tap0 vport (-want +got):\n%s", diff) + } +} + +func TestClientVportListBadStats(t *testing.T) { + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Valid header; not enough data for ovsh.VportAttrStats. + return []genetlink.Message{{ + Data: append( + // ovsh.Header. + []byte{0xff, 0xff, 0xff, 0xff}, + // netlink attributes. + mustMarshalAttributes([]netlink.Attribute{{ + Type: ovsh.VportAttrStats, + Data: []byte{0xff}, + }})..., + ), + }}, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + _, err = c.Vport.List(1) + if err == nil { + t.Fatalf("expected an error, but none occurred") + } + + t.Logf("OK error: %v", err) +} + +func TestClientGetVportByIDOK(t *testing.T) { + tap0 := Vport{ + DatapathID: 1, + ID: 1, + Spec: NewInternalVportSepc("tap0"), + Stats: VportStats{ + RxPackets: 100, + TxPackets: 200, + RxBytes: 1000, + TxBytes: 500, + RxDropped: 0, + TxDropped: 5, + RxErrors: 0, + TxErrors: 10, + }, + IfIndex: 10, + NetNsID: 9, + } + + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + return []genetlink.Message{ + { + Data: mustMarshalVport(tap0), + }, + }, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + vport, err := c.Vport.GetByID(1, 1) + if err != nil { + t.Fatalf("failed to get vport: %v", err) + } + + if diff := cmp.Diff(tap0, vport, optUnexportSpecBase, optUnexportSimpleVport); diff != "" { + t.Fatalf("unexpected tap0 vport (-want +got):\n%s", diff) + } +} + +func TestClientGetVportByIDBad(t *testing.T) { + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Valid header; + return []genetlink.Message{{ + Data: append( + // ovsh.Header. + []byte{0xff, 0xff, 0xff, 0xff}, + ), + }}, fmt.Errorf("400 bad request") + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + _, err = c.Vport.GetByID(1, 1) + if err == nil { + t.Fatalf("expected an error, but none occurred") + } + + t.Logf("OK error: %v", err) +} + +func TestClientGetVportByIDNotFound(t *testing.T) { + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Valid header; not enough data for ovsh.VportAttrs. + return []genetlink.Message{{ + Data: append( + // ovsh.Header. + []byte{0xff, 0xff, 0xff, 0xff}, + ), + }}, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + _, err = c.Vport.GetByID(1, 1) + if err == nil { + t.Fatalf("expected an error, but none occurred") + } + + t.Logf("OK error: %v", err) +} + +func TestClientGetVportByIDEmpty(t *testing.T) { + tap0 := Vport{} + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Empty messages + return []genetlink.Message{}, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + vport, err := c.Vport.GetByID(1, 1) + if err != nil { + t.Fatalf("failed to get vport: %v", err) + } + + if diff := cmp.Diff(tap0, vport, optUnexportSpecBase, optUnexportSimpleVport); diff != "" { + t.Fatalf("unexpected tap0 vport (-want +got):\n%s", diff) + } +} + +func TestClientGetVportByNameOK(t *testing.T) { + tap0 := Vport{ + DatapathID: 1, + ID: 1, + Spec: NewInternalVportSepc("tap0"), + Stats: VportStats{ + RxPackets: 100, + TxPackets: 200, + RxBytes: 1000, + TxBytes: 500, + RxDropped: 0, + TxDropped: 5, + RxErrors: 0, + TxErrors: 10, + }, + IfIndex: 10, + NetNsID: 9, + } + + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + return []genetlink.Message{ + { + Data: mustMarshalVport(tap0), + }, + }, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + vport, err := c.Vport.GetByName(1, "tap0") + if err != nil { + t.Fatalf("failed to get vport: %v", err) + } + + if diff := cmp.Diff(tap0, vport, optUnexportSpecBase, optUnexportSimpleVport); diff != "" { + t.Fatalf("unexpected tap0 vport (-want +got):\n%s", diff) + } +} + +func TestClientGetVportByNameBad(t *testing.T) { + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Valid header; + return []genetlink.Message{{ + Data: append( + // ovsh.Header. + []byte{0xff, 0xff, 0xff, 0xff}, + ), + }}, fmt.Errorf("400 bad request") + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + _, err = c.Vport.GetByName(1, "") + if err == nil { + t.Fatalf("expected an error, but none occurred") + } + + t.Logf("OK error: %v", err) +} + +func TestClientGetVportByNameNotFound(t *testing.T) { + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Valid header; not enough data for ovsh.VportAttrs. + return []genetlink.Message{{ + Data: append( + // ovsh.Header. + []byte{0xff, 0xff, 0xff, 0xff}, + ), + }}, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + _, err = c.Vport.GetByName(1, "") + if err == nil { + t.Fatalf("expected an error, but none occurred") + } + + t.Logf("OK error: %v", err) +} + +func TestClientGetVportByNameEmpty(t *testing.T) { + tap0 := Vport{} + conn := genltest.Dial(ovsFamilies(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { + // Empty messages + return []genetlink.Message{}, nil + })) + + c, err := newClient(conn) + if err != nil { + t.Fatalf("failed to create client: %v", err) + } + defer c.Close() + + vport, err := c.Vport.GetByName(1, "") + if err != nil { + t.Fatalf("failed to get vport: %v", err) + } + + if diff := cmp.Diff(tap0, vport, optUnexportSpecBase, optUnexportSimpleVport); diff != "" { + t.Fatalf("unexpected tap0 vport (-want +got):\n%s", diff) + } +} + +func mustMarshalVport(p Vport) []byte { + h := ovsh.Header{ + Ifindex: int32(p.DatapathID), + } + + hb := headerBytes(h) + + s := ovsh.VportStats{ + Rx_packets: p.Stats.RxPackets, + Tx_packets: p.Stats.TxPackets, + Rx_bytes: p.Stats.RxBytes, + Tx_bytes: p.Stats.TxBytes, + Rx_errors: p.Stats.RxErrors, + Tx_errors: p.Stats.TxErrors, + Rx_dropped: p.Stats.RxDropped, + Tx_dropped: p.Stats.TxDropped, + } + + sb := *(*[sizeofVportStats]byte)(unsafe.Pointer(&s)) + + ab := mustMarshalAttributes([]netlink.Attribute{ + { + Type: ovsh.VportAttrPortNo, + Data: nlenc.Uint32Bytes(uint32(p.ID)), + }, + { + Type: ovsh.VportAttrType, + Data: nlenc.Uint32Bytes(p.Spec.typeID()), + }, + { + Type: ovsh.VportAttrName, + Data: nlenc.Bytes(p.Spec.Name()), + }, + { + Type: ovsh.VportAttrStats, + Data: sb[:], + }, + { + Type: ovsh.VportAttrIfindex, + Data: nlenc.Uint32Bytes(p.IfIndex), + }, + { + Type: ovsh.VportAttrNetnsid, + Data: nlenc.Uint32Bytes(p.NetNsID), + }, + }) + + return append(hb[:], ab...) +}