├── .github └── workflows │ └── go.yml ├── AUTHORS ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── go.mod ├── go.sum ├── ovs ├── README.md ├── action.go ├── action_test.go ├── actionparser.go ├── actionparser_test.go ├── app.go ├── client.go ├── client_test.go ├── codegen.go ├── datapath.go ├── datapath_test.go ├── doc.go ├── flow.go ├── flow_test.go ├── flowstats.go ├── flowstats_test.go ├── match.go ├── match_test.go ├── matchflow.go ├── matchflow_test.go ├── matchparser.go ├── matchparser_test.go ├── openflow.go ├── openflow_test.go ├── ovs.go ├── ovs_test.go ├── portrange.go ├── portrange_test.go ├── portstats.go ├── portstats_test.go ├── proto_trace.go ├── proto_trace_test.go ├── table.go ├── table_test.go ├── vswitch.go └── vswitch_test.go ├── ovsdb ├── README.md ├── client.go ├── client_integration_test.go ├── client_test.go ├── doc.go ├── example_test.go ├── internal │ └── jsonrpc │ │ ├── conn.go │ │ ├── conn_test.go │ │ ├── doc.go │ │ └── testconn.go ├── result.go ├── rpc.go ├── rpc_test.go └── transact.go ├── ovsnl ├── README.md ├── client.go ├── client_linux_integration_test.go ├── client_linux_test.go ├── datapath.go ├── datapath_linux_test.go ├── doc.go └── internal │ └── ovsh │ ├── const.go │ ├── doc.go │ ├── ovsh.yml │ ├── struct.go │ └── types.go └── scripts ├── gofmt.sh ├── golint.sh ├── license.txt ├── licensecheck.sh └── prependlicense.sh /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: 1.17 20 | 21 | - name: OVS setup 22 | run: | 23 | sudo apt-get update 24 | sudo apt install openvswitch-switch 25 | sudo ovs-vsctl add-br ovsbr0 26 | 27 | - name: Check and test 28 | run: | 29 | export GOPATH=/home/runner/work 30 | export PATH=$PATH:$GOPATH/bin 31 | mkdir $GOPATH/src $GOPATH/pkg $GOPATH/bin 32 | go install honnef.co/go/tools/cmd/staticcheck@2020.2.1 33 | NEW=$GOPATH/src/github.com/digitalocean/go-openvswitch 34 | mkdir -p $NEW 35 | cp -r ./* $NEW 36 | cd $NEW 37 | go mod download 38 | go get golang.org/x/lint/golint 39 | go get -d ./... 40 | echo "=========START LICENSE CHECK============" 41 | ./scripts/licensecheck.sh 42 | echo "=========START BUILD============" 43 | go build -v -tags=gofuzz ./... 44 | echo "=========START VET============" 45 | go vet ./... 46 | echo "=========START GO FMT CHECK============" 47 | ./scripts/gofmt.sh 48 | echo "=========START GO LINT CHECK============" 49 | ./scripts/golint.sh 50 | echo "=========START STATICCHECK============" 51 | staticcheck ./... 52 | echo "=========START LINT============" 53 | golint -set_exit_status ./cmd/... ./internal/... 54 | echo "=========START TESTS IN OVS============" 55 | go test -v -race -short ./ovs/ 56 | echo "=========START TESTS IN OVSDB============" 57 | go test -v -race ./ovsdb/ 58 | go test -c -race ./ovsdb 59 | echo "=========START TESTS IN OVSNL============" 60 | go test -v -race ./ovsnl/ 61 | echo "=========START OVSDB.TEST============" 62 | sudo ./ovsdb.test -test.v 63 | echo "=========START SECURITY SCANNING============" 64 | go install github.com/praetorian-inc/gokart@latest 65 | gokart scan 66 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Maintainer 2 | ---------- 3 | DigitalOcean, Inc 4 | 5 | Original Authors 6 | ---------------- 7 | Matt Layher 8 | 9 | Contributors 10 | ------------ 11 | Michael Ben-Ami 12 | Tejas Kokje 13 | Kei Nohguchi 14 | Neal Shrader 15 | Sangeetha Srikanth 16 | Franck Rupin 17 | Adam Simeth 18 | Manmeet Singh -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | The `go-openvswitch` project makes use of the [GitHub Flow](https://guides.github.com/introduction/flow/) 5 | for contributions. 6 | 7 | If you'd like to contribute to the project, please 8 | [open an issue](https://github.com/digitalocean/go-openvswitch/issues/new) or find an 9 | [existing issue](https://github.com/digitalocean/go-openvswitch/issues) that you'd like 10 | to take on. This ensures that efforts are not duplicated, and that a new feature 11 | aligns with the focus of the rest of the repository. 12 | 13 | Once your suggestion has been submitted and discussed, please be sure that your 14 | code meets the following criteria: 15 | - code is completely `gofmt`'d 16 | - new features or codepaths have appropriate test coverage 17 | - `go test ./...` passes 18 | - `go vet ./...` passes 19 | - `staticcheck ./...` passes 20 | - `golint ./...` returns no warnings, including documentation comment warnings 21 | 22 | In addition, if this is your first time contributing to the `go-openvswitch` project, 23 | add your name and email address to the 24 | [AUTHORS](https://github.com/digitalocean/go-openvswitch/blob/master/AUTHORS) file 25 | under the "Contributors" section using the format: 26 | `First Last `. 27 | 28 | Finally, submit a pull request for review! -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | ============== 3 | 4 | _Version 2.0, January 2004_ 5 | _<>_ 6 | 7 | ### Terms and Conditions for use, reproduction, and distribution 8 | 9 | #### 1. Definitions 10 | 11 | “License” shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | “Licensor” shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | “Legal Entity” shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, “control” means **(i)** the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the 22 | outstanding shares, or **(iii)** beneficial ownership of such entity. 23 | 24 | “You” (or “Your”) shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | “Source” form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | “Object” form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | “Work” shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | “Derivative Works” shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | “Contribution” shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | “submitted” means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as “Not a Contribution.” 58 | 59 | “Contributor” shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | #### 2. Grant of Copyright License 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | #### 3. Grant of Patent License 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | #### 4. Redistribution 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | * **(a)** You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | * **(b)** You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | * **(c)** You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | * **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | 114 | You may add Your own copyright statement to Your modifications and may provide 115 | additional or different license terms and conditions for use, reproduction, or 116 | distribution of Your modifications, or for any such Derivative Works as a whole, 117 | provided Your use, reproduction, and distribution of the Work otherwise complies 118 | with the conditions stated in this License. 119 | 120 | #### 5. Submission of Contributions 121 | 122 | Unless You explicitly state otherwise, any Contribution intentionally submitted 123 | for inclusion in the Work by You to the Licensor shall be under the terms and 124 | conditions of this License, without any additional terms or conditions. 125 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 126 | any separate license agreement you may have executed with Licensor regarding 127 | such Contributions. 128 | 129 | #### 6. Trademarks 130 | 131 | This License does not grant permission to use the trade names, trademarks, 132 | service marks, or product names of the Licensor, except as required for 133 | reasonable and customary use in describing the origin of the Work and 134 | reproducing the content of the NOTICE file. 135 | 136 | #### 7. Disclaimer of Warranty 137 | 138 | Unless required by applicable law or agreed to in writing, Licensor provides the 139 | Work (and each Contributor provides its Contributions) on an “AS IS” BASIS, 140 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 141 | including, without limitation, any warranties or conditions of TITLE, 142 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 143 | solely responsible for determining the appropriateness of using or 144 | redistributing the Work and assume any risks associated with Your exercise of 145 | permissions under this License. 146 | 147 | #### 8. Limitation of Liability 148 | 149 | In no event and under no legal theory, whether in tort (including negligence), 150 | contract, or otherwise, unless required by applicable law (such as deliberate 151 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 152 | liable to You for damages, including any direct, indirect, special, incidental, 153 | or consequential damages of any character arising as a result of this License or 154 | out of the use or inability to use the Work (including but not limited to 155 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 156 | any and all other commercial damages or losses), even if such Contributor has 157 | been advised of the possibility of such damages. 158 | 159 | #### 9. Accepting Warranty or Additional Liability 160 | 161 | While redistributing the Work or Derivative Works thereof, You may choose to 162 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 163 | other liability obligations and/or rights consistent with this License. However, 164 | in accepting such obligations, You may act only on Your own behalf and on Your 165 | sole responsibility, not on behalf of any other Contributor, and only if You 166 | agree to indemnify, defend, and hold each Contributor harmless for any liability 167 | incurred by, or claims asserted against, such Contributor by reason of your 168 | accepting any such warranty or additional liability. 169 | 170 | _END OF TERMS AND CONDITIONS_ 171 | 172 | ### APPENDIX: How to apply the Apache License to your work 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets `[]` replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same “printed page” as the copyright notice for easier identification within 180 | third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | you may not use this file except in compliance with the License. 186 | You may obtain a copy of the License at 187 | 188 | http://www.apache.org/licenses/LICENSE-2.0 189 | 190 | Unless required by applicable law or agreed to in writing, software 191 | distributed under the License is distributed on an "AS IS" BASIS, 192 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 193 | See the License for the specific language governing permissions and 194 | limitations under the License. 195 | 196 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-openvswitch [![Build Status](https://travis-ci.org/digitalocean/go-openvswitch.svg?branch=master)](https://travis-ci.org/digitalocean/go-openvswitch) [![GoDoc](https://godoc.org/github.com/digitalocean/go-openvswitch?status.svg)](https://godoc.org/github.com/digitalocean/go-openvswitch) [![Go Report Card](https://goreportcard.com/badge/github.com/digitalocean/go-openvswitch)](https://goreportcard.com/report/github.com/digitalocean/go-openvswitch) 2 | ============== 3 | 4 | Go packages which enable interacting with Open vSwitch and related tools. Apache 2.0 Licensed. 5 | 6 | - `ovs`: Package ovs is a client library for Open vSwitch which enables programmatic control of the virtual switch. 7 | - `ovsdb`: Package ovsdb implements an OVSDB client, as described in RFC 7047. 8 | - `ovsnl`: Package ovsnl enables interaction with the Linux Open vSwitch generic netlink interface. 9 | 10 | See each package's README for additional information. -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/digitalocean/go-openvswitch 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.6 7 | github.com/mdlayher/genetlink v1.0.0 8 | github.com/mdlayher/netlink v1.4.1 9 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.5.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 4 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 5 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 6 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 8 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 10 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 11 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 12 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA= 13 | github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 14 | github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= 15 | github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= 16 | github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= 17 | github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw= 18 | github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs= 19 | github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA= 20 | github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U= 21 | github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190 h1:iycCSDo8EKVueI9sfVBBJmtNn9DnXV/K1YWwEJO+uOs= 22 | github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190/go.mod h1:NmKSdU4VGSiv1bMsdqNALI4RSvvjtz65tTMCnD05qLo= 23 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 24 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 25 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 26 | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY= 27 | github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo= 28 | github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0= 29 | github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc= 30 | github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= 31 | github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= 32 | github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= 33 | github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= 34 | github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8= 35 | github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 36 | github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU= 37 | github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys= 38 | github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8= 39 | github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI= 40 | github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q= 41 | github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks= 42 | github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/XV68LkQKYzKhIo/WW7j3Zi0YRAz/BOoanUc= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 45 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 47 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 48 | golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 49 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 50 | golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 51 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 52 | golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 53 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 54 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 55 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 56 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo= 57 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 58 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 59 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 63 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 64 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 65 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 70 | golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 73 | golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 74 | golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 75 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 77 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf h1:2ucpDCmfkl8Bd/FsLtiD653Wf96cW37s+iGx93zsu4k= 78 | golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 80 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 81 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 82 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 83 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 84 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 85 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 86 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 87 | -------------------------------------------------------------------------------- /ovs/README.md: -------------------------------------------------------------------------------- 1 | ovs 2 | === 3 | 4 | Package `ovs` is a client library for Open vSwitch which enables programmatic 5 | control of the virtual switch. 6 | 7 | Package `ovs` is a wrapper around the `ovs-vsctl` and `ovs-ofctl` utilities, but 8 | in the future, it may speak OVSDB and OpenFlow directly with the same interface. 9 | 10 | ```go 11 | // Create a *ovs.Client. Specify ovs.OptionFuncs to customize it. 12 | c := ovs.New( 13 | // Prepend "sudo" to all commands. 14 | ovs.Sudo(), 15 | ) 16 | 17 | // $ sudo ovs-vsctl --may-exist add-br ovsbr0 18 | if err := c.VSwitch.AddBridge("ovsbr0"); err != nil { 19 | log.Fatalf("failed to add bridge: %v", err) 20 | } 21 | 22 | // $ sudo ovs-ofctl add-flow ovsbr0 priority=100,ip,actions=drop 23 | err := c.OpenFlow.AddFlow("ovsbr0", &ovs.Flow{ 24 | Priority: 100, 25 | Protocol: ovs.ProtocolIPv4, 26 | Actions: []ovs.Action{ovs.Drop()}, 27 | }) 28 | if err != nil { 29 | log.Fatalf("failed to add flow: %v", err) 30 | } 31 | ``` 32 | -------------------------------------------------------------------------------- /ovs/actionparser.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "fmt" 21 | "io" 22 | "net" 23 | "regexp" 24 | "strconv" 25 | "strings" 26 | ) 27 | 28 | // An actionParser is a parser for OVS flow actions. 29 | type actionParser struct { 30 | r *bufio.Reader 31 | s stack 32 | } 33 | 34 | // newActionParser creates a new actionParser which wraps the input 35 | // io.Reader. 36 | func newActionParser(r io.Reader) *actionParser { 37 | return &actionParser{ 38 | r: bufio.NewReader(r), 39 | s: make(stack, 0), 40 | } 41 | } 42 | 43 | // eof is a sentinel rune for end of file. 44 | var eof = rune(0) 45 | 46 | // read reads a single rune from the wrapped io.Reader. It returns eof 47 | // if no more runes are present. 48 | func (p *actionParser) read() rune { 49 | ch, _, err := p.r.ReadRune() 50 | if err != nil { 51 | return eof 52 | } 53 | return ch 54 | } 55 | 56 | // Parse parses a slice of Actions using the wrapped io.Reader. The raw 57 | // action strings are also returned for inspection if needed. 58 | func (p *actionParser) Parse() ([]Action, []string, error) { 59 | var actions []Action 60 | var raw []string 61 | 62 | for { 63 | a, r, err := p.parseAction() 64 | if err != nil { 65 | // No more actions remain 66 | if err == io.EOF { 67 | break 68 | } 69 | 70 | return nil, nil, err 71 | } 72 | 73 | actions = append(actions, a) 74 | raw = append(raw, r) 75 | } 76 | 77 | return actions, raw, nil 78 | } 79 | 80 | // parseAction parses a single Action and its raw text from the wrapped 81 | // io.Reader. 82 | func (p *actionParser) parseAction() (Action, string, error) { 83 | // Track runes encountered 84 | var buf bytes.Buffer 85 | 86 | for { 87 | ch := p.read() 88 | 89 | // If comma encountered and no open parentheses, at end of this 90 | // action string 91 | if ch == ',' && p.s.len() == 0 { 92 | break 93 | } 94 | 95 | // If EOF encountered, at end of string 96 | if ch == eof { 97 | // If no items in buffer, end of this action 98 | if buf.Len() == 0 { 99 | return nil, "", io.EOF 100 | } 101 | 102 | // Parse action from buffer 103 | break 104 | } 105 | 106 | // Track open and closing parentheses using a stack to ensure 107 | // that they are appropriately matched 108 | switch ch { 109 | case '(': 110 | p.s.push() 111 | case ')': 112 | p.s.pop() 113 | } 114 | 115 | _, _ = buf.WriteRune(ch) 116 | } 117 | 118 | // Found an unmatched set of parentheses 119 | if p.s.len() > 0 { 120 | return nil, "", fmt.Errorf("invalid action: %q", buf.String()) 121 | } 122 | 123 | s := buf.String() 124 | act, err := parseAction(s) 125 | return act, s, err 126 | } 127 | 128 | // A stack is a basic stack with elements that have no value. 129 | type stack []struct{} 130 | 131 | // len returns the current length of the stack. 132 | func (s *stack) len() int { 133 | return len(*s) 134 | } 135 | 136 | // push adds an element to the stack. 137 | func (s *stack) push() { 138 | *s = append(*s, struct{}{}) 139 | } 140 | 141 | // pop removes an element from the stack. 142 | func (s *stack) pop() { 143 | *s = (*s)[:s.len()-1] 144 | } 145 | 146 | var ( 147 | // resubmitRe is the regex used to match the resubmit action 148 | // with port and table specified 149 | resubmitRe = regexp.MustCompile(`resubmit\((\d*),(\d*)\)`) 150 | 151 | // resubmitPortRe is the regex used to match the resubmit action 152 | // when only a port is specified 153 | resubmitPortRe = regexp.MustCompile(`resubmit:(\d+)`) 154 | 155 | // ctRe is the regex used to match the ct action with its 156 | // parameter list. 157 | ctRe = regexp.MustCompile(`ct\((\S+)\)`) 158 | 159 | // loadRe is the regex used to match the load action 160 | // with its parameters. 161 | loadRe = regexp.MustCompile(`load:(\S+)->(\S+)`) 162 | 163 | // moveRe is the regex used to match the move action 164 | // with its parameters. 165 | moveRe = regexp.MustCompile(`move:(\S+)->(\S+)`) 166 | 167 | // setFieldRe is the regex used to match the set_field action 168 | // with its parameters. 169 | setFieldRe = regexp.MustCompile(`set_field:(\S+)->(\S+)`) 170 | ) 171 | 172 | // TODO(mdlayher): replace parsing regex with arguments parsers 173 | 174 | // parseAction creates an Action function from the input string. 175 | func parseAction(s string) (Action, error) { 176 | // Simple actions which match a basic string 177 | switch strings.ToLower(s) { 178 | case actionDrop: 179 | return Drop(), nil 180 | case actionFlood: 181 | return Flood(), nil 182 | case actionInPort: 183 | return InPort(), nil 184 | case actionLocal: 185 | return Local(), nil 186 | case actionNormal: 187 | return Normal(), nil 188 | case actionStripVLAN: 189 | return StripVLAN(), nil 190 | } 191 | 192 | // ActionCT, with its arguments 193 | if ss := ctRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { 194 | // Results are: 195 | // - full string 196 | // - arguments list 197 | return ConnectionTracking(ss[0][1]), nil 198 | } 199 | 200 | // ActionModDataLinkDestination, with its hardware address. 201 | if strings.HasPrefix(s, patModDataLinkDestination[:len(patModDataLinkDestination)-2]) { 202 | var addr string 203 | n, err := fmt.Sscanf(s, patModDataLinkDestination, &addr) 204 | if err != nil { 205 | return nil, err 206 | } 207 | if n > 0 { 208 | mac, err := net.ParseMAC(addr) 209 | if err != nil { 210 | return nil, err 211 | } 212 | 213 | return ModDataLinkDestination(mac), nil 214 | } 215 | } 216 | 217 | // ActionModDataLinkSource, with its hardware address. 218 | if strings.HasPrefix(s, patModDataLinkSource[:len(patModDataLinkSource)-2]) { 219 | var addr string 220 | n, err := fmt.Sscanf(s, patModDataLinkSource, &addr) 221 | if err != nil { 222 | return nil, err 223 | } 224 | if n > 0 { 225 | mac, err := net.ParseMAC(addr) 226 | if err != nil { 227 | return nil, err 228 | } 229 | 230 | return ModDataLinkSource(mac), nil 231 | } 232 | } 233 | 234 | // ActionModNetworkDestination, with it hardware address 235 | if strings.HasPrefix(s, patModNetworkDestination[:len(patModNetworkDestination)-2]) { 236 | var ip string 237 | n, err := fmt.Sscanf(s, patModNetworkDestination, &ip) 238 | if err != nil { 239 | return nil, err 240 | } 241 | if n > 0 { 242 | ip4 := net.ParseIP(ip).To4() 243 | if ip4 == nil { 244 | return nil, fmt.Errorf("invalid IPv4 address: %s", ip) 245 | } 246 | 247 | return ModNetworkDestination(ip4), nil 248 | } 249 | } 250 | 251 | // ActionModNetworkSource, with it hardware address 252 | if strings.HasPrefix(s, patModNetworkSource[:len(patModNetworkSource)-2]) { 253 | var ip string 254 | n, err := fmt.Sscanf(s, patModNetworkSource, &ip) 255 | if err != nil { 256 | return nil, err 257 | } 258 | if n > 0 { 259 | ip4 := net.ParseIP(ip).To4() 260 | if ip4 == nil { 261 | return nil, fmt.Errorf("invalid IPv4 address: %s", ip) 262 | } 263 | 264 | return ModNetworkSource(ip4), nil 265 | } 266 | } 267 | 268 | // ActionModTransportDestinationPort, with its port. 269 | if strings.HasPrefix(s, patModTransportDestinationPort[:len(patModTransportDestinationPort)-2]) { 270 | var port uint16 271 | n, err := fmt.Sscanf(s, patModTransportDestinationPort, &port) 272 | if err != nil { 273 | return nil, err 274 | } 275 | if n > 0 { 276 | return ModTransportDestinationPort(port), nil 277 | } 278 | } 279 | 280 | // ActionModTransportSourcePort, with its port. 281 | if strings.HasPrefix(s, patModTransportSourcePort[:len(patModTransportSourcePort)-2]) { 282 | var port uint16 283 | n, err := fmt.Sscanf(s, patModTransportSourcePort, &port) 284 | if err != nil { 285 | return nil, err 286 | } 287 | if n > 0 { 288 | return ModTransportSourcePort(port), nil 289 | } 290 | } 291 | 292 | // ActionModVLANVID, with its VLAN ID 293 | if strings.HasPrefix(s, patModVLANVID[:len(patModVLANVID)-2]) { 294 | var vlan int 295 | n, err := fmt.Sscanf(s, patModVLANVID, &vlan) 296 | if err != nil { 297 | return nil, err 298 | } 299 | if n > 0 { 300 | return ModVLANVID(vlan), nil 301 | } 302 | } 303 | 304 | // ActionConjunction, with it's id, dimension number, and dimension size 305 | if strings.HasPrefix(s, patConjunction[:len(patConjunction)-10]) { 306 | var id, dimensionNumber, dimensionSize int 307 | n, err := fmt.Sscanf(s, patConjunction, &id, &dimensionNumber, &dimensionSize) 308 | if err != nil { 309 | return nil, err 310 | } 311 | if n > 0 { 312 | return Conjunction(id, dimensionNumber, dimensionSize), nil 313 | } 314 | } 315 | 316 | // ActionOutput, with its port number 317 | if strings.HasPrefix(s, patOutput[:len(patOutput)-2]) { 318 | var port int 319 | n, err := fmt.Sscanf(s, patOutput, &port) 320 | if err != nil { 321 | return nil, err 322 | } 323 | if n > 0 { 324 | return Output(port), nil 325 | } 326 | } 327 | 328 | // ActionResubmit, with both port number and table number 329 | if ss := resubmitRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 3 { 330 | var ( 331 | port int 332 | table int 333 | 334 | err error 335 | ) 336 | 337 | // Results are: 338 | // - full string 339 | // - port in parenthesis 340 | // - table in parenthesis 341 | 342 | if s := ss[0][1]; s != "" { 343 | port, err = strconv.Atoi(s) 344 | if err != nil { 345 | return nil, err 346 | } 347 | } 348 | if s := ss[0][2]; s != "" { 349 | table, err = strconv.Atoi(s) 350 | if err != nil { 351 | return nil, err 352 | } 353 | } 354 | 355 | return Resubmit(port, table), nil 356 | } 357 | 358 | // ActionResubmitPort, with only a port number 359 | if ss := resubmitPortRe.FindAllStringSubmatch(s, 1); len(ss) > 0 && len(ss[0]) == 2 { 360 | port, err := strconv.Atoi(ss[0][1]) 361 | if err != nil { 362 | return nil, err 363 | } 364 | 365 | return ResubmitPort(port), nil 366 | } 367 | 368 | if ss := loadRe.FindAllStringSubmatch(s, 2); len(ss) > 0 && len(ss[0]) == 3 { 369 | // Results are: 370 | // - full string 371 | // - value 372 | // - field 373 | return Load(ss[0][1], ss[0][2]), nil 374 | } 375 | 376 | if ss := moveRe.FindAllStringSubmatch(s, 2); len(ss) > 0 && len(ss[0]) == 3 { 377 | // Results are: 378 | // - full string 379 | // - value 380 | // - field 381 | return Move(ss[0][1], ss[0][2]), nil 382 | } 383 | 384 | if ss := setFieldRe.FindAllStringSubmatch(s, 2); len(ss) > 0 && len(ss[0]) == 3 { 385 | // Results are: 386 | // - full string 387 | // - value 388 | // - field 389 | return SetField(ss[0][1], ss[0][2]), nil 390 | } 391 | 392 | return nil, fmt.Errorf("no action matched for %q", s) 393 | } 394 | -------------------------------------------------------------------------------- /ovs/actionparser_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "net" 19 | "reflect" 20 | "strings" 21 | "testing" 22 | ) 23 | 24 | func Test_actionParser(t *testing.T) { 25 | var tests = []struct { 26 | name string 27 | in string 28 | raw []string 29 | invalid bool 30 | }{ 31 | { 32 | name: "invalid action", 33 | in: "strip_vlan,resubmit(", 34 | invalid: true, 35 | }, 36 | { 37 | name: "one action", 38 | in: "strip_vlan", 39 | raw: []string{ 40 | "strip_vlan", 41 | }, 42 | }, 43 | { 44 | name: "two actions", 45 | in: "strip_vlan,resubmit(,1)", 46 | raw: []string{ 47 | "strip_vlan", 48 | "resubmit(,1)", 49 | }, 50 | }, 51 | { 52 | name: "action with nested parentheses", 53 | in: "strip_vlan,resubmit(,1),ct(commit,exec(set_field:1->ct_label,set_field:1->ct_mark))", 54 | raw: []string{ 55 | "strip_vlan", 56 | "resubmit(,1)", 57 | "ct(commit,exec(set_field:1->ct_label,set_field:1->ct_mark))", 58 | }, 59 | }, 60 | } 61 | 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | p := newActionParser(strings.NewReader(tt.in)) 65 | actions, raw, err := p.Parse() 66 | if err != nil { 67 | if tt.invalid { 68 | return 69 | } 70 | 71 | t.Fatalf("unexpected error during parsing: %v", err) 72 | } 73 | 74 | if want, got := tt.raw, raw; !reflect.DeepEqual(want, got) { 75 | t.Fatalf("unexpected raw actions:\n- want: %v\n- got: %v", 76 | want, got) 77 | } 78 | 79 | as, err := marshalActions(actions) 80 | if err != nil { 81 | t.Fatalf("unexpected error: %v", err) 82 | } 83 | 84 | if want, got := raw, as; !reflect.DeepEqual(want, got) { 85 | t.Fatalf("unexpected actions after parsing:\n- want: %v\n- got: %v", 86 | want, got) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func Test_parseAction(t *testing.T) { 93 | var tests = []struct { 94 | desc string 95 | s string 96 | final string 97 | a Action 98 | invalid bool 99 | }{ 100 | { 101 | s: "foo", 102 | invalid: true, 103 | }, 104 | { 105 | s: "drop", 106 | a: Drop(), 107 | }, 108 | { 109 | s: "flood", 110 | a: Flood(), 111 | }, 112 | { 113 | s: "in_port", 114 | a: InPort(), 115 | }, 116 | { 117 | s: "local", 118 | a: Local(), 119 | }, 120 | { 121 | s: "LOCAL", 122 | a: Local(), 123 | }, 124 | { 125 | s: "normal", 126 | a: Normal(), 127 | }, 128 | { 129 | s: "NORMAL", 130 | a: Normal(), 131 | }, 132 | { 133 | s: "strip_vlan", 134 | a: StripVLAN(), 135 | }, 136 | { 137 | s: "ct()", 138 | invalid: true, 139 | }, 140 | { 141 | s: "ct(commit)", 142 | a: ConnectionTracking("commit"), 143 | }, 144 | { 145 | s: "mod_dl_dst:foo", 146 | invalid: true, 147 | }, 148 | { 149 | s: "mod_dl_dst:de:ad:be:ef:de:ad", 150 | a: ModDataLinkDestination(net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}), 151 | }, 152 | { 153 | s: "mod_dl_src:de:ad:be:ef:de:ad", 154 | a: ModDataLinkSource(net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}), 155 | }, 156 | { 157 | s: "mod_nw_dst:foo", 158 | invalid: true, 159 | }, 160 | { 161 | s: "mod_nw_dst:2001:db8::1", 162 | invalid: true, 163 | }, 164 | { 165 | s: "mod_nw_dst:192.168.1.1", 166 | a: ModNetworkDestination(net.IPv4(192, 168, 1, 1)), 167 | }, 168 | { 169 | s: "mod_nw_src:foo", 170 | invalid: true, 171 | }, 172 | { 173 | s: "mod_nw_src:2001:db8::1", 174 | invalid: true, 175 | }, 176 | { 177 | s: "mod_nw_src:192.168.1.1", 178 | a: ModNetworkSource(net.IPv4(192, 168, 1, 1)), 179 | }, 180 | { 181 | s: "mod_tp_dst:foo", 182 | invalid: true, 183 | }, 184 | { 185 | s: "mod_tp_dst:-1", 186 | invalid: true, 187 | }, 188 | { 189 | s: "mod_tp_dst:65536", 190 | invalid: true, 191 | }, 192 | { 193 | s: "mod_tp_dst:65535", 194 | a: ModTransportDestinationPort(65535), 195 | }, 196 | { 197 | s: "mod_tp_src:foo", 198 | invalid: true, 199 | }, 200 | { 201 | s: "mod_tp_src:-1", 202 | invalid: true, 203 | }, 204 | { 205 | s: "mod_tp_src:65536", 206 | invalid: true, 207 | }, 208 | { 209 | s: "mod_tp_src:65535", 210 | a: ModTransportSourcePort(65535), 211 | }, 212 | { 213 | s: "mod_vlan_vid:foo", 214 | invalid: true, 215 | }, 216 | { 217 | s: "mod_vlan_vid:10", 218 | a: ModVLANVID(10), 219 | }, 220 | { 221 | s: "output:foo", 222 | invalid: true, 223 | }, 224 | { 225 | s: "output:1", 226 | a: Output(1), 227 | }, 228 | { 229 | s: "resubmit(foo,)", 230 | invalid: true, 231 | }, 232 | { 233 | s: "resubmit(,bar)", 234 | invalid: true, 235 | }, 236 | { 237 | s: "resubmit(foo,bar)", 238 | invalid: true, 239 | }, 240 | { 241 | s: "resubmit:4", 242 | a: ResubmitPort(4), 243 | }, 244 | { 245 | s: "resubmit(1,)", 246 | a: Resubmit(1, 0), 247 | }, 248 | { 249 | s: "resubmit(,2)", 250 | a: Resubmit(0, 2), 251 | }, 252 | { 253 | s: "resubmit(1,2)", 254 | a: Resubmit(1, 2), 255 | }, 256 | { 257 | s: "resubmit(,25)", 258 | a: Resubmit(0, 25), 259 | }, 260 | { 261 | s: "load:->NXM_OF_ARP_OP[]", 262 | invalid: true, 263 | }, 264 | { 265 | s: "load:0x2->", 266 | invalid: true, 267 | }, 268 | { 269 | s: "load:0x2->NXM_OF_ARP_OP[]", 270 | a: Load("0x2", "NXM_OF_ARP_OP[]"), 271 | }, 272 | { 273 | s: "move:->NXM_OF_ARP_OP[]", 274 | invalid: true, 275 | }, 276 | { 277 | s: "move:NXM_OF_ARP_SPA[]->", 278 | invalid: true, 279 | }, 280 | { 281 | s: "move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[]", 282 | a: Move("move:NXM_OF_ARP_SPA[]", "NXM_OF_ARP_TPA[]"), 283 | }, 284 | { 285 | s: "set_field:->arp_spa", 286 | invalid: true, 287 | }, 288 | { 289 | s: "set_field:192.168.1.1->", 290 | invalid: true, 291 | }, 292 | { 293 | s: "set_field:192.168.1.1->arp_spa", 294 | a: SetField("192.168.1.1", "arp_spa"), 295 | }, 296 | { 297 | s: "conjunction(123,1/2)", 298 | a: Conjunction(123, 1, 2), 299 | }, 300 | { 301 | s: "conjunction(123,2/2)", 302 | a: Conjunction(123, 2, 2), 303 | }, 304 | { 305 | s: "conjunction(123,3/2)", 306 | invalid: true, 307 | }, 308 | { 309 | s: "conjunxxxxx(123,3/2)", 310 | invalid: true, 311 | }, 312 | } 313 | 314 | for _, tt := range tests { 315 | t.Run(tt.s, func(t *testing.T) { 316 | a, err := parseAction(tt.s) 317 | if err != nil && !tt.invalid { 318 | t.Fatalf("unexpected error: %v", err) 319 | } 320 | if tt.invalid { 321 | return 322 | } 323 | 324 | s, err := a.MarshalText() 325 | if err != nil { 326 | t.Fatalf("unexpected error: %v", err) 327 | } 328 | 329 | // Special case: LOCAL and NORMAL are converted to 330 | // the lower case counterpart by this package for 331 | // consistency. 332 | want := tt.s 333 | switch want { 334 | case "LOCAL", "NORMAL": 335 | want = strings.ToLower(want) 336 | } 337 | 338 | if tt.final != "" { 339 | want = tt.final 340 | } 341 | 342 | if got := string(s); want != got { 343 | t.Fatalf("unexpected action:\n- want: %q\n- got: %q", 344 | want, got) 345 | } 346 | }) 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /ovs/app.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | // AppService runs commands that are available from ovs-appctl 23 | type AppService struct { 24 | c *Client 25 | } 26 | 27 | // ProtoTrace runs ovs-appctl ofproto/trace on the given bridge and match flow 28 | // with the possibility to pass extra parameters like `--ct-next` and returns a *ProtoTrace. 29 | // Also returns err if there is any error parsing the output from ovs-appctl ofproto/trace. 30 | func (a *AppService) ProtoTrace(bridge string, protocol Protocol, matches []Match, params ...string) (*ProtoTrace, error) { 31 | matchFlows := []string{} 32 | if protocol != "" { 33 | matchFlows = append(matchFlows, string(protocol)) 34 | } 35 | 36 | for _, match := range matches { 37 | matchFlow, err := match.MarshalText() 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | matchFlows = append(matchFlows, string(matchFlow)) 43 | } 44 | 45 | matchArg := strings.Join(matchFlows, ",") 46 | args := []string{"ofproto/trace", bridge, matchArg} 47 | args = append(args, params...) 48 | out, err := a.exec(args...) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | pt := &ProtoTrace{ 54 | CommandStr: fmt.Sprintf("ovs-appctl %s", strings.Join(args, " ")), 55 | RawOutput: out, 56 | } 57 | err = pt.UnmarshalText(out) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return pt, nil 63 | } 64 | 65 | // exec executes 'ovs-appctl' + args passed in 66 | func (a *AppService) exec(args ...string) ([]byte, error) { 67 | return a.c.exec("ovs-appctl", args...) 68 | } 69 | -------------------------------------------------------------------------------- /ovs/client.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "io" 21 | "io/ioutil" 22 | "log" 23 | "os/exec" 24 | "strings" 25 | ) 26 | 27 | // A Client is a client type which enables programmatic control of Open 28 | // vSwitch. 29 | type Client struct { 30 | // OpenFlow wraps functionality of the 'ovs-ofctl' binary. 31 | OpenFlow *OpenFlowService 32 | 33 | // App wraps functionality of the 'ovs-appctl' binary 34 | App *AppService 35 | 36 | // VSwitch wraps functionality of the 'ovs-vsctl' binary. 37 | VSwitch *VSwitchService 38 | 39 | // DataPath wraps functionality of the 'ovs-dpctl' binary 40 | DataPath *DataPathService 41 | 42 | // Additional flags applied to all OVS actions, such as timeouts 43 | // or retries. 44 | flags []string 45 | 46 | // Additional flags applied to 'ovs-ofctl' commands. 47 | ofctlFlags []string 48 | 49 | // Enable or disable debugging log messages for OVS commands. 50 | debug bool 51 | 52 | // Prefix all commands with "sudo". 53 | sudo bool 54 | 55 | // Implementation of ExecFunc. 56 | execFunc ExecFunc 57 | 58 | // Implementation of PipeFunc. 59 | pipeFunc PipeFunc 60 | } 61 | 62 | // An ExecFunc is a function which accepts input arguments and returns raw 63 | // byte output and an error. ExecFuncs are swappable to enable testing 64 | // without OVS installed. 65 | type ExecFunc func(cmd string, args ...string) ([]byte, error) 66 | 67 | // shellExec is an ExecFunc which shells out to the binary cmd using the 68 | // arguments args, and returns its combined stdout and stderr and any errors 69 | // which may have occurred. 70 | func shellExec(cmd string, args ...string) ([]byte, error) { 71 | return exec.Command(cmd, args...).CombinedOutput() 72 | } 73 | 74 | // exec executes an ExecFunc using the values from cmd and args. 75 | // The ExecFunc may shell out to an appropriate binary, or may be swapped 76 | // for testing. 77 | func (c *Client) exec(cmd string, args ...string) ([]byte, error) { 78 | // Prepend recurring flags before arguments 79 | flags := append(c.flags, args...) 80 | 81 | // If needed, prefix sudo. 82 | if c.sudo { 83 | flags = append([]string{cmd}, flags...) 84 | cmd = "sudo" 85 | } 86 | 87 | c.debugf("exec: %s %v", cmd, flags) 88 | 89 | // Execute execFunc with all flags and clean up any whitespace or 90 | // newlines from its output. 91 | out, err := c.execFunc(cmd, flags...) 92 | if out != nil { 93 | out = bytes.TrimSpace(out) 94 | c.debugf("exec: %q", string(out)) 95 | } 96 | if err != nil { 97 | // Wrap errors in Error type for further introspection 98 | return nil, &Error{ 99 | Out: out, 100 | Err: err, 101 | } 102 | } 103 | 104 | return out, nil 105 | } 106 | 107 | // A PipeFunc is a function which accepts an input stdin stream, command, 108 | // and arguments, and returns command output and an error. PipeFuncs are 109 | // swappable to enable testing without OVS installed. 110 | type PipeFunc func(stdin io.Reader, cmd string, args ...string) ([]byte, error) 111 | 112 | // shellPipe is a PipeFunc which shells out to the binary cmd using the arguments 113 | // args, and writing to the command's stdin using stdin. 114 | func shellPipe(stdin io.Reader, cmd string, args ...string) ([]byte, error) { 115 | command := exec.Command(cmd, args...) 116 | 117 | stdout, err := command.StdoutPipe() 118 | if err != nil { 119 | return nil, err 120 | } 121 | stderr, err := command.StderrPipe() 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | wc, err := command.StdinPipe() 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | if err := command.Start(); err != nil { 132 | return nil, err 133 | } 134 | 135 | if _, err := io.Copy(wc, stdin); err != nil { 136 | return nil, err 137 | } 138 | 139 | // Needed to indicate to ovs-ofctl that stdin is done being read. 140 | // "... if the command being run will not exit until standard input is 141 | // closed, the caller must close the pipe." 142 | // Reference: https://golang.org/pkg/os/exec/#Cmd.StdinPipe 143 | if err := wc.Close(); err != nil { 144 | return nil, err 145 | } 146 | 147 | mr := io.MultiReader(stdout, stderr) 148 | b, err := ioutil.ReadAll(mr) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | return b, command.Wait() 154 | } 155 | 156 | // pipe executes a PipeFunc using the values from stdin, cmd, and args. 157 | // stdin is used to feed input data to the stdin of a forked process. 158 | // The PipeFunc may shell out to an appropriate binary, or may be swapped 159 | // for testing. 160 | func (c *Client) pipe(stdin io.Reader, cmd string, args ...string) error { 161 | // Prepend recurring flags before arguments 162 | flags := append(c.flags, args...) 163 | 164 | // If needed, prefix sudo. 165 | if c.sudo { 166 | flags = append([]string{cmd}, flags...) 167 | cmd = "sudo" 168 | } 169 | 170 | c.debugf("pipe: %s %v", cmd, flags) 171 | c.debugf("bundle:") 172 | 173 | tr := io.TeeReader(stdin, writerFunc(func(p []byte) (int, error) { 174 | c.debugf("%s", string(p)) 175 | return len(p), nil 176 | })) 177 | 178 | if out, err := c.pipeFunc(tr, cmd, flags...); err != nil { 179 | c.debugf("pipe error: %v: %q", err, string(out)) 180 | return &pipeError{ 181 | out: out, 182 | err: err, 183 | } 184 | } 185 | 186 | return nil 187 | 188 | } 189 | 190 | // A pipeError is an error returned by Client.pipe, containing combined 191 | // stdout/stderr from a process as well as its error. 192 | type pipeError struct { 193 | out []byte 194 | err error 195 | } 196 | 197 | // Error returns the string representation of a pipeError. 198 | func (e *pipeError) Error() string { 199 | return fmt.Sprintf("pipe error: %v: %q", e.err, string(e.out)) 200 | } 201 | 202 | // debugf prints a logging debug message when debugging is enabled. 203 | func (c *Client) debugf(format string, a ...interface{}) { 204 | if !c.debug { 205 | return 206 | } 207 | 208 | log.Printf("ovs: "+format, a...) 209 | } 210 | 211 | // New creates a new Client with zero or more OptionFunc configurations 212 | // applied. 213 | func New(options ...OptionFunc) *Client { 214 | // Always execute and pipe using shell when created with New. 215 | c := &Client{ 216 | flags: make([]string, 0), 217 | ofctlFlags: make([]string, 0), 218 | execFunc: shellExec, 219 | pipeFunc: shellPipe, 220 | } 221 | for _, o := range options { 222 | o(c) 223 | } 224 | 225 | vss := &VSwitchService{ 226 | c: c, 227 | } 228 | vss.Get = &VSwitchGetService{ 229 | v: vss, 230 | } 231 | vss.Set = &VSwitchSetService{ 232 | v: vss, 233 | } 234 | c.VSwitch = vss 235 | 236 | ofs := &OpenFlowService{ 237 | c: c, 238 | } 239 | c.OpenFlow = ofs 240 | 241 | app := &AppService{ 242 | c: c, 243 | } 244 | c.App = app 245 | 246 | c.DataPath = &DataPathService{ 247 | CLI: &DpCLI{ 248 | c: c, 249 | }, 250 | } 251 | 252 | return c 253 | } 254 | 255 | // An OptionFunc is a function which can apply configuration to a Client. 256 | type OptionFunc func(c *Client) 257 | 258 | // Timeout returns an OptionFunc which sets a timeout in seconds for all 259 | // Open vSwitch interactions. 260 | func Timeout(seconds int) OptionFunc { 261 | return func(c *Client) { 262 | c.flags = append(c.flags, fmt.Sprintf("--timeout=%d", seconds)) 263 | } 264 | } 265 | 266 | // Debug returns an OptionFunc which enables debugging output for the Client 267 | // type. 268 | func Debug(enable bool) OptionFunc { 269 | return func(c *Client) { 270 | c.debug = enable 271 | } 272 | } 273 | 274 | // Exec returns an OptionFunc which sets an ExecFunc for use with a Client. 275 | // This function should typically only be used in tests. 276 | func Exec(fn ExecFunc) OptionFunc { 277 | return func(c *Client) { 278 | c.execFunc = fn 279 | } 280 | } 281 | 282 | // Pipe returns an OptionFunc which sets a PipeFunc for use with a Client. 283 | // This function should typically only be used in tests. 284 | func Pipe(fn PipeFunc) OptionFunc { 285 | return func(c *Client) { 286 | c.pipeFunc = fn 287 | } 288 | } 289 | 290 | const ( 291 | // FlowFormatNXMTableID is a flow format which allows Nicira Extended match 292 | // with the ability to place a flow in a specific table. 293 | FlowFormatNXMTableID = "NXM+table_id" 294 | 295 | // FlowFormatOXMOpenFlow14 is a flow format which allows Open vSwitch 296 | // extensible match. 297 | FlowFormatOXMOpenFlow14 = "OXM-OpenFlow14" 298 | ) 299 | 300 | // FlowFormat specifies the flow format to be used when shelling to 301 | // 'ovs-ofctl'. 302 | func FlowFormat(format string) OptionFunc { 303 | return func(c *Client) { 304 | c.ofctlFlags = append(c.ofctlFlags, fmt.Sprintf("--flow-format=%s", format)) 305 | } 306 | } 307 | 308 | // Protocol constants for use with Protocols and BridgeOptions. 309 | const ( 310 | ProtocolOpenFlow10 = "OpenFlow10" 311 | ProtocolOpenFlow11 = "OpenFlow11" 312 | ProtocolOpenFlow12 = "OpenFlow12" 313 | ProtocolOpenFlow13 = "OpenFlow13" 314 | ProtocolOpenFlow14 = "OpenFlow14" 315 | ProtocolOpenFlow15 = "OpenFlow15" 316 | ) 317 | 318 | // Protocols specifies one or more OpenFlow protocol versions to be used when shelling 319 | // to 'ovs-ofctl'. 320 | func Protocols(versions []string) OptionFunc { 321 | return func(c *Client) { 322 | c.ofctlFlags = append(c.ofctlFlags, 323 | fmt.Sprintf("--protocols=%s", strings.Join(versions, ",")), 324 | ) 325 | } 326 | } 327 | 328 | // SetSSLParam configures SSL authentication using a private key, certificate, 329 | // and CA certificate for use with ovs-ofctl. 330 | func SetSSLParam(pkey string, cert string, cacert string) OptionFunc { 331 | return func(c *Client) { 332 | c.ofctlFlags = append(c.ofctlFlags, fmt.Sprintf("--private-key=%s", pkey), 333 | fmt.Sprintf("--certificate=%s", cert), fmt.Sprintf("--ca-cert=%s", cacert)) 334 | } 335 | } 336 | 337 | // SetTCPParam configures the OVSDB connection using a TCP format ip:port 338 | // for use with all ovs-vsctl commands. 339 | func SetTCPParam(addr string) OptionFunc { 340 | return func(c *Client) { 341 | c.flags = append(c.flags, fmt.Sprintf("--db=tcp:%s", addr)) 342 | } 343 | } 344 | 345 | // Sudo specifies that "sudo" should be prefixed to all OVS commands. 346 | func Sudo() OptionFunc { 347 | return func(c *Client) { 348 | c.sudo = true 349 | } 350 | } 351 | 352 | // A writerFunc is an adapter for a function to be used as an io.Writer. 353 | type writerFunc func(p []byte) (n int, err error) 354 | 355 | func (fn writerFunc) Write(p []byte) (int, error) { 356 | return fn(p) 357 | } 358 | -------------------------------------------------------------------------------- /ovs/client_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bytes" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestNew(t *testing.T) { 24 | var tests = []struct { 25 | desc string 26 | options []OptionFunc 27 | c *Client 28 | }{ 29 | { 30 | desc: "no options", 31 | c: &Client{ 32 | flags: make([]string, 0), 33 | ofctlFlags: make([]string, 0), 34 | debug: false, 35 | }, 36 | }, 37 | { 38 | desc: "Timeout(2)", 39 | options: []OptionFunc{Timeout(2)}, 40 | c: &Client{ 41 | flags: []string{"--timeout=2"}, 42 | ofctlFlags: make([]string, 0), 43 | debug: false, 44 | }, 45 | }, 46 | { 47 | desc: "Debug(true)", 48 | options: []OptionFunc{Debug(true)}, 49 | c: &Client{ 50 | flags: make([]string, 0), 51 | ofctlFlags: make([]string, 0), 52 | debug: true, 53 | }, 54 | }, 55 | { 56 | desc: "FlowFormat(FlowFormatNXMTableID)", 57 | options: []OptionFunc{FlowFormat(FlowFormatNXMTableID)}, 58 | c: &Client{ 59 | flags: make([]string, 0), 60 | ofctlFlags: []string{"--flow-format=NXM+table_id"}, 61 | }, 62 | }, 63 | { 64 | desc: "Protocols([]string{ProtocolOpenFlow14})", 65 | options: []OptionFunc{Protocols([]string{ProtocolOpenFlow14})}, 66 | c: &Client{ 67 | flags: make([]string, 0), 68 | ofctlFlags: []string{"--protocols=OpenFlow14"}, 69 | }, 70 | }, 71 | { 72 | desc: "Timeout(5), Debug(true)", 73 | options: []OptionFunc{ 74 | Timeout(5), 75 | Debug(true), 76 | }, 77 | c: &Client{ 78 | flags: []string{"--timeout=5"}, 79 | ofctlFlags: make([]string, 0), 80 | debug: true, 81 | }, 82 | }, 83 | { 84 | desc: "Sudo()", 85 | options: []OptionFunc{ 86 | Sudo(), 87 | }, 88 | c: &Client{ 89 | flags: make([]string, 0), 90 | ofctlFlags: make([]string, 0), 91 | sudo: true, 92 | }, 93 | }, 94 | { 95 | desc: "SetSSLParam(pkey, cert, cacert)", 96 | options: []OptionFunc{ 97 | SetSSLParam("privkey.pem", "cert.pem", "cacert.pem"), 98 | }, 99 | c: &Client{ 100 | flags: make([]string, 0), 101 | ofctlFlags: []string{"--private-key=privkey.pem", "--certificate=cert.pem", "--ca-cert=cacert.pem"}, 102 | }, 103 | }, 104 | { 105 | desc: "SetTCPParam(addr)", 106 | options: []OptionFunc{ 107 | SetTCPParam("127.0.0.1:6640"), 108 | }, 109 | c: &Client{ 110 | flags: []string{"--db=tcp:127.0.0.1:6640"}, 111 | ofctlFlags: make([]string, 0), 112 | }, 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | t.Run(tt.desc, func(t *testing.T) { 118 | c := New(tt.options...) 119 | 120 | if want, got := tt.c.flags, c.flags; !reflect.DeepEqual(want, got) { 121 | t.Fatalf("unexpected Client.flags:\n- want: %v\n- got: %v", 122 | want, got) 123 | } 124 | 125 | if want, got := tt.c.debug, c.debug; !reflect.DeepEqual(want, got) { 126 | t.Fatalf("unexpected Client.debug:\n- want: %v\n- got: %v", 127 | want, got) 128 | } 129 | if want, got := tt.c.sudo, c.sudo; !reflect.DeepEqual(want, got) { 130 | t.Fatalf("unexpected Client.sudo:\n- want: %v\n- got: %v", 131 | want, got) 132 | } 133 | 134 | if want, got := tt.c.ofctlFlags, c.ofctlFlags; !reflect.DeepEqual(want, got) { 135 | t.Fatalf("unexpected Client.ofctlFlags:\n- want: %v\n- got: %v", 136 | want, got) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func Test_shellPipe(t *testing.T) { 143 | b := bytes.TrimSpace([]byte(` 144 | foo 145 | bar 146 | baz 147 | `)) 148 | 149 | // stdin pipe must be consumed. This test will hang if broken. 150 | buf := bytes.NewBuffer(b) 151 | out, err := shellPipe(buf, "cat", "-") 152 | if err != nil { 153 | t.Fatalf("failed to pipe to cat: %v", err) 154 | } 155 | 156 | if want, got := b, out; !bytes.Equal(want, got) { 157 | t.Fatalf("unexpected bytes:\n- want: %v\n- got: %v", 158 | want, got) 159 | } 160 | } 161 | 162 | // testClient creates a new Client with the specified OptionFuncs applied and 163 | // using the specified ExecFunc. 164 | func testClient(options []OptionFunc, fn ExecFunc) *Client { 165 | options = append(options, Exec(fn)) 166 | c := New(options...) 167 | return c 168 | } 169 | -------------------------------------------------------------------------------- /ovs/codegen.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "net" 21 | "strconv" 22 | ) 23 | 24 | // hwAddrGoString converts a net.HardwareAddr into its Go syntax representation. 25 | func hwAddrGoString(addr net.HardwareAddr) string { 26 | buf := bytes.NewBufferString("net.HardwareAddr{") 27 | for i, b := range addr { 28 | _, _ = buf.WriteString(fmt.Sprintf("0x%02x", b)) 29 | 30 | if i != len(addr)-1 { 31 | _, _ = buf.WriteString(", ") 32 | } 33 | } 34 | _, _ = buf.WriteString("}") 35 | 36 | return buf.String() 37 | } 38 | 39 | // ipv4GoString converts a net.IP (IPv4 only) into its Go syntax representation. 40 | func ipv4GoString(ip net.IP) string { 41 | ip4 := ip.To4() 42 | if ip4 == nil { 43 | return `panic("invalid IPv4 address")` 44 | } 45 | 46 | buf := bytes.NewBufferString("net.IPv4(") 47 | for i, b := range ip4 { 48 | _, _ = buf.WriteString(strconv.Itoa(int(b))) 49 | 50 | if i != len(ip4)-1 { 51 | _, _ = buf.WriteString(", ") 52 | } 53 | } 54 | _, _ = buf.WriteString(")") 55 | 56 | return buf.String() 57 | } 58 | 59 | // bprintf is fmt.Sprintf, but it returns a byte slice instead of a string. 60 | func bprintf(format string, a ...interface{}) []byte { 61 | return []byte(fmt.Sprintf(format, a...)) 62 | } 63 | -------------------------------------------------------------------------------- /ovs/datapath.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 ovs 16 | 17 | import ( 18 | "errors" 19 | "regexp" 20 | "strconv" 21 | "strings" 22 | ) 23 | 24 | var ( 25 | errMissingMandatoryDataPathName = errors.New("datapath name argument is mandatory") 26 | errUninitializedClient = errors.New("client unitialized") 27 | errMissingMandatoryZone = errors.New("at least 1 zone is mandatory") 28 | errWrongArgumentNumber = errors.New("missing or too many arguments to setup ct limits") 29 | errWrongDefaultArgument = errors.New("wrong argument while setting default ct limits") 30 | errWrongZoneArgument = errors.New("wrong argument while setting zone ct limits") 31 | ) 32 | 33 | // CTLimit defines the type used to store a zone as it is returned 34 | // by ovs-dpctl ct-*-limits commands 35 | type CTLimit map[string]uint64 36 | 37 | // ConntrackOutput is a type defined to store the output 38 | // of ovs-dpctl ct-*-limits commands. For example it stores 39 | // such a cli output: 40 | // # ovs-dpctl ct-get-limits system@ovs-system zone=2,3 41 | // default limit=0 42 | // zone=2,limit=0,count=0 43 | // zone=3,limit=0,count=0 44 | type ConntrackOutput struct { 45 | // defaultLimit is used to store the global setting: default 46 | defaultLimit CTLimit 47 | // zones stores all remaning zone's settings 48 | zoneLimits []CTLimit 49 | } 50 | 51 | // DataPathReader is the interface defining the read operations 52 | // for the ovs DataPaths 53 | type DataPathReader interface { 54 | // Version is the method used to get the version of ovs-dpctl 55 | Version() (string, error) 56 | // GetDataPath is the method that returns all DataPaths setup 57 | // for an ovs switch 58 | GetDataPath() ([]string, error) 59 | } 60 | 61 | // DataPathWriter is the interface defining the wrtie operations 62 | // for the ovs DataPaths 63 | type DataPathWriter interface { 64 | // AddDataPath is the method used to add a datapath to the switch 65 | AddDataPath(string) error 66 | // DelDataPath is the method used to remove a datapath from the switch 67 | DelDataPath(string) error 68 | } 69 | 70 | // ConnTrackReader is the interface defining the read operations 71 | // of ovs conntrack 72 | type ConnTrackReader interface { 73 | // GetCTLimits is the method used to querying conntrack limits for a 74 | // datapath on a switch 75 | GetCTLimits(string, []uint64) (ConntrackOutput, error) 76 | } 77 | 78 | // ConnTrackWriter is the interface defining the write operations 79 | // of ovs conntrack 80 | type ConnTrackWriter interface { 81 | // SetCTLimits is the method used to setup a limit for a zone 82 | // belonging to a datapath of a switch 83 | SetCTLimits(string) (string, error) 84 | // DelCTLimits is the method used to remove a limit to a zone 85 | // belonging to a datapath of a switch 86 | DelCTLimits(string, []uint64) (string, error) 87 | } 88 | 89 | // CLI is an interface defining a contract for executing a command. 90 | // Implementation of shell cli is done by the Client concrete type 91 | type CLI interface { 92 | Exec(args ...string) ([]byte, error) 93 | } 94 | 95 | // DataPathService defines the concrete type used for DataPath operations 96 | // supported by the ovs-dpctl command 97 | type DataPathService struct { 98 | // We define here a CLI interface making easier to mock ovs-dpctl command 99 | // as in github.com/digitalocean/go-openvswitch/ovs/datapath_test.go 100 | CLI 101 | } 102 | 103 | // NewDataPathService is a builder for the DataPathService. 104 | // sudo is defined as a default option. 105 | func NewDataPathService() *DataPathService { 106 | return &DataPathService{ 107 | CLI: &DpCLI{ 108 | c: New(Sudo()), 109 | }, 110 | } 111 | } 112 | 113 | // Version retruns the ovs-dptcl --version currently installed 114 | func (dp *DataPathService) Version() (string, error) { 115 | result, err := dp.CLI.Exec("--version") 116 | if err != nil { 117 | return "", err 118 | } 119 | 120 | return string(result), nil 121 | } 122 | 123 | // GetDataPaths returns the output of the command 'ovs-dpctl dump-dps' 124 | func (dp *DataPathService) GetDataPaths() ([]string, error) { 125 | result, err := dp.CLI.Exec("dump-dps") 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | return strings.Split(string(result), "\n"), nil 131 | } 132 | 133 | // AddDataPath create a Datapath with the command 'ovs-dpctl add-dp ' 134 | // It takes one argument, the required DataPath Name and returns an error 135 | // if it failed 136 | func (dp *DataPathService) AddDataPath(dpName string) error { 137 | _, err := dp.CLI.Exec("add-dp", dpName) 138 | return err 139 | } 140 | 141 | // DelDataPath create a Datapath with the command 'ovs-dpctl del-dp ' 142 | // It takes one argument, the required DataPath Name and returns an error 143 | // if it failed 144 | func (dp *DataPathService) DelDataPath(dpName string) error { 145 | _, err := dp.CLI.Exec("del-dp", dpName) 146 | 147 | return err 148 | } 149 | 150 | // GetCTLimits returns the conntrack limits for a given datapath 151 | // equivalent to running: 'sudo ovs-dpctl ct-get-limits zone=<#1>,<#2>,...' 152 | func (dp *DataPathService) GetCTLimits(dpName string, zones []uint64) (*ConntrackOutput, error) { 153 | // Start by building the args 154 | if dpName == "" { 155 | return nil, errMissingMandatoryDataPathName 156 | } 157 | 158 | args := []string{"ct-get-limits", dpName} 159 | 160 | zoneParam := getZoneString(zones) 161 | if zoneParam != "" { 162 | args = append(args, zoneParam) 163 | } 164 | 165 | // call the cli 166 | results, err := dp.CLI.Exec(args...) 167 | if err != nil { 168 | return nil, err 169 | } 170 | 171 | // Process the results 172 | entries := strings.Split(string(results), "\n") 173 | ctOut := &ConntrackOutput{} 174 | 175 | r, err := regexp.Compile(`default`) 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | // First start extracting the default conntrack limit setup 181 | // If found the default value is removed from the entries 182 | for i, entry := range entries { 183 | if r.MatchString(entry) { 184 | ctOut.defaultLimit = make(CTLimit) 185 | limit, err := strconv.Atoi(strings.Split(entry, "=")[1]) 186 | if err != nil { 187 | return nil, err 188 | } 189 | ctOut.defaultLimit["default"] = uint64(limit) 190 | // As the default has been found let's remove it 191 | entries = append(entries[:i], entries[i+1:]...) 192 | } 193 | } 194 | 195 | // Now process the zones setup 196 | for _, entry := range entries { 197 | fields := strings.Split(entry, ",") 198 | z := make(CTLimit) 199 | for _, field := range fields { 200 | buf := strings.Split(field, "=") 201 | val, _ := strconv.Atoi(buf[1]) 202 | z[buf[0]] = uint64(val) 203 | } 204 | ctOut.zoneLimits = append(ctOut.zoneLimits, z) 205 | } 206 | 207 | return ctOut, nil 208 | } 209 | 210 | // SetCTLimits set the limit for a specific zone or globally. 211 | // Only one zone or default can be set up at once as the cli allows. 212 | // Examples of commands it wrapps: 213 | // sudo ovs-dpctl ct-set-limits system@ovs-system zone=331,limit=1000000 214 | // sudo ovs-dpctl ct-set-limits system@ovs-system default=1000000 215 | func (dp *DataPathService) SetCTLimits(dpName string, zone map[string]uint64) (string, error) { 216 | // Sanitize the input 217 | if dpName == "" { 218 | return "", errMissingMandatoryDataPathName 219 | } 220 | argsStr, err := ctSetLimitsArgsToString(zone) 221 | if err != nil { 222 | return "", err 223 | } 224 | // call the cli 225 | argsCLI := []string{"ct-set-limits", dpName, argsStr} 226 | results, err := dp.CLI.Exec(argsCLI...) 227 | 228 | return string(results), err 229 | } 230 | 231 | // DelCTLimits deletes limits setup for zones. It takes the Datapath name 232 | // and zones to delete the limits. 233 | // sudo ovs-dpctl ct-del-limits system@ovs-system zone=40,4 234 | func (dp *DataPathService) DelCTLimits(dpName string, zones []uint64) (string, error) { 235 | if dpName == "" { 236 | return "", errMissingMandatoryDataPathName 237 | } 238 | if len(zones) < 1 { 239 | return "", errMissingMandatoryZone 240 | } 241 | 242 | var firstZone uint64 243 | firstZone, zones = zones[0], zones[1:] 244 | zonesStr := "zone=" + strconv.FormatUint(firstZone, 10) 245 | for _, z := range zones { 246 | zonesStr += "," + strconv.FormatUint(z, 10) 247 | } 248 | 249 | // call the cli 250 | argsCLI := []string{"ct-del-limits", dpName, zonesStr} 251 | results, err := dp.CLI.Exec(argsCLI...) 252 | 253 | return string(results), err 254 | } 255 | 256 | // ctSetLimitsArgsToString helps formating and sanatizing an input 257 | // It takes a map and output a string like this: 258 | // - "zone=2,limit=10000" or "limit=10000,zone=2" 259 | // - "default=10000" 260 | func ctSetLimitsArgsToString(zone map[string]uint64) (string, error) { 261 | defaultSetup := false 262 | args := make([]string, 0) 263 | for k, v := range zone { 264 | if k == "default" { 265 | args = append(args, k+"="+strconv.FormatUint(v, 10)) 266 | defaultSetup = true 267 | } else if k == "zone" || k == "limit" { 268 | args = append(args, k+"="+strconv.FormatUint(v, 10)) 269 | } 270 | } 271 | 272 | // We need at most 2 arguments and at least 1 273 | if len(args) == 0 || len(args) > 2 { 274 | return "", errWrongArgumentNumber 275 | 276 | } 277 | // if we setup the default global setting we only need a single parameter 278 | // like "default=100000" and nothing else 279 | if defaultSetup && len(args) != 1 { 280 | return "", errWrongDefaultArgument 281 | } 282 | // if we setup a limit for dedicated zone we need 2 params like 283 | // "zone=3" and "limit=50000" 284 | if !defaultSetup && len(args) != 2 { 285 | return "", errWrongZoneArgument 286 | } 287 | 288 | var argsStr string 289 | argsStr, args = args[0], args[1:] 290 | if len(args) > 0 { 291 | for _, s := range args { 292 | argsStr += "," + s 293 | } 294 | } 295 | return argsStr, nil 296 | } 297 | 298 | // getZoneString takes the zones as []uint64 to return a formated 299 | // string usable in different ovs-dpctl commands 300 | // Example a slice: var zones = []uint64{2, 3, 4} 301 | // will output: "zone=2,3,4" 302 | func getZoneString(z []uint64) string { 303 | zonesStr := make([]string, 0) 304 | for _, zone := range z { 305 | zonesStr = append(zonesStr, strconv.FormatUint(zone, 10)) 306 | } 307 | 308 | var sb strings.Builder 309 | var firstZone string 310 | if len(zonesStr) > 0 { 311 | sb.WriteString("zone=") 312 | firstZone, zonesStr = zonesStr[0], zonesStr[1:] 313 | } 314 | sb.WriteString(firstZone) 315 | 316 | for _, zone := range zonesStr { 317 | sb.WriteString(",") 318 | sb.WriteString(zone) 319 | } 320 | 321 | return sb.String() 322 | } 323 | 324 | // DpCLI implements the CLI interface by invoking the Client exec 325 | // method. 326 | type DpCLI struct { 327 | // Wrapped client for ovs-dpctl 328 | c *Client 329 | } 330 | 331 | // Exec executes 'ovs-dpctl' + args passed in argument 332 | func (cli *DpCLI) Exec(args ...string) ([]byte, error) { 333 | if cli.c == nil { 334 | return nil, errUninitializedClient 335 | } 336 | 337 | return cli.c.exec("ovs-dpctl", args...) 338 | } 339 | -------------------------------------------------------------------------------- /ovs/doc.go: -------------------------------------------------------------------------------- 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 ovs is a client library for Open vSwitch which enables programmatic 16 | // control of the virtual switch. 17 | package ovs 18 | -------------------------------------------------------------------------------- /ovs/flowstats.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "errors" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | var ( 24 | // ErrInvalidFlowStats is returned when flow statistics from 'ovs-ofctl 25 | // dump-aggregate' do not match the expected output format. 26 | ErrInvalidFlowStats = errors.New("invalid flow statistics") 27 | ) 28 | 29 | // FlowStats contains a variety of statistics about an Open vSwitch port, 30 | // including its port ID and numbers about packet receive and transmit 31 | // operations. 32 | type FlowStats struct { 33 | PacketCount uint64 34 | ByteCount uint64 35 | } 36 | 37 | // UnmarshalText unmarshals a FlowStats from textual form. 38 | func (f *FlowStats) UnmarshalText(b []byte) error { 39 | // Make a copy per documentation for encoding.TextUnmarshaler. 40 | s := string(b) 41 | 42 | // Constants only needed within this method, to avoid polluting the 43 | // package namespace with generic names 44 | const ( 45 | packetCount = "packet_count" 46 | byteCount = "byte_count" 47 | flowCount = "flow_count" 48 | ) 49 | 50 | // Find the index of packet count to find stats. 51 | idx := strings.Index(s, packetCount) 52 | if idx == -1 { 53 | return ErrInvalidFlowStats 54 | } 55 | 56 | // Assume the last three fields are packets, bytes, and flows, in that order. 57 | ss := strings.Fields(s[idx:]) 58 | fields := []string{ 59 | packetCount, 60 | byteCount, 61 | flowCount, 62 | } 63 | 64 | if len(ss) != len(fields) { 65 | return ErrInvalidFlowStats 66 | } 67 | 68 | var values []uint64 69 | for i := range ss { 70 | // Split key from its integer value. 71 | kv := strings.Split(ss[i], "=") 72 | if len(kv) != 2 { 73 | return ErrInvalidFlowStats 74 | } 75 | 76 | // Verify keys appear in expected order. 77 | if kv[0] != fields[i] { 78 | return ErrInvalidFlowStats 79 | } 80 | 81 | n, err := strconv.ParseUint(kv[1], 10, 64) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | values = append(values, n) 87 | } 88 | 89 | *f = FlowStats{ 90 | PacketCount: values[0], 91 | ByteCount: values[1], 92 | // Flow count unused. 93 | } 94 | 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /ovs/flowstats_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/google/go-cmp/cmp" 21 | ) 22 | 23 | func TestFlowStatsUnmarshalText(t *testing.T) { 24 | var tests = []struct { 25 | desc string 26 | s string 27 | stats *FlowStats 28 | ok bool 29 | }{ 30 | { 31 | desc: "empty string", 32 | }, 33 | { 34 | desc: "too few fields", 35 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 byte_count=141379644", 36 | }, 37 | { 38 | desc: "too many fields", 39 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 byte_count=141379644 flow_count=2, flow_count=3", 40 | }, 41 | { 42 | desc: "packet_count missing", 43 | s: "NXST_AGGREGATE reply (xid=0x4): frame_count=642800 byte_count=141379644 flow_count=2", 44 | }, 45 | { 46 | desc: "byte_count missing", 47 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 bits*8_count=141379644 flow_count=2", 48 | }, 49 | { 50 | desc: "bad key=value", 51 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=1=foo byte_count=141379644 flow_count=2", 52 | }, 53 | { 54 | desc: "bad packet count", 55 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=toosmall byte_count=141379644 flow_count=2", 56 | }, 57 | { 58 | desc: "bad byte count", 59 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 byte_count=toolarge flow_count=2", 60 | }, 61 | { 62 | desc: "bad flow count", 63 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 byte_count=1 FLOW_count=2", 64 | }, 65 | { 66 | desc: "OK", 67 | s: "NXST_AGGREGATE reply (xid=0x4): packet_count=642800 byte_count=141379644 flow_count=2", 68 | stats: &FlowStats{ 69 | PacketCount: 642800, 70 | ByteCount: 141379644, 71 | }, 72 | ok: true, 73 | }, 74 | { 75 | desc: "OK, OpenFlow 1.4", 76 | s: "OFPST_AGGREGATE reply (OF1.4) (xid=0x2): packet_count=1207 byte_count=101673 flow_count=1", 77 | stats: &FlowStats{ 78 | PacketCount: 1207, 79 | ByteCount: 101673, 80 | }, 81 | ok: true, 82 | }, 83 | } 84 | 85 | for _, tt := range tests { 86 | t.Run(tt.desc, func(t *testing.T) { 87 | stats := new(FlowStats) 88 | err := stats.UnmarshalText([]byte(tt.s)) 89 | 90 | if err != nil && tt.ok { 91 | t.Fatalf("unexpected error: %v", err) 92 | } 93 | if err == nil && !tt.ok { 94 | t.Fatal("expected an error, but none occurred") 95 | } 96 | if err != nil { 97 | return 98 | } 99 | 100 | if diff := cmp.Diff(tt.stats, stats); diff != "" { 101 | t.Fatalf("unexpected FlowStats (-want +got):\n%s", diff) 102 | } 103 | }) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ovs/matchflow.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bytes" 19 | "errors" 20 | "fmt" 21 | "strconv" 22 | "strings" 23 | ) 24 | 25 | const ( 26 | // AnyTable is a special table value to match flows in any table. 27 | AnyTable = -1 28 | ) 29 | 30 | var ( 31 | // errEmptyMatchFlow is returned when a MatchFlow has no arguments. 32 | errEmptyMatchFlow = errors.New("match flow is empty") 33 | ) 34 | 35 | // TODO(mdlayher): it would be nice if MatchFlow was just a Flow. 36 | 37 | // A MatchFlow is an OpenFlow flow intended for flow deletion. It can be marshaled to its textual 38 | // form for use with Open vSwitch. 39 | type MatchFlow struct { 40 | Protocol Protocol 41 | InPort int 42 | Matches []Match 43 | Table int 44 | 45 | // Cookie indicates a cookie value to use when matching flows. 46 | Cookie uint64 47 | 48 | // CookieMask is a mask used alongside Cookie to enable matching flows 49 | // which match a mask. If CookieMask is not set, Cookie will be matched 50 | // exactly. 51 | CookieMask uint64 52 | } 53 | 54 | var _ error = &MatchFlowError{} 55 | 56 | // A MatchFlowError is an error encountered while marshaling or unmarshaling 57 | // a MatchFlow. 58 | type MatchFlowError struct { 59 | // Str indicates the string, if any, that caused the flow to 60 | // fail while unmarshaling. 61 | Str string 62 | 63 | // Err indicates the error that halted flow marshaling or unmarshaling. 64 | Err error 65 | } 66 | 67 | // Error returns the string representation of a MatchFlowError. 68 | func (e *MatchFlowError) Error() string { 69 | if e.Str == "" { 70 | return e.Err.Error() 71 | } 72 | 73 | return fmt.Sprintf("flow error due to string %q: %v", 74 | e.Str, e.Err) 75 | } 76 | 77 | // MarshalText marshals a MatchFlow into its textual form. 78 | func (f *MatchFlow) MarshalText() ([]byte, error) { 79 | matches, err := f.marshalMatches() 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | var b []byte 85 | 86 | if f.Protocol != "" { 87 | b = append(b, f.Protocol...) 88 | b = append(b, ',') 89 | } 90 | 91 | if f.InPort != 0 { 92 | b = append(b, inPort+"="...) 93 | 94 | // Special case, InPortLOCAL is converted to the literal string LOCAL 95 | if f.InPort == PortLOCAL { 96 | b = append(b, portLOCAL...) 97 | } else { 98 | b = strconv.AppendInt(b, int64(f.InPort), 10) 99 | } 100 | b = append(b, ',') 101 | } 102 | 103 | if len(matches) > 0 { 104 | b = append(b, strings.Join(matches, ",")...) 105 | b = append(b, ',') 106 | } 107 | 108 | if f.Cookie > 0 || f.CookieMask > 0 { 109 | // Hexadecimal cookies and masks are much easier to read. 110 | b = append(b, cookie+"="...) 111 | b = append(b, paddedHexUint64(f.Cookie)...) 112 | b = append(b, '/') 113 | 114 | if f.CookieMask == 0 { 115 | b = append(b, "-1"...) 116 | } else { 117 | b = append(b, paddedHexUint64(f.CookieMask)...) 118 | } 119 | 120 | b = append(b, ',') 121 | } 122 | 123 | if f.Table != AnyTable { 124 | b = append(b, table+"="...) 125 | b = strconv.AppendInt(b, int64(f.Table), 10) 126 | } 127 | 128 | b = bytes.Trim(b, ",") 129 | 130 | if len(b) == 0 { 131 | return nil, &MatchFlowError{ 132 | Err: errEmptyMatchFlow, 133 | } 134 | } 135 | 136 | return b, nil 137 | } 138 | 139 | // marshalMatches marshals all Matches in a MatchFlow to their text form. 140 | func (f *MatchFlow) marshalMatches() ([]string, error) { 141 | fns := make([]func() ([]byte, error), 0, len(f.Matches)) 142 | for _, fn := range f.Matches { 143 | fns = append(fns, fn.MarshalText) 144 | } 145 | 146 | return f.marshalFunctions(fns) 147 | } 148 | 149 | // marshalFunctions marshals a slice of functions to their text form. 150 | func (f *MatchFlow) marshalFunctions(fns []func() ([]byte, error)) ([]string, error) { 151 | out := make([]string, 0, len(fns)) 152 | for _, fn := range fns { 153 | o, err := fn() 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | out = append(out, string(o)) 159 | } 160 | 161 | return out, nil 162 | } 163 | -------------------------------------------------------------------------------- /ovs/matchflow_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "net" 19 | "reflect" 20 | "testing" 21 | ) 22 | 23 | func TestMatchFlowMarshalText(t *testing.T) { 24 | var tests = []struct { 25 | desc string 26 | f *MatchFlow 27 | s string 28 | err error 29 | }{ 30 | { 31 | desc: "empty", 32 | f: &MatchFlow{Table: AnyTable}, 33 | err: &MatchFlowError{ 34 | Err: errEmptyMatchFlow, 35 | }, 36 | }, 37 | { 38 | desc: "Flow with cookie=10/-1, in any table", 39 | f: &MatchFlow{ 40 | Cookie: 10, 41 | Table: AnyTable, 42 | }, 43 | s: "cookie=0x000000000000000a/-1", 44 | }, 45 | { 46 | desc: "Flow with cookie=0x1/0xf, in any table", 47 | f: &MatchFlow{ 48 | Cookie: 0x1, 49 | CookieMask: 0xf, 50 | Table: AnyTable, 51 | }, 52 | s: "cookie=0x0000000000000001/0x000000000000000f", 53 | }, 54 | { 55 | desc: "Flow with in_port=LOCAL", 56 | f: &MatchFlow{ 57 | InPort: PortLOCAL, 58 | }, 59 | s: "in_port=LOCAL,table=0", 60 | }, 61 | { 62 | desc: "ARP Flow", 63 | f: &MatchFlow{ 64 | Protocol: ProtocolARP, 65 | Matches: []Match{ 66 | ARPTargetHardwareAddress( 67 | net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, 68 | ), 69 | ARPTargetProtocolAddress("169.254.0.0/16"), 70 | }, 71 | Table: 1, 72 | }, 73 | s: "arp,arp_tha=aa:bb:cc:dd:ee:ff,arp_tpa=169.254.0.0/16,table=1", 74 | }, 75 | { 76 | desc: "ICMPv4 Flow", 77 | f: &MatchFlow{ 78 | Protocol: ProtocolICMPv4, 79 | Matches: []Match{ 80 | ICMPType(3), 81 | ICMPCode(1), 82 | DataLinkSource("00:11:22:33:44:55"), 83 | }, 84 | }, 85 | s: "icmp,icmp_type=3,icmp_code=1,dl_src=00:11:22:33:44:55,table=0", 86 | }, 87 | { 88 | desc: "ICMPv6 Flow", 89 | f: &MatchFlow{ 90 | Protocol: ProtocolICMPv6, 91 | InPort: 74, 92 | Matches: []Match{ 93 | ICMP6Type(135), 94 | IPv6Source("fe80:aaaa:bbbb:cccc:dddd::1/124"), 95 | NeighborDiscoverySourceLinkLayer( 96 | net.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, 97 | ), 98 | }, 99 | Table: 0, 100 | }, 101 | s: "icmp6,in_port=74,icmpv6_type=135,ipv6_src=fe80:aaaa:bbbb:cccc:dddd::1/124,nd_sll=00:11:22:33:44:55,table=0", 102 | }, 103 | { 104 | desc: "ICMPv6 Type and Code Flow", 105 | f: &MatchFlow{ 106 | Protocol: ProtocolICMPv6, 107 | InPort: 74, 108 | Matches: []Match{ 109 | ICMP6Type(1), 110 | ICMP6Code(3), 111 | IPv6Source("fe80:aaaa:bbbb:cccc:dddd::1/124"), 112 | }, 113 | Table: 0, 114 | }, 115 | s: "icmp6,in_port=74,icmpv6_type=1,icmpv6_code=3,ipv6_src=fe80:aaaa:bbbb:cccc:dddd::1/124,table=0", 116 | }, 117 | { 118 | desc: "IPv4 Flow", 119 | f: &MatchFlow{ 120 | Protocol: ProtocolIPv4, 121 | InPort: 31, 122 | Matches: []Match{ 123 | DataLinkSource("00:11:22:33:44:55"), 124 | NetworkSource("10.0.0.1"), 125 | }, 126 | Table: 0, 127 | }, 128 | s: "ip,in_port=31,dl_src=00:11:22:33:44:55,nw_src=10.0.0.1,table=0", 129 | }, 130 | { 131 | desc: "IPv6 Flow", 132 | f: &MatchFlow{ 133 | Protocol: ProtocolIPv6, 134 | Matches: []Match{ 135 | DataLinkDestination("01:02:03:04:05:06"), 136 | IPv6Destination("fe80::abcd:1"), 137 | }, 138 | Table: 1, 139 | }, 140 | s: "ipv6,dl_dst=01:02:03:04:05:06,ipv6_dst=fe80::abcd:1,table=1", 141 | }, 142 | { 143 | desc: "TCPv4 Flow", 144 | f: &MatchFlow{ 145 | Protocol: ProtocolTCPv4, 146 | InPort: 72, 147 | Matches: []Match{ 148 | TransportDestinationPort(995), 149 | }, 150 | Table: 0, 151 | }, 152 | s: "tcp,in_port=72,tp_dst=995,table=0", 153 | }, 154 | { 155 | desc: "TCPv6 Flow", 156 | f: &MatchFlow{ 157 | Protocol: ProtocolTCPv6, 158 | InPort: 15, 159 | Matches: []Match{ 160 | TransportDestinationPort(465), 161 | }, 162 | Table: 0, 163 | }, 164 | s: "tcp6,in_port=15,tp_dst=465,table=0", 165 | }, 166 | { 167 | desc: "UDPv4 Flow", 168 | f: &MatchFlow{ 169 | Protocol: ProtocolUDPv4, 170 | InPort: 33, 171 | Matches: []Match{ 172 | TransportDestinationPort(80), 173 | }, 174 | Table: 0, 175 | }, 176 | s: "udp,in_port=33,tp_dst=80,table=0", 177 | }, 178 | { 179 | desc: "UDPv6 Flow", 180 | f: &MatchFlow{ 181 | Protocol: ProtocolUDPv6, 182 | InPort: 49, 183 | Matches: []Match{ 184 | TransportDestinationPort(80), 185 | }, 186 | Table: 0, 187 | }, 188 | s: "udp6,in_port=49,tp_dst=80,table=0", 189 | }, 190 | { 191 | desc: "IPv4 SSH conntrack flow", 192 | f: &MatchFlow{ 193 | Protocol: ProtocolTCPv4, 194 | Matches: []Match{ 195 | ConnectionTrackingState( 196 | SetState(CTStateTracked), 197 | SetState(CTStateNew), 198 | ), 199 | NetworkDestination("192.0.2.1"), 200 | TransportDestinationPort(22), 201 | }, 202 | Table: 45, 203 | }, 204 | s: "tcp,ct_state=+trk+new,nw_dst=192.0.2.1,tp_dst=22,table=45", 205 | }, 206 | { 207 | desc: "TCP Flag Flow", 208 | f: &MatchFlow{ 209 | Protocol: ProtocolTCPv4, 210 | Matches: []Match{ 211 | TCPFlags( 212 | SetTCPFlag(TCPFlagSYN), 213 | SetTCPFlag(TCPFlagACK), 214 | ), 215 | NetworkDestination("192.0.2.1"), 216 | TransportDestinationPort(22), 217 | }, 218 | Table: 45, 219 | }, 220 | s: "tcp,tcp_flags=+syn+ack,nw_dst=192.0.2.1,tp_dst=22,table=45", 221 | }, 222 | { 223 | desc: "TP port range flow", 224 | f: &MatchFlow{ 225 | Protocol: ProtocolUDPv4, 226 | InPort: 33, 227 | Matches: []Match{ 228 | NetworkDestination("192.0.2.1"), 229 | TransportDestinationMaskedPort(0xea60, 0xffe0), 230 | }, 231 | Table: 55, 232 | }, 233 | s: "udp,in_port=33,nw_dst=192.0.2.1,tp_dst=0xea60/0xffe0,table=55", 234 | }, 235 | { 236 | desc: "Test zero cookie match", 237 | f: &MatchFlow{ 238 | Cookie: 0, 239 | CookieMask: 0xffffffffffffffff, 240 | Table: 45, 241 | }, 242 | s: "cookie=0x0000000000000000/0xffffffffffffffff,table=45", 243 | }, 244 | { 245 | desc: "Test match any cookie", 246 | f: &MatchFlow{ 247 | Cookie: 0, 248 | CookieMask: 0, 249 | Table: 45, 250 | }, 251 | s: "table=45", 252 | }, 253 | } 254 | 255 | for _, tt := range tests { 256 | t.Run(tt.desc, func(t *testing.T) { 257 | b, err := tt.f.MarshalText() 258 | if want, got := tt.err, err; !matchFlowErrorEqual(want, got) { 259 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", 260 | want, got) 261 | } 262 | 263 | if want, got := tt.s, string(b); want != got { 264 | t.Fatalf("unexpected Flow text:\n- want: %v\n- got: %v", 265 | want, got) 266 | } 267 | }) 268 | } 269 | } 270 | 271 | // matchFlowErrorEqual determines if two possible MatchFlowErrors are equal. 272 | func matchFlowErrorEqual(a error, b error) bool { 273 | // Special case: both nil is OK 274 | if a == nil && b == nil { 275 | return true 276 | } 277 | 278 | fa, ok := a.(*MatchFlowError) 279 | if !ok { 280 | return false 281 | } 282 | 283 | fb, ok := b.(*MatchFlowError) 284 | if !ok { 285 | return false 286 | } 287 | 288 | // Zero out Str field for comparison 289 | fa.Str = "" 290 | fb.Str = "" 291 | 292 | return reflect.DeepEqual(fa, fb) 293 | } 294 | -------------------------------------------------------------------------------- /ovs/ovs.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | ) 21 | 22 | // A FailMode is a failure mode which Open vSwitch uses when it cannot 23 | // contact a controller. 24 | type FailMode string 25 | 26 | // FailMode constants which can be used in OVS configurations. 27 | const ( 28 | FailModeStandalone FailMode = "standalone" 29 | FailModeSecure FailMode = "secure" 30 | ) 31 | 32 | // An InterfaceType is a network interface type recognized by Open vSwitch. 33 | type InterfaceType string 34 | 35 | // InterfaceType constants which can be used in OVS configurations. 36 | const ( 37 | InterfaceTypeGRE InterfaceType = "gre" 38 | InterfaceTypeInternal InterfaceType = "internal" 39 | InterfaceTypePatch InterfaceType = "patch" 40 | InterfaceTypeSTT InterfaceType = "stt" 41 | InterfaceTypeVXLAN InterfaceType = "vxlan" 42 | ) 43 | 44 | // A PortAction is a port actions to change the port characteristics of the 45 | // specific port through the ModPort API. 46 | type PortAction string 47 | 48 | // PortAction constants for ModPort API. 49 | const ( 50 | PortActionUp PortAction = "up" 51 | PortActionDown PortAction = "down" 52 | PortActionSTP PortAction = "stp" 53 | PortActionNoSTP PortAction = "no-stp" 54 | PortActionReceive PortAction = "receive" 55 | PortActionNoReceive PortAction = "no-receive" 56 | PortActionReceiveSTP PortAction = "receive-stp" 57 | PortActionNoReceiveSTP PortAction = "no-receive-stp" 58 | PortActionForward PortAction = "forward" 59 | PortActionNoForward PortAction = "no-forward" 60 | PortActionFlood PortAction = "flood" 61 | PortActionNoFlood PortAction = "no-flood" 62 | PortActionPacketIn PortAction = "packet-in" 63 | PortActionNoPacketIn PortAction = "no-packet-in" 64 | ) 65 | 66 | // An Error is an error returned when shelling out to an Open vSwitch control 67 | // program. It captures the combined stdout and stderr as well as the exit 68 | // code. 69 | type Error struct { 70 | Out []byte 71 | Err error 72 | } 73 | 74 | // Error returns the string representation of an Error. 75 | func (e *Error) Error() string { 76 | return fmt.Sprintf("%s: %s", e.Err, string(e.Out)) 77 | } 78 | 79 | // IsPortNotExist checks if err is of type Error and is caused by asking OVS for 80 | // information regarding a non-existent port. 81 | func IsPortNotExist(err error) bool { 82 | oerr, ok := err.(*Error) 83 | if !ok { 84 | return false 85 | } 86 | 87 | return bytes.HasPrefix(oerr.Out, []byte("ovs-vsctl: no port named ")) && 88 | oerr.Err.Error() == "exit status 1" 89 | } 90 | -------------------------------------------------------------------------------- /ovs/ovs_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "errors" 19 | "testing" 20 | ) 21 | 22 | func TestIsPortNotExist(t *testing.T) { 23 | var tests = []struct { 24 | desc string 25 | err error 26 | ok bool 27 | }{ 28 | { 29 | desc: "not type Error", 30 | err: errors.New("foo"), 31 | }, 32 | { 33 | desc: "type Error, wrong Error.Out", 34 | err: &Error{ 35 | Out: []byte("bar"), 36 | Err: errors.New("exit status 1"), 37 | }, 38 | }, 39 | { 40 | desc: "type Error, wrong Error.Err", 41 | err: &Error{ 42 | Out: []byte("ovs-vsctl: no port named foo"), 43 | Err: errors.New("exit status foo"), 44 | }, 45 | }, 46 | { 47 | desc: "ok", 48 | err: &Error{ 49 | Out: []byte("ovs-vsctl: no port named foo"), 50 | Err: errors.New("exit status 1"), 51 | }, 52 | ok: true, 53 | }, 54 | } 55 | 56 | for _, tt := range tests { 57 | t.Run(tt.desc, func(t *testing.T) { 58 | if want, got := tt.ok, IsPortNotExist(tt.err); want != got { 59 | t.Fatalf("unexpected IsPortNotExist(%v):\n- want: %v\n- got: %v", 60 | tt.err, want, got) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | // errStr is a helper to return the string form of an error, even if the 67 | // error is nil. 68 | func errStr(err error) string { 69 | if err == nil { 70 | return "" 71 | } 72 | 73 | return err.Error() 74 | } 75 | -------------------------------------------------------------------------------- /ovs/portrange.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "errors" 19 | "math" 20 | ) 21 | 22 | var ( 23 | // ErrInvalidPortRange is returned when there's a port range that invalid. 24 | ErrInvalidPortRange = errors.New("invalid port range") 25 | ) 26 | 27 | // An PortRange represents a range of ports expressed in 16 bit integers. The start and 28 | // end values of this range are inclusive. 29 | type PortRange struct { 30 | Start uint16 31 | End uint16 32 | } 33 | 34 | // A BitRange is a representation of a range of values from base value with a bitmask 35 | // applied. 36 | type BitRange struct { 37 | Value uint16 38 | Mask uint16 39 | } 40 | 41 | // BitwiseMatch returns an array of BitRanges that represent the range of integers 42 | // in the PortRange. 43 | func (r *PortRange) BitwiseMatch() ([]BitRange, error) { 44 | if r.Start <= 0 || r.End <= 0 { 45 | return nil, ErrInvalidPortRange 46 | } 47 | if r.Start > r.End { 48 | return nil, ErrInvalidPortRange 49 | } 50 | 51 | if r.Start == r.End { 52 | return []BitRange{ 53 | {Value: r.Start, Mask: 0xffff}, 54 | }, nil 55 | } 56 | 57 | bitRanges := []BitRange{} 58 | 59 | // Find the largest window we can get on a binary boundary 60 | window := (r.End - r.Start) + 1 61 | bitLength := uint(math.Floor(math.Log2(float64(window)))) 62 | 63 | rangeStart, rangeEnd := getRange(r.End, bitLength) 64 | 65 | // Decrement our mask until we fit inside the range we want from a binary boundary. 66 | for rangeEnd > r.End { 67 | bitLength-- 68 | rangeStart, rangeEnd = getRange(r.End, bitLength) 69 | } 70 | 71 | current := BitRange{ 72 | Value: rangeStart, 73 | Mask: getMask(bitLength), 74 | } 75 | 76 | // The range we picked out was from the middle of our set, so we'll need to recurse on 77 | // the remaining values for anything less than or greater than the current 78 | // range. 79 | 80 | if r.Start != rangeStart { 81 | leftRemainder := PortRange{ 82 | Start: r.Start, 83 | End: rangeStart - 1, 84 | } 85 | 86 | leftRemainingBitRanges, err := leftRemainder.BitwiseMatch() 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | bitRanges = append(bitRanges, leftRemainingBitRanges...) 92 | } 93 | 94 | // We append our current range here, so we're ordered properly. 95 | bitRanges = append(bitRanges, current) 96 | 97 | if r.End != rangeEnd { 98 | rightRemainder := PortRange{ 99 | Start: rangeEnd + 1, 100 | End: r.End, 101 | } 102 | 103 | rightRemainingBitRanges, err := rightRemainder.BitwiseMatch() 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | bitRanges = append(bitRanges, rightRemainingBitRanges...) 109 | } 110 | 111 | return bitRanges, nil 112 | } 113 | 114 | func getMask(bitLength uint) uint16 { 115 | // All 1s for everything that doesn't change in the range 116 | return math.MaxUint16 ^ uint16((1<`) 40 | ctThawRegexp = regexp.MustCompile(`thaw`) 41 | ctResumeFromRegexp = regexp.MustCompile(`Resuming from table`) 42 | ctResumeWithRegexp = regexp.MustCompile(`resume conntrack with`) 43 | tunNative = regexp.MustCompile(`native tunnel`) 44 | ) 45 | 46 | const ( 47 | popvlan = "popvlan" 48 | pushvlan = "pushvlan" 49 | drop = "drop" 50 | localPort = 65534 51 | ) 52 | 53 | // DataPathActions is a text unmarshaler for data path actions in ofproto/trace output 54 | type DataPathActions interface { 55 | encoding.TextUnmarshaler 56 | } 57 | 58 | // NewDataPathActions returns an implementation of DataPathActions 59 | func NewDataPathActions(actions string) DataPathActions { 60 | return &dataPathActions{ 61 | actions: actions, 62 | } 63 | } 64 | 65 | type dataPathActions struct { 66 | actions string 67 | } 68 | 69 | func (d *dataPathActions) UnmarshalText(b []byte) error { 70 | d.actions = string(b) 71 | return nil 72 | } 73 | 74 | // DataPathFlows represents the initial/final flows passed/returned from ofproto/trace 75 | type DataPathFlows struct { 76 | Protocol Protocol 77 | Matches []Match 78 | } 79 | 80 | // UnmarshalText unmarshals the initial/final data path flows from ofproto/trace output 81 | func (df *DataPathFlows) UnmarshalText(b []byte) error { 82 | matches := strings.Split(string(b), ",") 83 | 84 | if len(matches) == 0 { 85 | return errors.New("error unmarshalling text, no comma delimiter found") 86 | } 87 | 88 | for _, match := range matches { 89 | switch Protocol(match) { 90 | case ProtocolARP, ProtocolICMPv4, ProtocolICMPv6, 91 | ProtocolIPv4, ProtocolIPv6, ProtocolTCPv4, 92 | ProtocolTCPv6, ProtocolUDPv4, ProtocolUDPv6: 93 | df.Protocol = Protocol(match) 94 | continue 95 | } 96 | 97 | // We can safely skip these keywords 98 | switch { 99 | case match == "eth": 100 | continue 101 | case match == "unchanged": 102 | continue 103 | case recircIDRegexp.MatchString(match): 104 | continue 105 | } 106 | 107 | kv := strings.Split(match, "=") 108 | if len(kv) != 2 { 109 | return fmt.Errorf("unexpected match format for match %q", match) 110 | } 111 | 112 | switch strings.TrimSpace(kv[0]) { 113 | case inPort: 114 | // Parse in_port=LOCAL into a new match. 115 | if strings.TrimSpace(kv[1]) == portLOCAL { 116 | df.Matches = append(df.Matches, InPortMatch(localPort)) 117 | continue 118 | } 119 | } 120 | 121 | m, err := parseMatch(kv[0], kv[1]) 122 | if err != nil { 123 | return err 124 | } 125 | // The keyword will be skipped if unknown, 126 | // don't add a nil value 127 | if m != nil { 128 | df.Matches = append(df.Matches, m) 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | 135 | // ProtoTrace is a type representing output from ovs-app-ctl ofproto/trace 136 | type ProtoTrace struct { 137 | CommandStr string 138 | InputFlow *DataPathFlows 139 | FinalFlow *DataPathFlows 140 | DataPathActions DataPathActions 141 | FlowActions []string 142 | RawOutput []byte 143 | } 144 | 145 | // UnmarshalText unmarshals ProtoTrace text into a ProtoTrace type. 146 | // Not implemented yet. 147 | func (pt *ProtoTrace) UnmarshalText(b []byte) error { 148 | lines := strings.Split(string(b), "\n") 149 | for _, line := range lines { 150 | if matches, matched := checkMatch(datapathActionsRegexp, line); matched { 151 | 152 | if recircRegexp.MatchString(line) { 153 | pt.FlowActions = append(pt.FlowActions, "recirc") 154 | } 155 | 156 | // first index is always the left most match, following 157 | // are the actual matches 158 | pt.DataPathActions = &dataPathActions{ 159 | actions: matches[1], 160 | } 161 | 162 | continue 163 | } 164 | 165 | if matches, matched := checkMatch(initialFlowRegexp, line); matched { 166 | flow := &DataPathFlows{} 167 | err := flow.UnmarshalText([]byte(matches[1])) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | pt.InputFlow = flow 173 | continue 174 | } 175 | 176 | if matches, matched := checkMatch(finalFlowRegexp, line); matched { 177 | flow := &DataPathFlows{} 178 | err := flow.UnmarshalText([]byte(matches[1])) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | pt.FinalFlow = flow 184 | continue 185 | } 186 | 187 | if _, matched := checkMatch(megaFlowRegexp, line); matched { 188 | continue 189 | } 190 | 191 | if _, matched := checkMatch(traceStartRegexp, line); matched { 192 | continue 193 | } 194 | 195 | if _, matched := checkMatch(traceFlowRegexp, line); matched { 196 | continue 197 | } 198 | 199 | // We can safely skip these keywords 200 | switch { 201 | case ctCommentRegexp.MatchString(line): 202 | continue 203 | case ctThawRegexp.MatchString(line): 204 | continue 205 | case ctResumeFromRegexp.MatchString(line): 206 | continue 207 | case ctResumeWithRegexp.MatchString(line): 208 | continue 209 | case tunNative.MatchString(line): 210 | continue 211 | } 212 | 213 | if matches, matched := checkMatch(traceActionRegexp, line); matched { 214 | pt.FlowActions = append(pt.FlowActions, matches[1]) 215 | continue 216 | } 217 | } 218 | 219 | return nil 220 | } 221 | 222 | func checkMatch(re *regexp.Regexp, s string) ([]string, bool) { 223 | matches := re.FindStringSubmatch(s) 224 | if len(matches) == 0 { 225 | return matches, false 226 | } 227 | 228 | return matches, true 229 | } 230 | -------------------------------------------------------------------------------- /ovs/table.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "errors" 19 | "strconv" 20 | "strings" 21 | ) 22 | 23 | var ( 24 | // ErrInvalidTable is returned when tables from 'ovs-ofctl dump-tables' 25 | // do not match the expected output format. 26 | ErrInvalidTable = errors.New("invalid openflow table") 27 | ) 28 | 29 | // A Table is an Open vSwitch table. 30 | type Table struct { 31 | ID int 32 | Name string 33 | Wild string 34 | Max int 35 | Active int 36 | Lookup uint64 37 | Matched uint64 38 | } 39 | 40 | // UnmarshalText unmarshals a Table from textual form as output by 41 | // 'ovs-ofctl dump-tables': 42 | // 0: classifier: wild=0x3fffff, max=1000000, active=0 43 | // lookup=0, matched=0 44 | func (t *Table) UnmarshalText(b []byte) error { 45 | // Make a copy per documentation for encoding.TextUnmarshaler. 46 | s := string(b) 47 | 48 | ss := strings.Fields(s) 49 | if len(ss) != 7 && len(ss) != 8 { 50 | return ErrInvalidTable 51 | } 52 | 53 | // ID has trailing colon which must be removed 54 | id, err := strconv.ParseInt(strings.TrimSuffix(ss[0], ":"), 10, 0) 55 | if err != nil { 56 | return err 57 | } 58 | t.ID = int(id) 59 | 60 | // Numeric fields start at index 2 normally, but if the table name 61 | // does not contain a trailing colon (OVS tables that aren't "classifier"), 62 | // it will start at index 3 63 | idx := 2 64 | if !strings.HasSuffix(ss[1], ":") { 65 | idx = 3 66 | } 67 | // Name has trailing colon which must be removed 68 | t.Name = strings.TrimSuffix(ss[1], ":") 69 | 70 | out := make([]uint64, 0, 4) 71 | for i, str := range ss[idx:] { 72 | // Strip trailing commas from key/value fields 73 | str = strings.TrimSuffix(str, ",") 74 | 75 | pair := strings.Split(str, "=") 76 | if len(pair) != 2 { 77 | return ErrInvalidTable 78 | } 79 | 80 | if i == 0 { 81 | t.Wild = pair[1] 82 | continue 83 | } 84 | 85 | n, err := strconv.ParseUint(pair[1], 10, 64) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | out = append(out, n) 91 | } 92 | 93 | t.Max = int(out[0]) 94 | t.Active = int(out[1]) 95 | t.Lookup = out[2] 96 | t.Matched = out[3] 97 | 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /ovs/table_test.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "reflect" 19 | "strconv" 20 | "testing" 21 | ) 22 | 23 | func TestTableUnmarshalText(t *testing.T) { 24 | var tests = []struct { 25 | desc string 26 | s string 27 | tb *Table 28 | err error 29 | }{ 30 | { 31 | desc: "empty string", 32 | err: ErrInvalidTable, 33 | }, 34 | { 35 | desc: "too few fields", 36 | s: ` 37 | 0: classifier: wild=0x3fffff, max=1000000, active=0 38 | lookup=0, 39 | `, 40 | err: ErrInvalidTable, 41 | }, 42 | { 43 | desc: "too many fields", 44 | s: ` 45 | 1: table1 : wild=0x3fffff, max=1000000, active=0 46 | lookup=0, matched=0, foo=0 47 | `, 48 | err: ErrInvalidTable, 49 | }, 50 | { 51 | desc: "invalid integer ID", 52 | s: ` 53 | foo: classifier: wild=0x3fffff, max=1000000, active=0 54 | lookup=0, matched=0 55 | `, 56 | err: &strconv.NumError{ 57 | Func: "ParseInt", 58 | Num: "foo", 59 | Err: strconv.ErrSyntax, 60 | }, 61 | }, 62 | { 63 | desc: "broken key=value pair", 64 | s: ` 65 | 0: classifier: wild 0x3fffff, max=1000000, active=0 66 | lookup=0, matched=0 67 | `, 68 | err: ErrInvalidTable, 69 | }, 70 | { 71 | desc: "invalid integer max", 72 | s: ` 73 | 0: classifier: wild=0x3fffff, max=foo, active=0 74 | lookup=0, matched=0 75 | `, 76 | err: &strconv.NumError{ 77 | Func: "ParseUint", 78 | Num: "foo", 79 | Err: strconv.ErrSyntax, 80 | }, 81 | }, 82 | { 83 | desc: "OK classifier table", 84 | s: ` 85 | 0: classifier: wild=0x3fffff, max=1000000, active=1 86 | lookup=2, matched=3 87 | `, 88 | tb: &Table{ 89 | ID: 0, 90 | Name: "classifier", 91 | Wild: "0x3fffff", 92 | Max: 1000000, 93 | Active: 1, 94 | Lookup: 2, 95 | Matched: 3, 96 | }, 97 | }, 98 | { 99 | desc: "OK table", 100 | s: ` 101 | 1: table1 : wild=0x3fffff, max=1000000, active=1 102 | lookup=2, matched=3 103 | `, 104 | tb: &Table{ 105 | ID: 1, 106 | Name: "table1", 107 | Wild: "0x3fffff", 108 | Max: 1000000, 109 | Active: 1, 110 | Lookup: 2, 111 | Matched: 3, 112 | }, 113 | }, 114 | } 115 | 116 | for _, tt := range tests { 117 | t.Run(tt.desc, func(t *testing.T) { 118 | tb := new(Table) 119 | err := tb.UnmarshalText([]byte(tt.s)) 120 | 121 | if want, got := errStr(tt.err), errStr(err); want != got { 122 | t.Fatalf("unexpected error:\n- want: %v\n- got: %v", 123 | want, got) 124 | } 125 | if err != nil { 126 | return 127 | } 128 | 129 | if want, got := tt.tb, tb; !reflect.DeepEqual(want, got) { 130 | t.Fatalf("unexpected Table:\n- want: %#v\n- got: %#v", 131 | want, got) 132 | } 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /ovs/vswitch.go: -------------------------------------------------------------------------------- 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 ovs 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "strings" 21 | ) 22 | 23 | const ( 24 | // DefaultIngressRatePolicing is used to disable the ingress policing, 25 | // which is the default behavior. 26 | DefaultIngressRatePolicing = int64(-1) 27 | 28 | // DefaultIngressBurstPolicing is to change the ingress policing 29 | // burst to the default size, 1000 kb. 30 | DefaultIngressBurstPolicing = int64(-1) 31 | ) 32 | 33 | // A VSwitchService is used in a Client to execute 'ovs-vsctl' commands. 34 | type VSwitchService struct { 35 | // Get wraps functionality of the 'ovs-vsctl get' subcommand. 36 | Get *VSwitchGetService 37 | 38 | // Set wraps functionality of the 'ovs-vsctl set' subcommand. 39 | Set *VSwitchSetService 40 | 41 | // Wrapped Client for ExecFunc and debugging. 42 | c *Client 43 | } 44 | 45 | // AddBridge attaches a bridge to Open vSwitch. The bridge may or may 46 | // not already exist. 47 | func (v *VSwitchService) AddBridge(bridge string) error { 48 | _, err := v.exec("--may-exist", "add-br", bridge) 49 | return err 50 | } 51 | 52 | // AddPort attaches a port to a bridge on Open vSwitch. The port may or may 53 | // not already exist. 54 | func (v *VSwitchService) AddPort(bridge string, port string) error { 55 | _, err := v.exec("--may-exist", "add-port", bridge, string(port)) 56 | return err 57 | } 58 | 59 | // DeleteBridge detaches a bridge from Open vSwitch. The bridge may or may 60 | // not already exist. 61 | func (v *VSwitchService) DeleteBridge(bridge string) error { 62 | _, err := v.exec("--if-exists", "del-br", bridge) 63 | return err 64 | } 65 | 66 | // DeletePort detaches a port from a bridge on Open vSwitch. The port may or may 67 | // not already exist. 68 | func (v *VSwitchService) DeletePort(bridge string, port string) error { 69 | _, err := v.exec("--if-exists", "del-port", bridge, string(port)) 70 | return err 71 | } 72 | 73 | // ListPorts lists the ports in Open vSwitch. 74 | func (v *VSwitchService) ListPorts(bridge string) ([]string, error) { 75 | output, err := v.exec("list-ports", bridge) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | // Do no ports exist? 81 | if len(output) == 0 { 82 | return nil, nil 83 | } 84 | 85 | ports := strings.Split(strings.TrimSpace(string(output)), "\n") 86 | return ports, nil 87 | } 88 | 89 | // ListBridges lists the bridges in Open vSwitch. 90 | func (v *VSwitchService) ListBridges() ([]string, error) { 91 | output, err := v.exec("list-br") 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | // Do no bridges exist? 97 | if len(output) == 0 { 98 | return nil, nil 99 | } 100 | 101 | bridges := strings.Split(strings.TrimSpace(string(output)), "\n") 102 | return bridges, nil 103 | } 104 | 105 | // PortToBridge attempts to determine which bridge a port is attached to. 106 | // If port does not exist, an error will be returned, which can be checked 107 | // using IsPortNotExist. 108 | func (v *VSwitchService) PortToBridge(port string) (string, error) { 109 | out, err := v.exec("port-to-br", string(port)) 110 | if err != nil { 111 | return "", err 112 | } 113 | 114 | return string(out), nil 115 | } 116 | 117 | // GetFailMode gets the FailMode for the specified bridge. 118 | func (v *VSwitchService) GetFailMode(bridge string) (FailMode, error) { 119 | out, err := v.exec("get-fail-mode", bridge) 120 | if err != nil { 121 | return "", err 122 | } 123 | 124 | return FailMode(out), nil 125 | } 126 | 127 | // SetFailMode sets the specified FailMode for the specified bridge. 128 | func (v *VSwitchService) SetFailMode(bridge string, mode FailMode) error { 129 | _, err := v.exec("set-fail-mode", bridge, string(mode)) 130 | return err 131 | } 132 | 133 | // SetController sets the controller for this bridge so that ovs-ofctl 134 | // can use this address to communicate. 135 | func (v *VSwitchService) SetController(bridge string, address string) error { 136 | _, err := v.exec("set-controller", bridge, address) 137 | return err 138 | } 139 | 140 | // GetController gets the controller address for this bridge. 141 | func (v *VSwitchService) GetController(bridge string) (string, error) { 142 | address, err := v.exec("get-controller", bridge) 143 | if err != nil { 144 | return "", err 145 | } 146 | 147 | return strings.TrimSpace(string(address)), nil 148 | } 149 | 150 | // exec executes an ExecFunc using 'ovs-vsctl'. 151 | func (v *VSwitchService) exec(args ...string) ([]byte, error) { 152 | return v.c.exec("ovs-vsctl", args...) 153 | } 154 | 155 | // A VSwitchGetService is used in a VSwitchService to execute 'ovs-vsctl get' 156 | // subcommands. 157 | type VSwitchGetService struct { 158 | // v provides the required exec method. 159 | v *VSwitchService 160 | } 161 | 162 | // Bridge gets configuration for a bridge and returns the values through 163 | // a BridgeOptions struct. 164 | func (v *VSwitchGetService) Bridge(bridge string) (BridgeOptions, error) { 165 | // We only support the protocol option at this point. 166 | args := []string{"--format=json", "get", "bridge", bridge, "protocols"} 167 | out, err := v.v.exec(args...) 168 | if err != nil { 169 | return BridgeOptions{}, err 170 | } 171 | 172 | var protocols []string 173 | if err := json.Unmarshal(out, &protocols); err != nil { 174 | return BridgeOptions{}, err 175 | } 176 | 177 | return BridgeOptions{ 178 | Protocols: protocols, 179 | }, nil 180 | } 181 | 182 | // A VSwitchSetService is used in a VSwitchService to execute 'ovs-vsctl set' 183 | // subcommands. 184 | type VSwitchSetService struct { 185 | // v provides the required exec method. 186 | v *VSwitchService 187 | } 188 | 189 | // Bridge sets configuration for a bridge using the values from a BridgeOptions 190 | // struct. 191 | func (v *VSwitchSetService) Bridge(bridge string, options BridgeOptions) error { 192 | // Prepend command line arguments before expanding options slice 193 | // and appending it 194 | args := []string{"set", "bridge", bridge} 195 | args = append(args, options.slice()...) 196 | 197 | _, err := v.v.exec(args...) 198 | return err 199 | } 200 | 201 | // An BridgeOptions enables configuration of a bridge. 202 | type BridgeOptions struct { 203 | // Protocols specifies the OpenFlow protocols the bridge should use. 204 | Protocols []string 205 | } 206 | 207 | // slice creates a string slice containing any non-zero option values from the 208 | // struct in the format expected by Open vSwitch. 209 | func (o BridgeOptions) slice() []string { 210 | var s []string 211 | 212 | if len(o.Protocols) > 0 { 213 | s = append(s, fmt.Sprintf("protocols=%s", strings.Join(o.Protocols, ","))) 214 | } 215 | 216 | return s 217 | } 218 | 219 | // Interface sets configuration for an interface using the values from an 220 | // InterfaceOptions struct. 221 | func (v *VSwitchSetService) Interface(ifi string, options InterfaceOptions) error { 222 | // Prepend command line arguments before expanding options slice 223 | // and appending it 224 | args := []string{"set", "interface", ifi} 225 | args = append(args, options.slice()...) 226 | 227 | _, err := v.v.exec(args...) 228 | return err 229 | } 230 | 231 | // An InterfaceOptions struct enables configuration of an Interface. 232 | type InterfaceOptions struct { 233 | // Type specifies the Open vSwitch interface type. 234 | Type InterfaceType 235 | 236 | // Peer specifies an interface to peer with when creating a patch interface. 237 | Peer string 238 | 239 | // MTURequest specifies the maximum transmission unit associated with an 240 | // interface. 241 | MTURequest int 242 | 243 | // Ingress Policing 244 | // 245 | // These settings control ingress policing for packets received on this 246 | // interface. On a physical interface, this limits the rate at which 247 | // traffic is allowed into the system from the outside; on a virtual 248 | // interface (one connected to a virtual machine), this limits the rate 249 | // at which the VM is able to transmit. 250 | 251 | // IngressRatePolicing specifies the maximum rate for data received on 252 | // this interface in kbps. Data received faster than this rate is dropped. 253 | // Set to 0 (the default) to disable policing. 254 | IngressRatePolicing int64 255 | 256 | // IngressBurstPolicing specifies the maximum burst size for data received on 257 | // this interface in kb. The default burst size if set to 0 is 1000 kb. 258 | // This value has no effect if IngressRatePolicing is set to 0. Specifying 259 | // a larger burst size lets the algorithm be more forgiving, which is important 260 | // for protocols like TCP that react severely to dropped packets. The burst 261 | // size should be at least the size of the interface's MTU. Specifying a 262 | // value that is numerically at least as large as 10% of IngressRatePolicing 263 | // helps TCP come closer to achieving the full rate. 264 | IngressBurstPolicing int64 265 | 266 | // RemoteIP can be populated when the interface is a tunnel interface type 267 | // for example "stt" or "vxlan". It specifies the remote IP address with which to 268 | // form tunnels when traffic is sent to this port. Optionally it could be set to 269 | // "flow" which expects the flow to set tunnel destination. 270 | RemoteIP string 271 | 272 | // Key can be populated when the interface is a tunnel interface type 273 | // for example "stt" or "vxlan". It specifies the tunnel ID to attach to 274 | // tunneled traffic leaving this interface. Optionally it could be set to 275 | // "flow" which expects the flow to set tunnel ID. 276 | Key string 277 | } 278 | 279 | // slice creates a string slice containing any non-zero option values from the 280 | // struct in the format expected by Open vSwitch. 281 | func (i InterfaceOptions) slice() []string { 282 | var s []string 283 | 284 | if i.Type != "" { 285 | s = append(s, fmt.Sprintf("type=%s", i.Type)) 286 | } 287 | 288 | if i.Peer != "" { 289 | s = append(s, fmt.Sprintf("options:peer=%s", i.Peer)) 290 | } 291 | 292 | if i.MTURequest > 0 { 293 | s = append(s, fmt.Sprintf("mtu_request=%d", i.MTURequest)) 294 | } 295 | 296 | if i.IngressRatePolicing == DefaultIngressRatePolicing { 297 | // Set to 0 (the default) to disable policing. 298 | s = append(s, "ingress_policing_rate=0") 299 | } else if i.IngressRatePolicing > 0 { 300 | s = append(s, fmt.Sprintf("ingress_policing_rate=%d", i.IngressRatePolicing)) 301 | } 302 | 303 | if i.IngressBurstPolicing == DefaultIngressBurstPolicing { 304 | // Set to 0 (the default) to the default burst size. 305 | s = append(s, "ingress_policing_burst=0") 306 | } else if i.IngressBurstPolicing > 0 { 307 | s = append(s, fmt.Sprintf("ingress_policing_burst=%d", i.IngressBurstPolicing)) 308 | } 309 | 310 | if i.RemoteIP != "" { 311 | s = append(s, fmt.Sprintf("options:remote_ip=%s", i.RemoteIP)) 312 | } 313 | 314 | if i.Key != "" { 315 | s = append(s, fmt.Sprintf("options:key=%s", i.Key)) 316 | } 317 | 318 | return s 319 | } 320 | -------------------------------------------------------------------------------- /ovsdb/README.md: -------------------------------------------------------------------------------- 1 | ovsdb 2 | ===== 3 | 4 | Package `ovsdb` implements an OVSDB client, as described in [RFC 7047](https://tools.ietf.org/html/rfc7047). 5 | 6 | Package `ovsdb` allows you to communicate with an instance of `ovsdb-server` using 7 | the OVSDB protocol. 8 | 9 | ```go 10 | // Dial an OVSDB connection and create a *ovsdb.Client. 11 | c, err := ovsdb.Dial("unix", "/var/run/openvswitch/db.sock") 12 | if err != nil { 13 | log.Fatalf("failed to dial: %v", err) 14 | } 15 | // Be sure to close the connection! 16 | defer c.Close() 17 | 18 | // Ask ovsdb-server for all of its databases, but only allow the RPC 19 | // a limited amount of time to complete before timing out. 20 | ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 21 | defer cancel() 22 | 23 | dbs, err := c.ListDatabases(ctx) 24 | if err != nil { 25 | log.Fatalf("failed to list databases: %v", err) 26 | } 27 | 28 | for _, d := range dbs { 29 | log.Println(d) 30 | } 31 | ``` -------------------------------------------------------------------------------- /ovsdb/client_integration_test.go: -------------------------------------------------------------------------------- 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 | "context" 19 | "fmt" 20 | "sync" 21 | "testing" 22 | "time" 23 | 24 | "github.com/digitalocean/go-openvswitch/ovsdb" 25 | "github.com/google/go-cmp/cmp" 26 | ) 27 | 28 | func TestClientIntegration(t *testing.T) { 29 | c := dialOVSDB(t) 30 | defer c.Close() 31 | 32 | // Cancel RPCs if they take too long. 33 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 34 | defer cancel() 35 | 36 | t.Run("echo", func(t *testing.T) { 37 | testClientEcho(ctx, t, c) 38 | }) 39 | 40 | t.Run("databases", func(t *testing.T) { 41 | testClientDatabases(ctx, t, c) 42 | }) 43 | } 44 | 45 | func TestClientIntegrationConcurrent(t *testing.T) { 46 | c := dialOVSDB(t) 47 | defer c.Close() 48 | 49 | const n = 512 50 | 51 | // Wait for all goroutines to start before performing RPCs, 52 | // wait for them all to exit before ending the test. 53 | var startWG, doneWG sync.WaitGroup 54 | startWG.Add(n) 55 | doneWG.Add(n) 56 | 57 | // Block all goroutines until they're done spinning up. 58 | sigC := make(chan struct{}) 59 | 60 | for i := 0; i < n; i++ { 61 | go func(c *ovsdb.Client) { 62 | // Block goroutines until all are spun up. 63 | startWG.Done() 64 | <-sigC 65 | 66 | for j := 0; j < 4; j++ { 67 | _, err := c.ListDatabases(context.Background()) 68 | if err != nil { 69 | panic(fmt.Sprintf("failed to query concurrently: %v", err)) 70 | } 71 | } 72 | 73 | doneWG.Done() 74 | }(c) 75 | } 76 | 77 | // Unblock all goroutines once they're all spun up, and wait 78 | // for them all to finish reading. 79 | startWG.Wait() 80 | close(sigC) 81 | doneWG.Wait() 82 | } 83 | 84 | func testClientDatabases(ctx context.Context, t *testing.T, c *ovsdb.Client) { 85 | dbs, err := c.ListDatabases(ctx) 86 | if err != nil { 87 | t.Fatalf("failed to list databases: %v", err) 88 | } 89 | 90 | want := []string{"Open_vSwitch", "_Server"} 91 | 92 | if diff := cmp.Diff(want, dbs); diff != "" { 93 | t.Fatalf("unexpected databases (-want +got):\n%s", diff) 94 | } 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 | } 110 | } 111 | 112 | func testClientEcho(ctx context.Context, t *testing.T, c *ovsdb.Client) { 113 | if err := c.Echo(ctx); err != nil { 114 | t.Fatalf("failed to echo: %v", err) 115 | } 116 | } 117 | 118 | func dialOVSDB(t *testing.T) *ovsdb.Client { 119 | t.Helper() 120 | 121 | // Assume the standard Linux location for the socket. 122 | const sock = "/var/run/openvswitch/db.sock" 123 | c, err := ovsdb.Dial("unix", sock) 124 | if err != nil { 125 | t.Skipf("could not access %q: %v", sock, err) 126 | } 127 | 128 | return c 129 | } 130 | -------------------------------------------------------------------------------- /ovsdb/client_test.go: -------------------------------------------------------------------------------- 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 | "context" 19 | "encoding/json" 20 | "fmt" 21 | "log" 22 | "os" 23 | "strconv" 24 | "sync/atomic" 25 | "testing" 26 | "time" 27 | 28 | "github.com/digitalocean/go-openvswitch/ovsdb" 29 | "github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc" 30 | "github.com/google/go-cmp/cmp" 31 | ) 32 | 33 | func TestClientJSONRPCError(t *testing.T) { 34 | const str = "some error" 35 | 36 | c, _, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 37 | return jsonrpc.Response{ 38 | ID: strPtr("1"), 39 | Error: str, 40 | } 41 | }) 42 | defer done() 43 | 44 | _, err := c.ListDatabases(context.Background()) 45 | if err == nil { 46 | t.Fatal("expected an error, but none occurred") 47 | } 48 | } 49 | 50 | func TestClientOVSDBError(t *testing.T) { 51 | const str = "some error" 52 | 53 | c, _, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 54 | return jsonrpc.Response{ 55 | ID: strPtr("1"), 56 | Result: mustMarshalJSON(t, &ovsdb.Error{ 57 | Err: str, 58 | Details: "malformed", 59 | Syntax: "{}", 60 | }), 61 | } 62 | }) 63 | defer done() 64 | 65 | _, err := c.ListDatabases(context.Background()) 66 | if err == nil { 67 | t.Fatal("expected an error, but none occurred") 68 | } 69 | 70 | oerr, ok := err.(*ovsdb.Error) 71 | if !ok { 72 | t.Fatalf("error of wrong type: %#v", err) 73 | } 74 | 75 | if diff := cmp.Diff(str, oerr.Err); diff != "" { 76 | t.Fatalf("unexpected error (-want +got):\n%s", diff) 77 | } 78 | } 79 | 80 | func TestClientBadCallback(t *testing.T) { 81 | c, notifC, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 82 | return jsonrpc.Response{ 83 | ID: strPtr("1"), 84 | Result: mustMarshalJSON(t, []string{"foo"}), 85 | } 86 | }) 87 | defer done() 88 | 89 | // Client doesn't have a callback for this ID. 90 | notifC <- &jsonrpc.Response{ 91 | Method: "crash", 92 | ID: strPtr("foo"), 93 | } 94 | 95 | if _, err := c.ListDatabases(context.Background()); err != nil { 96 | t.Fatalf("unexpected error: %v", err) 97 | } 98 | } 99 | 100 | func TestClientContextCancelBeforeRPC(t *testing.T) { 101 | // Context canceled before RPC even begins. 102 | ctx, cancel := context.WithCancel(context.Background()) 103 | cancel() 104 | 105 | c, _, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 106 | return jsonrpc.Response{ 107 | ID: strPtr("1"), 108 | Result: mustMarshalJSON(t, []string{"foo"}), 109 | } 110 | }) 111 | defer done() 112 | 113 | _, err := c.ListDatabases(ctx) 114 | if err != context.Canceled { 115 | t.Fatalf("expected context canceled error: %v", err) 116 | } 117 | } 118 | 119 | func TestClientContextCancelDuringRPC(t *testing.T) { 120 | if testing.Short() { 121 | t.Skip("skipping during short test run") 122 | } 123 | 124 | // Context canceled during long RPC. 125 | ctx, cancel := context.WithCancel(context.Background()) 126 | defer cancel() 127 | 128 | c, _, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 129 | // RPC canceled; RPC server still processing. 130 | // TODO(mdlayher): try to do something smarter than sleeping in a test. 131 | cancel() 132 | <-ctx.Done() 133 | 134 | time.Sleep(500 * time.Millisecond) 135 | 136 | return jsonrpc.Response{ 137 | ID: strPtr("1"), 138 | Result: mustMarshalJSON(t, []string{"foo"}), 139 | } 140 | }) 141 | defer done() 142 | 143 | _, err := c.ListDatabases(ctx) 144 | if err != context.Canceled { 145 | t.Fatalf("expected context canceled error: %v", err) 146 | } 147 | } 148 | 149 | func TestClientLeakCallbacks(t *testing.T) { 150 | if testing.Short() { 151 | t.Skip("skipping during short test run") 152 | } 153 | 154 | c, _, done := testClient(t, func(_ jsonrpc.Request) jsonrpc.Response { 155 | // Only respond with messages that don't match an incoming request. 156 | return jsonrpc.Response{ 157 | ID: strPtr("foo"), 158 | Result: mustMarshalJSON(t, []string{"foo"}), 159 | } 160 | }) 161 | defer done() 162 | 163 | // Expect no callbacks registered before RPCs, and none after RPCs time out. 164 | var want ovsdb.ClientStats 165 | want.Callbacks.Current = 0 166 | 167 | if diff := cmp.Diff(want, c.Stats()); diff != "" { 168 | t.Fatalf("unexpected starting client stats (-want +got):\n%s", diff) 169 | } 170 | 171 | for i := 0; i < 5; i++ { 172 | // Give enough time for an RPC to be sent so we don't immediately time out. 173 | ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) 174 | defer cancel() 175 | 176 | _, err := c.ListDatabases(ctx) 177 | if err != context.DeadlineExceeded { 178 | t.Fatalf("expected context deadline exceeded error: %v", err) 179 | } 180 | } 181 | 182 | if diff := cmp.Diff(want, c.Stats()); diff != "" { 183 | t.Fatalf("unexpected ending client stats (-want +got):\n%s", diff) 184 | } 185 | } 186 | 187 | func TestClientEchoLoop(t *testing.T) { 188 | if testing.Short() { 189 | t.Skip("skipping during short test run") 190 | } 191 | 192 | // Count the number of requests sent to the server. 193 | echo := ovsdb.EchoInterval(50 * time.Millisecond) 194 | var reqID int64 195 | 196 | c, _, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response { 197 | if diff := cmp.Diff("echo", req.Method); diff != "" { 198 | panicf("unexpected RPC method (-want +got):\n%s", diff) 199 | } 200 | 201 | // Keep incrementing the request ID to match the client. 202 | id := strconv.Itoa(int(atomic.AddInt64(&reqID, 1))) 203 | return jsonrpc.Response{ 204 | ID: &id, 205 | Result: mustMarshalJSON(t, req.Params), 206 | } 207 | }, echo) 208 | defer done() 209 | 210 | // Fail the test if the RPCs don't fire. 211 | timer := time.AfterFunc(2*time.Second, func() { 212 | panicf("took too long to wait for echo RPCs") 213 | }) 214 | defer timer.Stop() 215 | 216 | // Ensure that background echo RPCs are being sent. 217 | tick := time.NewTicker(100 * time.Millisecond) 218 | defer tick.Stop() 219 | 220 | for { 221 | // Just wait for a handful of RPCs to be sent before success. 222 | <-tick.C 223 | 224 | stats := c.Stats() 225 | 226 | if n := stats.EchoLoop.Failure; n > 0 { 227 | t.Fatalf("echo loop RPC failed %d times", n) 228 | } 229 | 230 | if n := stats.EchoLoop.Success; n > 5 { 231 | break 232 | } 233 | } 234 | } 235 | 236 | func TestClientEchoNotification(t *testing.T) { 237 | if testing.Short() { 238 | t.Skip("skipping during short test run") 239 | } 240 | 241 | c, notifC, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response { 242 | if diff := cmp.Diff("echo", req.Method); diff != "" { 243 | panicf("unexpected RPC method (-want +got):\n%s", diff) 244 | } 245 | 246 | return jsonrpc.Response{ 247 | ID: &req.ID, 248 | Result: mustMarshalJSON(t, req.Params), 249 | } 250 | }) 251 | defer done() 252 | 253 | // Prompt the client to send an echo in the same way ovsdb-server does. 254 | notifC <- &jsonrpc.Response{ 255 | ID: strPtr("echo"), 256 | Method: "echo", 257 | } 258 | 259 | // Fail the test if the RPCs don't fire. 260 | timer := time.AfterFunc(2*time.Second, func() { 261 | panicf("took too long to wait for echo RPCs") 262 | }) 263 | defer timer.Stop() 264 | 265 | // Ensure that background echo RPCs are being sent. 266 | tick := time.NewTicker(100 * time.Millisecond) 267 | defer tick.Stop() 268 | 269 | for { 270 | // Just wait for a single echo RPC cycle before success. 271 | <-tick.C 272 | 273 | stats := c.Stats() 274 | 275 | if n := stats.EchoLoop.Failure; n > 0 { 276 | t.Fatalf("echo loop RPC failed %d times", n) 277 | } 278 | 279 | if n := stats.EchoLoop.Success; n > 0 { 280 | break 281 | } 282 | } 283 | } 284 | 285 | func testClient(t *testing.T, fn jsonrpc.TestFunc, options ...ovsdb.OptionFunc) (*ovsdb.Client, chan<- *jsonrpc.Response, func()) { 286 | t.Helper() 287 | 288 | // Prepend a verbose logger so the caller can override it easily. 289 | if testing.Verbose() { 290 | options = append([]ovsdb.OptionFunc{ 291 | ovsdb.Debug(log.New(os.Stderr, "", 0)), 292 | }, options...) 293 | } 294 | 295 | conn, notifC, done := jsonrpc.TestNetConn(t, fn) 296 | 297 | c, err := ovsdb.New(conn, options...) 298 | if err != nil { 299 | t.Fatalf("failed to dial: %v", err) 300 | } 301 | 302 | return c, notifC, func() { 303 | _ = c.Close() 304 | done() 305 | 306 | // Make sure that the Client cleaned up appropriately. 307 | stats := c.Stats() 308 | 309 | if diff := cmp.Diff(0, stats.Callbacks.Current); diff != "" { 310 | t.Fatalf("unexpected final number of callbacks (-want +got):\n%s", diff) 311 | } 312 | } 313 | } 314 | 315 | func mustMarshalJSON(t *testing.T, v interface{}) []byte { 316 | t.Helper() 317 | 318 | b, err := json.Marshal(v) 319 | if err != nil { 320 | t.Fatalf("failed to marshal JSON: %v", err) 321 | } 322 | 323 | return b 324 | } 325 | 326 | func strPtr(s string) *string { 327 | return &s 328 | } 329 | 330 | func panicf(format string, a ...interface{}) { 331 | panic(fmt.Sprintf(format, a...)) 332 | } 333 | -------------------------------------------------------------------------------- /ovsdb/doc.go: -------------------------------------------------------------------------------- 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 17 | -------------------------------------------------------------------------------- /ovsdb/example_test.go: -------------------------------------------------------------------------------- 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 | "context" 19 | "fmt" 20 | "log" 21 | "time" 22 | 23 | "github.com/digitalocean/go-openvswitch/ovsdb" 24 | ) 25 | 26 | // This example demonstrates basic usage of a Client. The Client connects to 27 | // ovsdb-server and requests a list of all databases known to the server. 28 | func ExampleClient_listDatabases() { 29 | // Dial an OVSDB connection and create a *ovsdb.Client. 30 | c, err := ovsdb.Dial("unix", "/var/run/openvswitch/db.sock") 31 | if err != nil { 32 | log.Fatalf("failed to dial: %v", err) 33 | } 34 | // Be sure to close the connection! 35 | defer c.Close() 36 | 37 | // Ask ovsdb-server for all of its databases, but only allow the RPC 38 | // a limited amount of time to complete before timing out. 39 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 40 | defer cancel() 41 | 42 | dbs, err := c.ListDatabases(ctx) 43 | if err != nil { 44 | log.Fatalf("failed to list databases: %v", err) 45 | } 46 | 47 | for _, d := range dbs { 48 | fmt.Println(d) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ovsdb/internal/jsonrpc/conn.go: -------------------------------------------------------------------------------- 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 jsonrpc 16 | 17 | import ( 18 | "encoding/json" 19 | "errors" 20 | "fmt" 21 | "io" 22 | "log" 23 | "sync" 24 | ) 25 | 26 | // A Request is a JSON-RPC request. 27 | type Request struct { 28 | ID string `json:"id"` 29 | Method string `json:"method"` 30 | Params interface{} `json:"params"` 31 | } 32 | 33 | // A Response is either a JSON-RPC response, or a JSON-RPC request notification. 34 | type Response struct { 35 | // Non-null for response; null for request notification. 36 | ID *string `json:"id"` 37 | 38 | // Response fields. 39 | Result json.RawMessage `json:"result,omitempty"` 40 | Error interface{} `json:"error"` 41 | 42 | // Request notification fields. 43 | Method string `json:"method,omitempty"` 44 | Params json.RawMessage `json:"params,omitempty"` 45 | } 46 | 47 | // Err returns an error, if one occurred in a Response. 48 | func (r *Response) Err() error { 49 | // TODO(mdlayher): better errors. 50 | if r.Error == nil { 51 | return nil 52 | } 53 | 54 | return fmt.Errorf("received JSON-RPC error: %#v", r.Error) 55 | } 56 | 57 | // NewConn creates a new Conn with the input io.ReadWriteCloser. 58 | // If a logger is specified, it is used for debug logs. 59 | func NewConn(rwc io.ReadWriteCloser, ll *log.Logger) *Conn { 60 | if ll != nil { 61 | rwc = &debugReadWriteCloser{ 62 | rwc: rwc, 63 | ll: ll, 64 | } 65 | } 66 | 67 | return &Conn{ 68 | c: rwc, 69 | enc: json.NewEncoder(rwc), 70 | dec: json.NewDecoder(rwc), 71 | } 72 | } 73 | 74 | // A Conn is a JSON-RPC connection. 75 | type Conn struct { 76 | c io.Closer 77 | 78 | encMu sync.Mutex 79 | enc *json.Encoder 80 | 81 | decMu sync.Mutex 82 | dec *json.Decoder 83 | } 84 | 85 | // Close closes the connection. 86 | func (c *Conn) Close() error { 87 | // TODO(mdlayher): acquiring mutex will block forever if receive loop 88 | // is happening elsewhere. Any way to avoid this? 89 | return c.c.Close() 90 | } 91 | 92 | // Send sends a single JSON-RPC request. 93 | func (c *Conn) Send(req Request) error { 94 | if req.ID == "" { 95 | return errors.New("JSON-RPC request ID must not be empty") 96 | } 97 | 98 | // Non-nil array required for ovsdb-server to reply. 99 | if req.Params == nil { 100 | req.Params = []interface{}{} 101 | } 102 | 103 | c.encMu.Lock() 104 | defer c.encMu.Unlock() 105 | 106 | if err := c.enc.Encode(req); err != nil { 107 | return fmt.Errorf("failed to encode JSON-RPC request: %v", err) 108 | } 109 | 110 | return nil 111 | } 112 | 113 | // Receive receives a single JSON-RPC response. 114 | func (c *Conn) Receive() (*Response, error) { 115 | c.decMu.Lock() 116 | defer c.decMu.Unlock() 117 | 118 | var res Response 119 | if err := c.dec.Decode(&res); err != nil { 120 | // Don't mask EOF errors with added detail. 121 | if err == io.EOF { 122 | return nil, err 123 | } 124 | 125 | return nil, fmt.Errorf("failed to decode JSON-RPC response: %v", err) 126 | } 127 | 128 | return &res, nil 129 | } 130 | 131 | type debugReadWriteCloser struct { 132 | rwc io.ReadWriteCloser 133 | ll *log.Logger 134 | } 135 | 136 | func (rwc *debugReadWriteCloser) Read(b []byte) (int, error) { 137 | n, err := rwc.rwc.Read(b) 138 | if err != nil { 139 | return n, err 140 | } 141 | 142 | rwc.ll.Printf(" read: %s", string(b[:n])) 143 | return n, nil 144 | } 145 | 146 | func (rwc *debugReadWriteCloser) Write(b []byte) (int, error) { 147 | n, err := rwc.rwc.Write(b) 148 | if err != nil { 149 | return n, err 150 | } 151 | 152 | rwc.ll.Printf("write: %s", string(b[:n])) 153 | return n, nil 154 | } 155 | 156 | func (rwc *debugReadWriteCloser) Close() error { 157 | err := rwc.rwc.Close() 158 | rwc.ll.Println("close:", err) 159 | return err 160 | } 161 | -------------------------------------------------------------------------------- /ovsdb/internal/jsonrpc/conn_test.go: -------------------------------------------------------------------------------- 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 jsonrpc_test 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "testing" 22 | 23 | "github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc" 24 | "github.com/google/go-cmp/cmp" 25 | ) 26 | 27 | func TestConnSendNoRequestID(t *testing.T) { 28 | c, _, done := jsonrpc.TestConn(t, nil) 29 | defer done() 30 | 31 | if err := c.Send(jsonrpc.Request{}); err == nil { 32 | t.Fatal("expected an error, but none occurred") 33 | } 34 | } 35 | 36 | func TestConnReceiveEOF(t *testing.T) { 37 | c := jsonrpc.NewConn(&eofReadWriteCloser{}, nil) 38 | 39 | // Conn should not mask io.EOF. 40 | _, err := c.Receive() 41 | if err != io.EOF { 42 | t.Fatalf("unexpected error: %v", err) 43 | } 44 | } 45 | 46 | func TestConnSendReceiveError(t *testing.T) { 47 | // TODO(mdlayher): what does this actually look like? 48 | type rpcError struct { 49 | Details string 50 | } 51 | 52 | c, _, done := jsonrpc.TestConn(t, func(_ jsonrpc.Request) jsonrpc.Response { 53 | return jsonrpc.Response{ 54 | ID: strPtr("10"), 55 | Error: rpcError{ 56 | Details: "some error", 57 | }, 58 | } 59 | }) 60 | defer done() 61 | 62 | if err := c.Send(jsonrpc.Request{ID: "10"}); err != nil { 63 | t.Fatalf("failed to send request: %v", err) 64 | } 65 | 66 | res, err := c.Receive() 67 | if err != nil { 68 | t.Fatalf("failed to receive response: %v", err) 69 | } 70 | 71 | if err := res.Err(); err == nil { 72 | t.Fatal("expected an error, but none occurred") 73 | } 74 | } 75 | 76 | func TestConnSendReceiveOK(t *testing.T) { 77 | req := jsonrpc.Request{ 78 | Method: "hello", 79 | Params: []interface{}{"world"}, 80 | ID: "1", 81 | } 82 | 83 | type message struct { 84 | Message string `json:"message"` 85 | } 86 | 87 | want := message{ 88 | Message: "hello world", 89 | } 90 | 91 | c, _, done := jsonrpc.TestConn(t, func(got jsonrpc.Request) jsonrpc.Response { 92 | if diff := cmp.Diff(req, got); diff != "" { 93 | panicf("unexpected request (-want +got):\n%s", diff) 94 | } 95 | 96 | return jsonrpc.Response{ 97 | ID: strPtr("1"), 98 | Result: mustMarshalJSON(t, want), 99 | } 100 | }) 101 | defer done() 102 | 103 | if err := c.Send(req); err != nil { 104 | t.Fatalf("failed to send request: %v", err) 105 | } 106 | 107 | res, err := c.Receive() 108 | if err != nil { 109 | t.Fatalf("failed to receive response: %v", err) 110 | } 111 | 112 | if err := res.Err(); err != nil { 113 | t.Fatalf("request failed: %v", err) 114 | } 115 | 116 | var out message 117 | if err := json.Unmarshal(res.Result, &out); err != nil { 118 | t.Fatalf("failed to unmarshal JSON: %v", err) 119 | } 120 | 121 | if diff := cmp.Diff(want, out); diff != "" { 122 | t.Fatalf("unexpected response (-want +got):\n%s", diff) 123 | } 124 | } 125 | 126 | func TestConnSendReceiveNotificationsOK(t *testing.T) { 127 | const id = "10" 128 | 129 | req := jsonrpc.Request{ 130 | ID: id, 131 | Method: "monitor", 132 | Params: []interface{}{"Open_vSwitch"}, 133 | } 134 | 135 | res := jsonrpc.Response{ 136 | ID: strPtr(id), 137 | Result: mustMarshalJSON(t, "some bytes"), 138 | } 139 | 140 | c, notifC, done := jsonrpc.TestConn(t, func(got jsonrpc.Request) jsonrpc.Response { 141 | if diff := cmp.Diff(req, got); diff != "" { 142 | panicf("unexpected request (-want +got):\n%s", diff) 143 | } 144 | 145 | return res 146 | }) 147 | defer done() 148 | 149 | note := &jsonrpc.Response{ 150 | Method: "notify", 151 | } 152 | notifC <- note 153 | notifC <- note 154 | 155 | if err := c.Send(req); err != nil { 156 | t.Fatalf("failed to send request: %v", err) 157 | } 158 | 159 | var responses, notes int 160 | for i := 0; i < 3; i++ { 161 | res, err := c.Receive() 162 | if err != nil { 163 | t.Fatalf("failed to receive response: %v", err) 164 | } 165 | 166 | if res.ID != nil { 167 | responses++ 168 | if diff := cmp.Diff(req.ID, *res.ID); diff != "" { 169 | t.Fatalf("unexpected response request ID (-want +got):\n%s", diff) 170 | } 171 | 172 | continue 173 | } 174 | 175 | notes++ 176 | if diff := cmp.Diff(note.Method, res.Method); diff != "" { 177 | t.Fatalf("unexpected notification method (-want +got):\n%s", diff) 178 | } 179 | } 180 | 181 | if diff := cmp.Diff(1, responses); diff != "" { 182 | t.Fatalf("unexpected number of responses (-want +got):\n%s", diff) 183 | } 184 | 185 | if diff := cmp.Diff(2, notes); diff != "" { 186 | t.Fatalf("unexpected number of notifications (-want +got):\n%s", diff) 187 | } 188 | } 189 | 190 | func mustMarshalJSON(t *testing.T, v interface{}) []byte { 191 | t.Helper() 192 | 193 | b, err := json.Marshal(v) 194 | if err != nil { 195 | t.Fatalf("failed to marshal JSON: %v", err) 196 | } 197 | 198 | return b 199 | } 200 | 201 | func strPtr(s string) *string { 202 | return &s 203 | } 204 | 205 | func panicf(format string, a ...interface{}) { 206 | panic(fmt.Sprintf(format, a...)) 207 | } 208 | 209 | type eofReadWriteCloser struct { 210 | io.ReadWriteCloser 211 | } 212 | 213 | func (rwc *eofReadWriteCloser) Read(b []byte) (int, error) { 214 | return 0, io.EOF 215 | } 216 | -------------------------------------------------------------------------------- /ovsdb/internal/jsonrpc/doc.go: -------------------------------------------------------------------------------- 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 jsonrpc is a minimal JSON-RPC 1.0 implementation. 16 | package jsonrpc 17 | -------------------------------------------------------------------------------- /ovsdb/internal/jsonrpc/testconn.go: -------------------------------------------------------------------------------- 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 jsonrpc 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "io" 21 | "log" 22 | "net" 23 | "os" 24 | "strings" 25 | "sync" 26 | "testing" 27 | ) 28 | 29 | // A TestFunc is used to create RPC responses in TestConn. 30 | type TestFunc func(req Request) Response 31 | 32 | // TestConn creates a Conn backed by a server that calls a TestFunc. 33 | // Notifications can be pushed to the client using the channel. 34 | // Invoke the returned closure to clean up its resources. 35 | func TestConn(t *testing.T, fn TestFunc) (*Conn, chan<- *Response, func()) { 36 | t.Helper() 37 | 38 | conn, notifC, done := TestNetConn(t, fn) 39 | 40 | c := NewConn(conn, log.New(os.Stderr, "", 0)) 41 | 42 | return c, notifC, func() { 43 | _ = c.Close() 44 | done() 45 | } 46 | } 47 | 48 | // TestNetConn creates a net.Conn backed by a server that calls a TestFunc. 49 | // Notifications can be pushed to the client using the channel. 50 | // Invoke the returned closure to clean up its resources. 51 | func TestNetConn(t *testing.T, fn TestFunc) (net.Conn, chan<- *Response, func()) { 52 | t.Helper() 53 | 54 | l, err := net.Listen("tcp", ":0") 55 | if err != nil { 56 | t.Fatalf("failed to listen: %v", err) 57 | } 58 | 59 | var wg sync.WaitGroup 60 | wg.Add(1) 61 | 62 | notifC := make(chan *Response, 16) 63 | 64 | go func() { 65 | defer wg.Done() 66 | 67 | // Accept a single connection. 68 | c, err := l.Accept() 69 | if err != nil { 70 | if isNetworkCloseError(err) { 71 | return 72 | } 73 | 74 | panicf("failed to accept: %v", err) 75 | } 76 | defer c.Close() 77 | 78 | dec := json.NewDecoder(c) 79 | 80 | var encMu sync.RWMutex 81 | enc := json.NewEncoder(c) 82 | 83 | // Push RPC notifications to the client. 84 | var notifWG sync.WaitGroup 85 | notifWG.Add(1) 86 | defer notifWG.Wait() 87 | 88 | go func() { 89 | defer notifWG.Done() 90 | 91 | for n := range notifC { 92 | encMu.Lock() 93 | err := enc.Encode(n) 94 | encMu.Unlock() 95 | 96 | if err != nil { 97 | if isNetworkCloseError(err) { 98 | return 99 | } 100 | 101 | panicf("failed to encode notification: %v", err) 102 | } 103 | } 104 | }() 105 | 106 | // Handle RPC requests and responses to and from the client. 107 | for { 108 | var req Request 109 | if err := dec.Decode(&req); err != nil { 110 | if isNetworkCloseError(err) { 111 | return 112 | } 113 | 114 | panicf("failed to decode request: %#v", err) 115 | } 116 | 117 | res := fn(req) 118 | 119 | encMu.Lock() 120 | err := enc.Encode(res) 121 | encMu.Unlock() 122 | 123 | if err != nil { 124 | if isNetworkCloseError(err) { 125 | return 126 | } 127 | 128 | panicf("failed to encode response: %v", err) 129 | } 130 | } 131 | }() 132 | 133 | c, err := net.Dial("tcp", l.Addr().String()) 134 | if err != nil { 135 | t.Fatalf("failed to dial: %v", err) 136 | } 137 | 138 | return c, notifC, func() { 139 | // Ensure types are cleaned up, and ensure goroutine stops. 140 | _ = l.Close() 141 | _ = c.Close() 142 | close(notifC) 143 | wg.Wait() 144 | } 145 | } 146 | 147 | func panicf(format string, a ...interface{}) { 148 | panic(fmt.Sprintf(format, a...)) 149 | } 150 | 151 | func isNetworkCloseError(err error) bool { 152 | return err == io.EOF || 153 | strings.Contains(err.Error(), "use of closed network") || 154 | strings.Contains(err.Error(), "connection reset by peer") 155 | } 156 | -------------------------------------------------------------------------------- /ovsdb/result.go: -------------------------------------------------------------------------------- 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 | ) 22 | 23 | // A result is used to unmarshal JSON-RPC results, and to check for any errors. 24 | type result struct { 25 | Reply interface{} 26 | Err *Error 27 | } 28 | 29 | // An rpcResponse is a response used in RPC callbacks. 30 | type rpcResponse struct { 31 | Result json.RawMessage 32 | Error error 33 | } 34 | 35 | // rpcResult handles any errors from an rpcResponse and unmarshals results into 36 | // a result. 37 | func rpcResult(res rpcResponse, r *result) error { 38 | if err := res.Error; err != nil { 39 | return err 40 | } 41 | 42 | if err := json.Unmarshal(res.Result, &r); err != nil { 43 | return err 44 | } 45 | 46 | // OVSDB server returned an error, return it. 47 | if r.Err != nil { 48 | return r.Err 49 | } 50 | 51 | return nil 52 | } 53 | 54 | // errPrefix is a prefix that occurs if an error is present in a JSON-RPC response. 55 | var errPrefix = []byte(`{"error":`) 56 | 57 | func (r *result) UnmarshalJSON(b []byte) error { 58 | // No error? Return the result. 59 | if !bytes.HasPrefix(b, errPrefix) { 60 | return json.Unmarshal(b, r.Reply) 61 | } 62 | 63 | // Found an error, unmarshal and return it later. 64 | var e Error 65 | if err := json.Unmarshal(b, &e); err != nil { 66 | return err 67 | } 68 | 69 | r.Err = &e 70 | return nil 71 | } 72 | 73 | var _ error = &Error{} 74 | 75 | // An Error is an error returned by an OVSDB server. Its fields can be 76 | // used to determine the cause of an error. 77 | type Error struct { 78 | Err string `json:"error"` 79 | Details string `json:"details"` 80 | Syntax string `json:"syntax"` 81 | } 82 | 83 | func (e *Error) Error() string { 84 | return fmt.Sprintf("%s: %s: %s", e.Err, e.Details, e.Syntax) 85 | } 86 | -------------------------------------------------------------------------------- /ovsdb/rpc.go: -------------------------------------------------------------------------------- 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 | "context" 19 | "fmt" 20 | ) 21 | 22 | // ListDatabases returns the name of all databases known to the OVSDB server. 23 | func (c *Client) ListDatabases(ctx context.Context) ([]string, error) { 24 | var dbs []string 25 | if err := c.rpc(ctx, "list_dbs", &dbs, nil); err != nil { 26 | return nil, err 27 | } 28 | 29 | return dbs, nil 30 | } 31 | 32 | // Echo verifies that the OVSDB connection is alive, and can be used to keep 33 | // the connection alive. 34 | func (c *Client) Echo(ctx context.Context) error { 35 | req := [1]string{"github.com/digitalocean/go-openvswitch/ovsdb"} 36 | 37 | var res [1]string 38 | if err := c.rpc(ctx, "echo", &res, req); err != nil { 39 | return err 40 | } 41 | 42 | if res[0] != req[0] { 43 | return fmt.Errorf("invalid echo response: %q", res[0]) 44 | } 45 | 46 | return nil 47 | } 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 | } 81 | -------------------------------------------------------------------------------- /ovsdb/rpc_test.go: -------------------------------------------------------------------------------- 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 | "context" 19 | "testing" 20 | 21 | "github.com/digitalocean/go-openvswitch/ovsdb" 22 | "github.com/digitalocean/go-openvswitch/ovsdb/internal/jsonrpc" 23 | "github.com/google/go-cmp/cmp" 24 | ) 25 | 26 | func TestClientListDatabases(t *testing.T) { 27 | want := []string{"Open_vSwitch", "test"} 28 | 29 | c, _, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response { 30 | if diff := cmp.Diff("list_dbs", req.Method); diff != "" { 31 | panicf("unexpected RPC method (-want +got):\n%s", diff) 32 | } 33 | 34 | // Client should send an empty array parameter. 35 | ps := req.Params.([]interface{}) 36 | 37 | if diff := cmp.Diff(0, len(ps)); diff != "" { 38 | panicf("unexpected number of RPC parameters (-want +got):\n%s", diff) 39 | } 40 | 41 | return jsonrpc.Response{ 42 | ID: strPtr("1"), 43 | Result: mustMarshalJSON(t, want), 44 | } 45 | }) 46 | defer done() 47 | 48 | dbs, err := c.ListDatabases(context.Background()) 49 | if err != nil { 50 | t.Fatalf("failed to list databases: %v", err) 51 | } 52 | 53 | if diff := cmp.Diff(want, dbs); diff != "" { 54 | t.Fatalf("unexpected databases (-want +got):\n%s", diff) 55 | } 56 | } 57 | 58 | func TestClientEchoError(t *testing.T) { 59 | c, _, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response { 60 | return jsonrpc.Response{ 61 | ID: strPtr("1"), 62 | Result: mustMarshalJSON(t, []string{"foo"}), 63 | } 64 | }) 65 | defer done() 66 | 67 | if err := c.Echo(context.Background()); err == nil { 68 | t.Fatal("expected an error, but none occurred") 69 | } 70 | } 71 | 72 | func TestClientEchoOK(t *testing.T) { 73 | const echo = "github.com/digitalocean/go-openvswitch/ovsdb" 74 | 75 | c, _, done := testClient(t, func(req jsonrpc.Request) jsonrpc.Response { 76 | if diff := cmp.Diff("echo", req.Method); diff != "" { 77 | panicf("unexpected RPC method (-want +got):\n%s", diff) 78 | } 79 | 80 | if diff := cmp.Diff([]interface{}{echo}, req.Params); diff != "" { 81 | panicf("unexpected RPC parameters (-want +got):\n%s", diff) 82 | } 83 | 84 | return jsonrpc.Response{ 85 | ID: strPtr("1"), 86 | Result: mustMarshalJSON(t, []string{echo}), 87 | } 88 | }) 89 | defer done() 90 | 91 | if err := c.Echo(context.Background()); err != nil { 92 | t.Fatalf("failed to echo: %v", err) 93 | } 94 | } 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 | } 155 | -------------------------------------------------------------------------------- /ovsdb/transact.go: -------------------------------------------------------------------------------- 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 | } 105 | -------------------------------------------------------------------------------- /ovsnl/README.md: -------------------------------------------------------------------------------- 1 | ovsnl 2 | ===== 3 | 4 | Package `ovsnl` enables interaction with the Linux Open vSwitch generic 5 | netlink interface. 6 | 7 | ```go 8 | // Dial a generic netlink connection and create a *ovsnl.Client. 9 | c, err := ovsnl.New() 10 | if err != nil { 11 | // If OVS generic netlink families aren't available, do nothing. 12 | if os.IsNotExist(err) { 13 | log.Printf("generic netlink OVS families not found: %v", err) 14 | return 15 | } 16 | 17 | log.Fatalf("failed to create client %v", err) 18 | } 19 | // Be sure to close the generic netlink connection! 20 | defer c.Close() 21 | 22 | // List available OVS datapaths. 23 | dps, err := c.Datapath.List() 24 | if err != nil { 25 | log.Fatalf("failed to list datapaths: %v", err) 26 | } 27 | 28 | for _, d := range dps { 29 | log.Printf("datapath: %q, flows: %d", d.Name, d.Stats.Flows) 30 | } 31 | ``` -------------------------------------------------------------------------------- /ovsnl/client.go: -------------------------------------------------------------------------------- 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 ovsnl 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strings" 21 | "unsafe" 22 | 23 | "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" 24 | "github.com/mdlayher/genetlink" 25 | ) 26 | 27 | // Sizes of various structures, used in unsafe casts. 28 | const ( 29 | sizeofHeader = int(unsafe.Sizeof(ovsh.Header{})) 30 | 31 | sizeofDPStats = int(unsafe.Sizeof(ovsh.DPStats{})) 32 | sizeofDPMegaflowStats = int(unsafe.Sizeof(ovsh.DPMegaflowStats{})) 33 | ) 34 | 35 | // A Client is a Linux Open vSwitch generic netlink client. 36 | type Client struct { 37 | // Datapath provides access to DatapathService methods. 38 | Datapath *DatapathService 39 | 40 | c *genetlink.Conn 41 | } 42 | 43 | // New creates a new Linux Open vSwitch generic netlink client. 44 | // 45 | // If no OvS generic netlink families are available on this system, an 46 | // error will be returned which can be checked using os.IsNotExist. 47 | func New() (*Client, error) { 48 | c, err := genetlink.Dial(nil) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return newClient(c) 54 | } 55 | 56 | // newClient is the internal Client constructor, used in tests. 57 | func newClient(c *genetlink.Conn) (*Client, error) { 58 | // Must ensure that the generic netlink connection is closed on any errors 59 | // that occur before it is returned to the caller. 60 | 61 | families, err := c.ListFamilies() 62 | if err != nil { 63 | _ = c.Close() 64 | return nil, err 65 | } 66 | 67 | client := &Client{c: c} 68 | if err := client.init(families); err != nil { 69 | _ = c.Close() 70 | return nil, err 71 | } 72 | 73 | return client, nil 74 | } 75 | 76 | // Close closes the Client's generic netlink connection. 77 | func (c *Client) Close() error { 78 | return c.c.Close() 79 | } 80 | 81 | // init initializes the generic netlink family service of Client. 82 | func (c *Client) init(families []genetlink.Family) error { 83 | var gotf int 84 | 85 | for _, f := range families { 86 | // Ignore any families without the OVS prefix. 87 | if !strings.HasPrefix(f.Name, "ovs_") { 88 | continue 89 | } 90 | // Ignore any families that might be unknown. 91 | if err := c.initFamily(f); err != nil { 92 | continue 93 | } 94 | gotf++ 95 | } 96 | 97 | // No known families; return error for os.IsNotExist check. 98 | if gotf == 0 { 99 | return os.ErrNotExist 100 | } 101 | 102 | return nil 103 | } 104 | 105 | // initFamily initializes a single generic netlink family service. 106 | func (c *Client) initFamily(f genetlink.Family) error { 107 | switch f.Name { 108 | case ovsh.DatapathFamily: 109 | c.Datapath = &DatapathService{ 110 | f: f, 111 | c: c, 112 | } 113 | return nil 114 | default: 115 | // Unknown OVS netlink family, nothing we can do. 116 | return fmt.Errorf("unknown OVS generic netlink family: %q", f.Name) 117 | } 118 | } 119 | 120 | // headerBytes converts an ovsh.Header into a byte slice. 121 | func headerBytes(h ovsh.Header) []byte { 122 | b := *(*[sizeofHeader]byte)(unsafe.Pointer(&h)) 123 | return b[:] 124 | } 125 | 126 | // parseHeader converts a byte slice into ovsh.Header. 127 | func parseHeader(b []byte) (ovsh.Header, error) { 128 | // Verify that the byte slice is long enough before doing unsafe casts. 129 | if l := len(b); l < sizeofHeader { 130 | return ovsh.Header{}, fmt.Errorf("not enough data for OVS message header: %d bytes", l) 131 | } 132 | 133 | h := *(*ovsh.Header)(unsafe.Pointer(&b[:sizeofHeader][0])) 134 | return h, nil 135 | } 136 | -------------------------------------------------------------------------------- /ovsnl/client_linux_integration_test.go: -------------------------------------------------------------------------------- 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 | //+build linux 16 | 17 | package ovsnl_test 18 | 19 | import ( 20 | "net" 21 | "os" 22 | "testing" 23 | 24 | "github.com/digitalocean/go-openvswitch/ovsnl" 25 | "github.com/google/go-cmp/cmp" 26 | ) 27 | 28 | func TestLinuxClientIntegration(t *testing.T) { 29 | c, err := ovsnl.New() 30 | if err != nil { 31 | if os.IsNotExist(err) { 32 | t.Skipf("generic netlink OVS families not found: %v", err) 33 | } 34 | 35 | t.Fatalf("failed to create client %v", err) 36 | } 37 | defer c.Close() 38 | 39 | const ( 40 | ovsSystem = "ovs-system" 41 | ovsBridge = "ovsbr0" 42 | ) 43 | 44 | // Ensure required interfaces exist for remaining tests. 45 | for _, ifi := range []string{ovsSystem, ovsBridge} { 46 | if _, err := net.InterfaceByName(ifi); err != nil { 47 | t.Skipf("failed to check for OVS interface %q: %v", ifi, err) 48 | } 49 | } 50 | 51 | t.Run("datapath", func(t *testing.T) { 52 | testClientDatapath(t, c, ovsSystem) 53 | }) 54 | } 55 | 56 | func testClientDatapath(t *testing.T, c *ovsnl.Client, datapath string) { 57 | dps, err := c.Datapath.List() 58 | if err != nil { 59 | t.Fatalf("failed to list datapaths: %v", err) 60 | } 61 | 62 | if diff := cmp.Diff(1, len(dps)); diff != "" { 63 | t.Fatalf("unexpected number of datapaths (-want +got):\n%s", diff) 64 | } 65 | 66 | if diff := cmp.Diff(datapath, dps[0].Name); diff != "" { 67 | t.Fatalf("unexpected datapath name (-want +got):\n%s", diff) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ovsnl/client_linux_test.go: -------------------------------------------------------------------------------- 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 | //+build linux 16 | 17 | package ovsnl 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "testing" 23 | 24 | "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" 25 | "github.com/mdlayher/genetlink" 26 | "github.com/mdlayher/genetlink/genltest" 27 | "github.com/mdlayher/netlink" 28 | "github.com/mdlayher/netlink/nlenc" 29 | "golang.org/x/sys/unix" 30 | ) 31 | 32 | func TestClientNoFamiliesIsNotExist(t *testing.T) { 33 | conn := genltest.Dial(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { 34 | // Unrelated generic netlink families. 35 | return familyMessages([]string{ 36 | "TASKSTATS", 37 | "nl80211", 38 | }), nil 39 | }) 40 | 41 | _, err := newClient(conn) 42 | if !os.IsNotExist(err) { 43 | t.Fatalf("expected is not exist error, but got: %v", err) 44 | } 45 | 46 | t.Logf("OK error: %v", err) 47 | } 48 | 49 | func TestClientUnknownFamilies(t *testing.T) { 50 | conn := genltest.Dial(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { 51 | return familyMessages([]string{ 52 | "ovs_foo", 53 | }), nil 54 | }) 55 | 56 | _, err := newClient(conn) 57 | if err == nil { 58 | t.Fatalf("expected an error, but none occurred") 59 | } 60 | 61 | t.Logf("OK error: %v", err) 62 | } 63 | 64 | func TestClientNoFamilies(t *testing.T) { 65 | conn := genltest.Dial(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { 66 | // Too few OVS families. 67 | return nil, nil 68 | }) 69 | 70 | _, err := newClient(conn) 71 | if err == nil { 72 | t.Fatalf("expected an error, but none occurred") 73 | } 74 | 75 | t.Logf("OK error: %v", err) 76 | } 77 | 78 | func TestClientKnownFamilies(t *testing.T) { 79 | conn := genltest.Dial(func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { 80 | return familyMessages([]string{ 81 | ovsh.DatapathFamily, 82 | }), nil 83 | }) 84 | 85 | _, err := newClient(conn) 86 | if err != nil { 87 | t.Fatalf("failed to create client: %v", err) 88 | } 89 | } 90 | 91 | func familyMessages(families []string) []genetlink.Message { 92 | msgs := make([]genetlink.Message, 0, len(families)) 93 | 94 | var id uint16 95 | for _, f := range families { 96 | msgs = append(msgs, genetlink.Message{ 97 | Data: mustMarshalAttributes([]netlink.Attribute{ 98 | { 99 | Type: unix.CTRL_ATTR_FAMILY_ID, 100 | Data: nlenc.Uint16Bytes(id), 101 | }, 102 | { 103 | Type: unix.CTRL_ATTR_FAMILY_NAME, 104 | Data: nlenc.Bytes(f), 105 | }, 106 | }), 107 | }) 108 | 109 | id++ 110 | } 111 | 112 | return msgs 113 | } 114 | 115 | // ovsFamilies creates a genltest.Func which intercepts "list family" requests 116 | // and returns all the OVS families. Other requests are passed through to fn. 117 | func ovsFamilies(fn genltest.Func) genltest.Func { 118 | return func(greq genetlink.Message, nreq netlink.Message) ([]genetlink.Message, error) { 119 | if nreq.Header.Type == unix.GENL_ID_CTRL && greq.Header.Command == unix.CTRL_CMD_GETFAMILY { 120 | return familyMessages([]string{ 121 | ovsh.DatapathFamily, 122 | ovsh.FlowFamily, 123 | ovsh.PacketFamily, 124 | ovsh.VportFamily, 125 | ovsh.MeterFamily, 126 | }), nil 127 | } 128 | 129 | return fn(greq, nreq) 130 | } 131 | } 132 | 133 | func mustMarshalAttributes(attrs []netlink.Attribute) []byte { 134 | b, err := netlink.MarshalAttributes(attrs) 135 | if err != nil { 136 | panic(fmt.Sprintf("failed to marshal attributes: %v", err)) 137 | } 138 | 139 | return b 140 | } 141 | -------------------------------------------------------------------------------- /ovsnl/datapath.go: -------------------------------------------------------------------------------- 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 ovsnl 16 | 17 | import ( 18 | "fmt" 19 | "unsafe" 20 | 21 | "github.com/digitalocean/go-openvswitch/ovsnl/internal/ovsh" 22 | "github.com/mdlayher/genetlink" 23 | "github.com/mdlayher/netlink" 24 | "github.com/mdlayher/netlink/nlenc" 25 | ) 26 | 27 | // A DatapathService provides access to methods which interact with the 28 | // "ovs_datapath" generic netlink family. 29 | type DatapathService struct { 30 | c *Client 31 | f genetlink.Family 32 | } 33 | 34 | // A Datapath is an Open vSwitch in-kernel datapath. 35 | type Datapath struct { 36 | Index int 37 | Name string 38 | Features DatapathFeatures 39 | Stats DatapathStats 40 | MegaflowStats DatapathMegaflowStats 41 | } 42 | 43 | // DatapathFeatures is a set of bit flags that specify features for a datapath. 44 | type DatapathFeatures uint32 45 | 46 | // Possible DatapathFeatures flag values. 47 | const ( 48 | DatapathFeaturesUnaligned DatapathFeatures = ovsh.DpFUnaligned 49 | DatapathFeaturesVPortPIDs DatapathFeatures = ovsh.DpFVportPids 50 | ) 51 | 52 | // String returns the string representation of a DatapathFeatures. 53 | func (f DatapathFeatures) String() string { 54 | names := []string{ 55 | "unaligned", 56 | "vportpids", 57 | } 58 | 59 | var s string 60 | for i, name := range names { 61 | if f&(1< struct.go" 27 | 28 | // Apply license headers to generated files. 29 | //go:generate ../../../scripts/prependlicense.sh const.go 30 | //go:generate ../../../scripts/prependlicense.sh struct.go 31 | 32 | // Clean up build artifacts. 33 | //go:generate rm -rf openvswitch.h _obj/ 34 | -------------------------------------------------------------------------------- /ovsnl/internal/ovsh/ovsh.yml: -------------------------------------------------------------------------------- 1 | --- 2 | GENERATOR: 3 | PackageName: ovsh 4 | 5 | PARSER: 6 | IncludePaths: [/usr/include] 7 | SourcesPaths: [openvswitch.h] 8 | 9 | TRANSLATOR: 10 | ConstRules: 11 | defines: expand 12 | enum: expand 13 | Rules: 14 | const: 15 | - {transform: lower} 16 | - {action: accept, from: "(?i)ovs_"} 17 | - {action: replace, from: "(?i)ovs_", to: _} 18 | - {transform: export} 19 | post-global: 20 | - {load: snakecase} 21 | -------------------------------------------------------------------------------- /ovsnl/internal/ovsh/struct.go: -------------------------------------------------------------------------------- 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 | // Created by cgo -godefs - DO NOT EDIT 16 | // cgo -godefs types.go 17 | 18 | package ovsh 19 | 20 | type Header struct { 21 | Ifindex int32 22 | } 23 | 24 | type DPStats struct { 25 | Hit uint64 26 | Missed uint64 27 | Lost uint64 28 | Flows uint64 29 | } 30 | 31 | type DPMegaflowStats struct { 32 | Mask_hit uint64 33 | Masks uint32 34 | Pad0 uint32 35 | Pad1 uint64 36 | Pad2 uint64 37 | } 38 | 39 | type VportStats struct { 40 | Rx_packets uint64 41 | Tx_packets uint64 42 | Rx_bytes uint64 43 | Tx_bytes uint64 44 | Rx_errors uint64 45 | Tx_errors uint64 46 | Rx_dropped uint64 47 | Tx_dropped uint64 48 | } 49 | 50 | type FlowStats struct { 51 | Packets uint64 52 | Bytes uint64 53 | } 54 | -------------------------------------------------------------------------------- /ovsnl/internal/ovsh/types.go: -------------------------------------------------------------------------------- 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 | //+build ignore 16 | 17 | package ovsh 18 | 19 | // #include "openvswitch.h" 20 | import "C" 21 | 22 | type Header C.struct_ovs_header 23 | 24 | type DPStats C.struct_ovs_dp_stats 25 | 26 | type DPMegaflowStats C.struct_ovs_dp_megaflow_stats 27 | 28 | type VportStats C.struct_ovs_vport_stats 29 | 30 | type FlowStats C.struct_ovs_flow_stats 31 | -------------------------------------------------------------------------------- /scripts/gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify that all files are correctly gofmt'd, with the exception of 4 | # generated code. 5 | EXIT=0 6 | GOFMT=$(go fmt ./... | grep -v -E "ovsnl.*test|ovsnl/internal/ovsh" ) 7 | 8 | if [[ ! -z $GOFMT ]]; then 9 | echo -e "Files that are not gofmt'd:\n" 10 | echo "$GOFMT" 11 | EXIT=1 12 | fi 13 | 14 | exit $EXIT 15 | -------------------------------------------------------------------------------- /scripts/golint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify that all files are correctly golint'd, with the exception of 4 | # generated code. 5 | EXIT=0 6 | GOLINT=$(golint ./... | grep -v -E "ovsnl.*test|ovsnl/internal/ovsh") 7 | 8 | if [[ ! -z $GOLINT ]]; then 9 | echo "$GOLINT" 10 | EXIT=1 11 | fi 12 | 13 | exit $EXIT 14 | -------------------------------------------------------------------------------- /scripts/license.txt: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /scripts/licensecheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Verify that the correct license block is present in all Go source 4 | # files. 5 | LICENSE=./scripts/license.txt 6 | NB_LINES=$(cat $LICENSE| wc -l) 7 | EXPECTED=$(head -$NB_LINES $LICENSE | tail +2) 8 | 9 | # Update the string if there is a new year to consider 10 | COPYRIGHT_YEARS="2017 2021" 11 | 12 | # Scan each Go source file for license. 13 | EXIT=0 14 | GOFILES=$(find . -name "*.go") 15 | 16 | for FILE in $GOFILES; do 17 | # Start validating the Copyright line of for each header. 18 | # Years can change from a source file to another. 19 | read -r -a COPYRIGHT_TOKENS <<< $(head -n 1 $FILE |awk '{print $2 " " $3 " " $4}') 20 | if [ "${COPYRIGHT_TOKENS[0]}" != "Copyright" ]; then 21 | echo "Bad Copyright token ${COPYRIGHT_TOKENS[0]} in $FILE" 22 | EXIT=1 23 | elif [ $(echo $COPYRIGHT_YEARS | grep -c ${COPYRIGHT_TOKENS[1]}) != "1" ]; then 24 | echo "Bad Year token ${COPYRIGHT_TOKENS[1]} in $FILE" 25 | EXIT=1 26 | elif [ "${COPYRIGHT_TOKENS[2]}" != "DigitalOcean." ]; then 27 | echo "Bad DigitalOcean token ${COPYRIGHT_TOKENS[2]} in $FILE" 28 | EXIT=1 29 | fi 30 | 31 | BLOCK=$(head -n 14 $FILE|tail +2) 32 | 33 | if [ "$BLOCK" != "$EXPECTED" ]; then 34 | echo "file missing license: $FILE" 35 | EXIT=1 36 | fi 37 | done 38 | 39 | exit $EXIT 40 | -------------------------------------------------------------------------------- /scripts/prependlicense.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # License block to be prepended to file 4 | BASEDIR=$(dirname "$0") 5 | LICENSE=$(cat $BASEDIR/license.txt) 6 | 7 | if [ -z "$1" ]; then 8 | echo "missing filename argument" 9 | echo "usage: prependlicense.sh [filename]" 10 | exit 1 11 | fi 12 | 13 | # Prepend license block to file 14 | echo -e "$LICENSE\n" | cat - $1 > .prependlicense && mv .prependlicense $1 15 | --------------------------------------------------------------------------------