Skip to content

Commit e4d121d

Browse files
committed
ovsdb: initial commit
1 parent 0a4a53a commit e4d121d

File tree

5 files changed

+334
-1
lines changed

5 files changed

+334
-1
lines changed

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,6 @@ script:
1616
- go vet ./...
1717
- staticcheck ./...
1818
- ./scripts/golint.sh
19-
- go test -v -race ./...
19+
- go test -race ./...
20+
- go test -c ./ovsdb
21+
- sudo ./ovsdb.test -test.v

ovsdb/client.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ovsdb
16+
17+
import (
18+
"bytes"
19+
"encoding/json"
20+
"fmt"
21+
"net/rpc"
22+
"net/rpc/jsonrpc"
23+
)
24+
25+
// A Client is an OVSDB client.
26+
type Client struct {
27+
rc *rpc.Client
28+
}
29+
30+
// Dial dials a connection to an OVSDB server and returns a Client.
31+
func Dial(network, addr string) (*Client, error) {
32+
c, err := jsonrpc.Dial(network, addr)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
return &Client{
38+
rc: c,
39+
}, nil
40+
}
41+
42+
// Close closes a Client's connection.
43+
func (c *Client) Close() error {
44+
return c.rc.Close()
45+
}
46+
47+
// ListDatabases returns the name of all databases known to the OVSDB server.
48+
func (c *Client) ListDatabases() ([]string, error) {
49+
var dbs []string
50+
if err := c.rpc("list_dbs", nil, &dbs); err != nil {
51+
return nil, err
52+
}
53+
54+
return dbs, nil
55+
}
56+
57+
// rpc performs a single RPC request, and checks the response for errors.
58+
func (c *Client) rpc(method string, args, reply interface{}) error {
59+
// Captures any JSON-RPC errors.
60+
r := result{
61+
Reply: reply,
62+
}
63+
64+
if err := c.rc.Call(method, args, &r); err != nil {
65+
return err
66+
}
67+
68+
// OVSDB server returned an error, return it.
69+
if r.Err != nil {
70+
return r.Err
71+
}
72+
73+
return nil
74+
}
75+
76+
// A result is used to unmarshal JSON-RPC results, and to check for any errors.
77+
type result struct {
78+
Reply interface{}
79+
Err *Error
80+
}
81+
82+
// errPrefix is a prefix that occurs if an error is present in a JSON-RPC response.
83+
var errPrefix = []byte(`{"error":`)
84+
85+
func (r *result) UnmarshalJSON(b []byte) error {
86+
// No error? Return the result.
87+
if !bytes.HasPrefix(b, errPrefix) {
88+
return json.Unmarshal(b, r.Reply)
89+
}
90+
91+
// Found an error, unmarshal and return it later.
92+
var e Error
93+
if err := json.Unmarshal(b, &e); err != nil {
94+
return err
95+
}
96+
97+
r.Err = &e
98+
return nil
99+
}
100+
101+
var _ error = &Error{}
102+
103+
// An Error is an error returned by an OVSDB server. Its fields can be
104+
// used to determine the cause of an error.
105+
type Error struct {
106+
Err string `json:"error"`
107+
Details string `json:"details"`
108+
Syntax string `json:"syntax"`
109+
}
110+
111+
func (e *Error) Error() string {
112+
return fmt.Sprintf("%s: %s: %s", e.Err, e.Details, e.Syntax)
113+
}

ovsdb/client_integration_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ovsdb_test
16+
17+
import (
18+
"os"
19+
"testing"
20+
21+
"github.com/digitalocean/go-openvswitch/ovsdb"
22+
"github.com/google/go-cmp/cmp"
23+
)
24+
25+
func TestClientIntegration(t *testing.T) {
26+
// Assume the standard Linux location for the socket.
27+
const sock = "/var/run/openvswitch/db.sock"
28+
if _, err := os.Open(sock); err != nil {
29+
t.Skipf("could not access %q: %v", sock, err)
30+
}
31+
32+
c, err := ovsdb.Dial("unix", sock)
33+
if err != nil {
34+
t.Fatalf("failed to dial: %v", err)
35+
}
36+
defer c.Close()
37+
38+
t.Run("databases", func(t *testing.T) {
39+
testClientDatabases(t, c)
40+
})
41+
}
42+
43+
func testClientDatabases(t *testing.T, c *ovsdb.Client) {
44+
dbs, err := c.ListDatabases()
45+
if err != nil {
46+
t.Fatalf("failed to list databases: %v", err)
47+
}
48+
49+
want := []string{"Open_vSwitch"}
50+
51+
if diff := cmp.Diff(want, dbs); diff != "" {
52+
t.Fatalf("unexpected databases (-want +got):\n%s", diff)
53+
}
54+
}

ovsdb/client_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package ovsdb_test
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
"net"
21+
"sync"
22+
"testing"
23+
24+
"github.com/digitalocean/go-openvswitch/ovsdb"
25+
"github.com/google/go-cmp/cmp"
26+
)
27+
28+
func TestClientError(t *testing.T) {
29+
const str = "some error"
30+
31+
c, done := testClient(t, func(_ string, _ []interface{}) interface{} {
32+
return &ovsdb.Error{
33+
Err: str,
34+
Details: "malformed",
35+
Syntax: "{}",
36+
}
37+
})
38+
defer done()
39+
40+
_, err := c.ListDatabases()
41+
if err == nil {
42+
t.Fatal("expected an error, but none occurred")
43+
}
44+
45+
oerr, ok := err.(*ovsdb.Error)
46+
if !ok {
47+
t.Fatalf("error of wrong type: %#v", err)
48+
}
49+
50+
if diff := cmp.Diff(str, oerr.Err); diff != "" {
51+
t.Fatalf("unexpected error (-want +got):\n%s", diff)
52+
}
53+
}
54+
func TestClientListDatabases(t *testing.T) {
55+
want := []string{"Open_vSwitch", "test"}
56+
57+
c, done := testClient(t, func(method string, params []interface{}) interface{} {
58+
if diff := cmp.Diff("list_dbs", method); diff != "" {
59+
t.Fatalf("unexpected RPC method (-want +got):\n%s", diff)
60+
}
61+
62+
if diff := cmp.Diff(1, len(params)); diff != "" {
63+
t.Fatalf("unexpected number of RPC parameters (-want +got):\n%s", diff)
64+
}
65+
66+
return want
67+
})
68+
defer done()
69+
70+
dbs, err := c.ListDatabases()
71+
if err != nil {
72+
t.Fatalf("failed to list databases: %v", err)
73+
}
74+
75+
if diff := cmp.Diff(want, dbs); diff != "" {
76+
t.Fatalf("unexpected databases (-want +got):\n%s", diff)
77+
}
78+
}
79+
80+
type rpcFunc func(method string, params []interface{}) interface{}
81+
82+
func testClient(t *testing.T, fn rpcFunc) (*ovsdb.Client, func()) {
83+
t.Helper()
84+
85+
l, err := net.Listen("tcp", ":0")
86+
if err != nil {
87+
t.Fatalf("failed to listen: %v", err)
88+
}
89+
90+
var wg sync.WaitGroup
91+
wg.Add(1)
92+
93+
go func() {
94+
defer wg.Done()
95+
96+
// Accept a single connection.
97+
c, err := l.Accept()
98+
if err != nil {
99+
panicf("failed to accept: %v", err)
100+
}
101+
defer c.Close()
102+
_ = l.Close()
103+
104+
if err := handleConn(c, fn); err != nil {
105+
panicf("failed to handle connection: %v", err)
106+
}
107+
}()
108+
109+
c, err := ovsdb.Dial("tcp", l.Addr().String())
110+
if err != nil {
111+
t.Fatalf("failed to dial: %v", err)
112+
}
113+
114+
return c, func() {
115+
// Ensure types are cleaned up, and ensure goroutine stops.
116+
_ = l.Close()
117+
_ = c.Close()
118+
wg.Wait()
119+
}
120+
}
121+
122+
func handleConn(c net.Conn, fn rpcFunc) error {
123+
var req struct {
124+
Method string `json:"method"`
125+
Params []interface{} `json:"params"`
126+
ID int `json:"id"`
127+
}
128+
129+
var res struct {
130+
Result interface{} `json:"result"`
131+
ID int `json:"id"`
132+
}
133+
134+
if err := json.NewDecoder(c).Decode(&req); err != nil {
135+
return err
136+
}
137+
138+
result := fn(req.Method, req.Params)
139+
140+
res.ID = req.ID
141+
res.Result = result
142+
143+
return json.NewEncoder(c).Encode(res)
144+
}
145+
146+
func panicf(format string, a ...interface{}) {
147+
panic(fmt.Sprintf(format, a...))
148+
}

ovsdb/doc.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// Copyright 2017 DigitalOcean.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package ovsdb implements an OVSDB client, as described in RFC 7047.
16+
package ovsdb

0 commit comments

Comments
 (0)