diff --git a/.travis.yml b/.travis.yml index 39e32d7..b55f665 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ before_install: - sudo apt install openvswitch-switch - sudo ovs-vsctl add-br ovsbr0 - go get github.com/golang/lint/golint + - go get github.com/google/go-cmp/cmp - go get honnef.co/go/tools/cmd/staticcheck - go get -d ./... script: diff --git a/ovs/vswitch.go b/ovs/vswitch.go index d06b302..49ef99e 100644 --- a/ovs/vswitch.go +++ b/ovs/vswitch.go @@ -17,6 +17,7 @@ package ovs import ( "encoding/json" "fmt" + "strconv" "strings" ) @@ -179,6 +180,47 @@ func (v *VSwitchGetService) Bridge(bridge string) (BridgeOptions, error) { }, nil } +// Port gets configuration for a port and returns the values through +// a PortOptions struct. +func (v *VSwitchGetService) Port(port string) (PortOptions, error) { + // We only support the tag, vlan_mode, trunk option at this point. + args := []string{"--format=json", "get", "port", port, "tag", "vlan_mode", "trunk"} + out, err := v.v.exec(args...) + if err != nil { + return PortOptions{}, err + } + + // If the option is not exist, OVS will return "[]\n" + options := strings.Split(strings.TrimSpace(string(out)), "\n") + + var tag *int + if options[0] != "[]" { + tagNum, err := strconv.Atoi(options[0]) + if err != nil { + return PortOptions{}, err + } + tag = &tagNum + } + + var vlanMode *string + if options[1] != "[]" { + vlanMode = &options[1] + } + + var trunk []int + if options[2] != "[]" { + if err := json.Unmarshal([]byte(options[2]), &trunk); err != nil { + return PortOptions{}, err + } + } + + return PortOptions{ + Tag: tag, + VLANMode: vlanMode, + Trunk: trunk, + }, nil +} + // A VSwitchSetService is used in a VSwitchService to execute 'ovs-vsctl set' // subcommands. type VSwitchSetService struct { @@ -216,6 +258,49 @@ func (o BridgeOptions) slice() []string { return s } +// Port sets configuration for a port using the values from a PortOptions +// struct. +func (v *VSwitchSetService) Port(port string, options PortOptions) error { + // Prepend command line arguments before expanding options slice + // and appending it + args := []string{"set", "port", port} + args = append(args, options.slice()...) + + _, err := v.v.exec(args...) + return err +} + +// An PortOptions struct enables configuration of a port. +type PortOptions struct { + Tag *int + VLANMode *string + Trunk []int +} + +// slice creates a string slice containing any non-zero option values from the +// struct in the format expected by Open vSwitch. +func (o PortOptions) slice() []string { + var s []string + + if o.Tag != nil { + s = append(s, fmt.Sprintf("tag=%s,", strconv.Itoa(*o.Tag))) + } + + if o.VLANMode != nil { + s = append(s, fmt.Sprintf("vlan_mode=%s", *o.VLANMode)) + } + + if len(o.Trunk) > 0 { + var strTrunk string + for _, trunk := range o.Trunk { + strTrunk += fmt.Sprintf("%s,", strconv.Itoa(trunk)) + } + s = append(s, fmt.Sprintf("trunk=%s", strTrunk)) + } + + return s +} + // Interface sets configuration for an interface using the values from an // InterfaceOptions struct. func (v *VSwitchSetService) Interface(ifi string, options InterfaceOptions) error { diff --git a/ovs/vswitch_test.go b/ovs/vswitch_test.go index 8dfa9c8..aad2457 100644 --- a/ovs/vswitch_test.go +++ b/ovs/vswitch_test.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "reflect" + "strconv" "strings" "testing" ) @@ -501,6 +502,101 @@ func TestClientVSwitchSetBridgeProtocolsOK(t *testing.T) { } } +func TestClientVSwitchGetPortOptionsOK(t *testing.T) { + const port = "bond0" + vlanModeStr := "trunk" + vlanMode := &vlanModeStr + trunk := []int{1, 2, 3, 4, 5} + + c := testClient([]OptionFunc{Timeout(1)}, func(cmd string, args ...string) ([]byte, error) { + if want, got := "ovs-vsctl", cmd; want != got { + t.Fatalf("incorrect command:\n- want: %v\n- got: %v", + want, got) + } + + wantArgs := []string{ + "--timeout=1", + "--format=json", + "get", + "port", + port, + "tag", + "vlan_mode", + "trunk", + } + if want, got := wantArgs, args; !reflect.DeepEqual(want, got) { + t.Fatalf("incorrect arguments\n- want: %v\n- got: %v", + want, got) + } + + // Make the return value with newline to simulate + // the ovs-vsctl output. + data := "[]\n" + data += fmt.Sprintf("%s\n", vlanModeStr) + t, err := json.Marshal(&trunk) + if err != nil { + return nil, err + } + data += fmt.Sprintf("%s\n", string(t)) + return []byte(fmt.Sprintln(data)), err + }) + + got, err := c.VSwitch.Get.Port(port) + if err != nil { + t.Fatalf("unexpected error for Client.VSwitch.Get.Port: %v", err) + } + if got.Tag != nil { + t.Fatalf("unexpected tag for Client.VSwitch.Get.Port: %v", *got.Tag) + } + if !reflect.DeepEqual(*got.VLANMode, *vlanMode) { + t.Fatalf("unexpected vlan_mode for Client.VSwitch.Get.Port: %v", *got.VLANMode) + } + if !reflect.DeepEqual(got.Trunk, trunk) { + t.Fatalf("unexpected trunk for Client.VSwitch.Get.Port: %v", got.Trunk) + } +} + +func TestClientVSwitchSetPortOptionsOK(t *testing.T) { + const port = "bond0" + vlanModeStr := "trunk" + vlanMode := &vlanModeStr + trunk := []int{1, 2, 3, 4, 5} + + c := testClient([]OptionFunc{Timeout(1)}, func(cmd string, args ...string) ([]byte, error) { + if want, got := "ovs-vsctl", cmd; want != got { + t.Fatalf("incorrect command:\n- want: %v\n- got: %v", + want, got) + } + + var trunkSequence string + for _, trunk := range trunk { + trunkSequence += fmt.Sprintf("%s,", strconv.Itoa(int(trunk))) + } + wantArgs := []string{ + "--timeout=1", + "set", + "port", + port, + fmt.Sprintf("vlan_mode=%s", *vlanMode), + fmt.Sprintf("trunk=%s", trunkSequence), + } + if want, got := wantArgs, args; !reflect.DeepEqual(want, got) { + t.Fatalf("incorrect arguments\n- want: %v\n- got: %v", + want, got) + } + + return nil, nil + }) + + err := c.VSwitch.Set.Port(port, PortOptions{ + VLANMode: vlanMode, + Trunk: trunk, + }) + if err != nil { + t.Fatalf("unexpected error for Client.VSwitch.Set.Bridge: %v", err) + } +} + func TestClientVSwitchSetInterfaceTypeOK(t *testing.T) { ifi := "bond0" ifiType := InterfaceTypePatch