Skip to content

Commit 14e26d9

Browse files
committed
ovsdb: initial Transact RPC and types
1 parent 2c976d1 commit 14e26d9

File tree

4 files changed

+225
-6
lines changed

4 files changed

+225
-6
lines changed

ovsdb/client_integration_test.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"sync"
2121
"testing"
22+
"time"
2223

2324
"github.com/digitalocean/go-openvswitch/ovsdb"
2425
"github.com/google/go-cmp/cmp"
@@ -28,11 +29,16 @@ func TestClientIntegration(t *testing.T) {
2829
c := dialOVSDB(t)
2930
defer c.Close()
3031

32+
// Cancel RPCs if they take too long.
33+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
34+
defer cancel()
35+
3136
t.Run("echo", func(t *testing.T) {
32-
testClientEcho(t, c)
37+
testClientEcho(ctx, t, c)
3338
})
39+
3440
t.Run("databases", func(t *testing.T) {
35-
testClientDatabases(t, c)
41+
testClientDatabases(ctx, t, c)
3642
})
3743
}
3844

@@ -75,8 +81,8 @@ func TestClientIntegrationConcurrent(t *testing.T) {
7581
doneWG.Wait()
7682
}
7783

78-
func testClientDatabases(t *testing.T, c *ovsdb.Client) {
79-
dbs, err := c.ListDatabases(context.Background())
84+
func testClientDatabases(ctx context.Context, t *testing.T, c *ovsdb.Client) {
85+
dbs, err := c.ListDatabases(ctx)
8086
if err != nil {
8187
t.Fatalf("failed to list databases: %v", err)
8288
}
@@ -86,10 +92,25 @@ func testClientDatabases(t *testing.T, c *ovsdb.Client) {
8692
if diff := cmp.Diff(want, dbs); diff != "" {
8793
t.Fatalf("unexpected databases (-want +got):\n%s", diff)
8894
}
95+
96+
for _, d := range dbs {
97+
rows, err := c.Transact(ctx, d, []ovsdb.TransactOp{
98+
ovsdb.Select{
99+
Table: "Bridge",
100+
},
101+
})
102+
if err != nil {
103+
t.Fatalf("failed to perform transaction: %v", err)
104+
}
105+
106+
for i, r := range rows {
107+
t.Logf("[%02d] %v", i, r)
108+
}
109+
}
89110
}
90111

91-
func testClientEcho(t *testing.T, c *ovsdb.Client) {
92-
if err := c.Echo(context.Background()); err != nil {
112+
func testClientEcho(ctx context.Context, t *testing.T, c *ovsdb.Client) {
113+
if err := c.Echo(ctx); err != nil {
93114
t.Fatalf("failed to echo: %v", err)
94115
}
95116
}

ovsdb/rpc.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,36 @@ func (c *Client) Echo(ctx context.Context) error {
4545

4646
return nil
4747
}
48+
49+
// A Row is a database row. Its keys are database column names, and its values
50+
// are database column values.
51+
type Row map[string]interface{}
52+
53+
// TODO(mdlayher): try to make concrete types for row values.
54+
55+
// Transact creates and executes a transaction on the specified database.
56+
// Each operation is applied in the order they appear in ops.
57+
func (c *Client) Transact(ctx context.Context, db string, ops []TransactOp) ([]Row, error) {
58+
// Required because transact uses an unusual syntax for its arguments.
59+
arg := transactArg{
60+
Database: db,
61+
Ops: ops,
62+
}
63+
64+
// TODO(mdlayher): deal with non-select ops too.
65+
var out []struct {
66+
Rows []Row `json:"rows"`
67+
}
68+
69+
if err := c.rpc(ctx, "transact", &out, arg); err != nil {
70+
return nil, err
71+
}
72+
73+
// Flatten results from all selects into one slice of rows.
74+
var rows []Row
75+
for _, o := range out {
76+
rows = append(rows, o.Rows...)
77+
}
78+
79+
return rows, nil
80+
}

ovsdb/rpc_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"testing"
2020

21+
"github.com/digitalocean/go-openvswitch/ovsdb"
2122
"github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc"
2223
"github.com/google/go-cmp/cmp"
2324
)
@@ -91,3 +92,63 @@ func TestClientEchoOK(t *testing.T) {
9192
t.Fatalf("failed to echo: %v", err)
9293
}
9394
}
95+
96+
func TestClientTransactSelect(t *testing.T) {
97+
const db = "Open_vSwitch"
98+
99+
c, _, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response {
100+
if diff := cmp.Diff("transact", req.Method); diff != "" {
101+
panicf("unexpected RPC method (-want +got):\n%s", diff)
102+
}
103+
104+
// TODO(mdlayher): clean up with JSON unmarshaler implementations.
105+
params := []interface{}{
106+
db,
107+
map[string]interface{}{
108+
"op": "select",
109+
"table": "Bridge",
110+
"where": []interface{}{
111+
[]interface{}{"name", "==", "ovsbr0"},
112+
},
113+
},
114+
}
115+
116+
if diff := cmp.Diff(params, req.Params); diff != "" {
117+
panicf("unexpected RPC parameters (-want +got):\n%s", diff)
118+
}
119+
120+
type result struct {
121+
Rows []ovsdb.Row `json:"rows"`
122+
}
123+
124+
return jsonrpc.Response{
125+
ID: strPtr("1"),
126+
Result: mustMarshalJSON(t, []result{{
127+
Rows: []ovsdb.Row{{
128+
"name": "ovsbr0",
129+
}},
130+
}}),
131+
}
132+
})
133+
defer done()
134+
135+
ops := []ovsdb.TransactOp{ovsdb.Select{
136+
Table: "Bridge",
137+
Where: []ovsdb.Cond{
138+
ovsdb.Equal("name", "ovsbr0"),
139+
},
140+
}}
141+
142+
rows, err := c.Transact(context.Background(), db, ops)
143+
if err != nil {
144+
t.Fatalf("failed to perform transaction: %v", err)
145+
}
146+
147+
want := []ovsdb.Row{{
148+
"name": "ovsbr0",
149+
}}
150+
151+
if diff := cmp.Diff(want, rows); diff != "" {
152+
t.Fatalf("unexpected rows (-want +got):\n%s", diff)
153+
}
154+
}

ovsdb/transact.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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 "encoding/json"
18+
19+
// A Cond is a conditional expression which is evaluated by the OVSDB server
20+
// in a transaction.
21+
type Cond struct {
22+
Column, Function, Value string
23+
}
24+
25+
// TODO(mdlayher): more helper functions? Cond as an interface?
26+
27+
// Equal creates a Cond that ensures a column's value equals the
28+
// specified value.
29+
func Equal(column, value string) Cond {
30+
return Cond{
31+
Column: column,
32+
Function: "==",
33+
Value: value,
34+
}
35+
}
36+
37+
// MarshalJSON implements json.Marshaler.
38+
func (c Cond) MarshalJSON() ([]byte, error) {
39+
// Conditionals are expected in three element arrays.
40+
return json.Marshal([3]string{
41+
c.Column,
42+
c.Function,
43+
c.Value,
44+
})
45+
}
46+
47+
// A TransactOp is an operation that can be applied with Client.Transact.
48+
type TransactOp interface {
49+
json.Marshaler
50+
}
51+
52+
var _ TransactOp = Select{}
53+
54+
// Select is a TransactOp which fetches information from a database.
55+
type Select struct {
56+
// The name of the table to select from.
57+
Table string
58+
59+
// Zero or more Conds for conditional select.
60+
Where []Cond
61+
62+
// TODO(mdlayher): specify columns.
63+
}
64+
65+
// MarshalJSON implements json.Marshaler.
66+
func (s Select) MarshalJSON() ([]byte, error) {
67+
// Send an empty array instead of nil if no where clause.
68+
where := s.Where
69+
if where == nil {
70+
where = []Cond{}
71+
}
72+
73+
sel := struct {
74+
Op string `json:"op"`
75+
Table string `json:"table"`
76+
Where []Cond `json:"where"`
77+
}{
78+
Op: "select",
79+
Table: s.Table,
80+
Where: where,
81+
}
82+
83+
return json.Marshal(sel)
84+
}
85+
86+
// A transactArg is used to properly JSON marshal the arguments for a
87+
// transact RPC.
88+
type transactArg struct {
89+
Database string
90+
Ops []TransactOp
91+
}
92+
93+
// MarshalJSON implements json.Marshaler.
94+
func (t transactArg) MarshalJSON() ([]byte, error) {
95+
out := []interface{}{
96+
t.Database,
97+
}
98+
99+
for _, op := range t.Ops {
100+
out = append(out, op)
101+
}
102+
103+
return json.Marshal(out)
104+
}

0 commit comments

Comments
 (0)