├── demo ├── radius │ └── freeradius │ │ ├── certs │ │ ├── ca.serial │ │ ├── ca.ext │ │ ├── server.ext │ │ ├── dh │ │ ├── ca.csr │ │ ├── server.csr │ │ ├── generate_cert.sh │ │ ├── ca.key │ │ ├── server.key │ │ ├── server.pem │ │ └── ca.pem │ │ ├── dictionary │ │ ├── sites-enabled │ │ ├── default │ │ └── inner-tunnel │ │ ├── users │ │ ├── eap.conf │ │ └── radiusd.conf ├── faucet_ovs │ ├── ovs.png │ ├── interface_layout.png │ ├── cleanup.sh │ ├── faucet.yaml │ ├── gauge.yaml │ ├── README.md │ └── setup.sh ├── facuet_hw │ ├── ovs_hw.png │ ├── interface_layout.png │ ├── cleanup.sh │ ├── at.conf │ ├── README.md │ ├── faucet.yaml │ └── setup.sh ├── Link022_diagrams-demo.png ├── link022-gasket-diagram.png ├── samples │ └── gnmi_set.sh ├── gasket │ └── faucet │ │ ├── faucet.yaml │ │ └── gasket │ │ ├── rules.yaml │ │ ├── base-no-authed-acls.yaml │ │ └── auth.yaml ├── util │ ├── servers.conf │ ├── cleanup_servers.sh │ └── server.sh ├── cert │ ├── generate_cert.sh │ ├── client │ │ ├── client.crt │ │ ├── ca.crt │ │ └── client.key │ └── server │ │ ├── ca.crt │ │ ├── server.crt │ │ └── server.key ├── README.gasket.md ├── ap_config_1r.json ├── ap_config_2r.json └── README.md ├── tests ├── ap_provision.json ├── README.md ├── ap_dot11r.json ├── ap_apmanagers.json ├── link022_ap_config.json ├── ap_config.json └── integration.py ├── .travis.yml ├── testkit ├── testdata │ ├── provisionaps_test.json │ ├── dot11r_test.json │ ├── apmanagers_test.json │ └── simple_test.json ├── gnmitest │ ├── testfile │ │ └── ap_config.json │ └── worker_test.go ├── common │ └── common.go ├── test_kit.go └── util │ └── gnmiutil │ ├── helper_test.go │ └── helper.go ├── doc.go ├── agent ├── syscmd │ ├── states.go │ ├── device.go │ ├── hostapd.go │ ├── bridge.go │ ├── base.go │ ├── syscmd_test.go │ └── interface.go ├── context │ └── context.go ├── monitoring │ └── prometheus │ │ ├── exposition_server.go │ │ └── README.md ├── README.md ├── service │ ├── base.go │ └── interface.go ├── util │ ├── mock │ │ └── openconfig_test.go │ └── ocutil │ │ ├── openconfig.go │ │ └── openconfig_test.go ├── controller │ └── controller.go ├── gnmi │ ├── handler.go │ └── server.go └── agent.go ├── test.sh ├── CONTRIBUTING.md ├── openconfig ├── models │ └── gasket.yang ├── examples │ └── openconfig.go └── scripts │ └── generate_wifi_oc_schema.sh ├── README.md └── emulator ├── emulator.py └── README.md /demo/radius/freeradius/certs/ca.serial: -------------------------------------------------------------------------------- 1 | 02 2 | -------------------------------------------------------------------------------- /demo/radius/freeradius/dictionary: -------------------------------------------------------------------------------- 1 | $INCLUDE /usr/share/freeradius/dictionary 2 | -------------------------------------------------------------------------------- /demo/faucet_ovs/ovs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/faucet_ovs/ovs.png -------------------------------------------------------------------------------- /demo/facuet_hw/ovs_hw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/facuet_hw/ovs_hw.png -------------------------------------------------------------------------------- /demo/Link022_diagrams-demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/Link022_diagrams-demo.png -------------------------------------------------------------------------------- /demo/link022-gasket-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/link022-gasket-diagram.png -------------------------------------------------------------------------------- /demo/facuet_hw/interface_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/facuet_hw/interface_layout.png -------------------------------------------------------------------------------- /demo/faucet_ovs/interface_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/link022/HEAD/demo/faucet_ovs/interface_layout.png -------------------------------------------------------------------------------- /tests/ap_provision.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-ap-manager:config":{ 3 | "mac":"00:11:22:33:44:55", 4 | "country-code":"US", 5 | "hostname":"link022-pi-ap" 6 | } 7 | } -------------------------------------------------------------------------------- /demo/radius/freeradius/sites-enabled/default: -------------------------------------------------------------------------------- 1 | authorize { 2 | eap { 3 | ok = return 4 | } 5 | users 6 | } 7 | 8 | authenticate { 9 | eap 10 | } 11 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/ca.ext: -------------------------------------------------------------------------------- 1 | extensions = x509v3 2 | 3 | [ x509v3 ] 4 | basicConstraints = CA:true,pathlen:0 5 | nsCertType = sslCA,emailCA,objCA 6 | nsComment = "Sunnyvale CA" 7 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/server.ext: -------------------------------------------------------------------------------- 1 | extensions = x509v3 2 | 3 | [ x509v3 ] 4 | nsCertType = server 5 | keyUsage = digitalSignature,nonRepudiation,keyEncipherment 6 | extendedKeyUsage = msSGC,nsSGC,serverAuth 7 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/dh: -------------------------------------------------------------------------------- 1 | -----BEGIN DH PARAMETERS----- 2 | MIGHAoGBAIdyYPu0soYoH6urV2CodQL7wYX3upco6BYd+Y+9YP5VTMsK9X+q84WE 3 | Qic9CqFjYr0GU3j8ETtHCkKo9wzLVbJsUUsTcd2YG1N0g4skzW5bJZm27UpcKf34 4 | Ye2wlQqSEMcXKM3jK5HjEkE/6pF1fJszxiTrbAcVJw3DYUJLHnCrAgEC 5 | -----END DH PARAMETERS----- 6 | -------------------------------------------------------------------------------- /demo/radius/freeradius/users: -------------------------------------------------------------------------------- 1 | host-authed Cleartext-Password := "authedpwd" 2 | Tunnel-Type = VLAN, 3 | Tunnel-Medium-Type = IEEE-802, 4 | Tunnel-Private-Group-Id = "authed", 5 | Reply-Message = "welcome-host-authed", 6 | Session-Timeout = 3600, 7 | Termination-Action = RADIUS-Request 8 | -------------------------------------------------------------------------------- /demo/samples/gnmi_set.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=$PATH:~/go/bin 4 | sudo env PATH=$PATH ip netns exec lk22 gnmi_set \ 5 | -ca=../cert/client/ca.crt \ 6 | -cert=../cert/client/client.crt \ 7 | -key=../cert/client/client.key \ 8 | -target_name=www.example.com \ 9 | -target_addr=192.168.11.8:10162 \ 10 | -replace=/:@../ap_config_1r.json 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - master 5 | - 1.10.x 6 | 7 | notifications: 8 | email: false 9 | 10 | before_install: 11 | - go get -t -v ./... 12 | 13 | script: 14 | - ./test.sh 15 | - go vet ./... 16 | - go build -o binary/link022_agent agent/agent.go 17 | 18 | after_success: 19 | - bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /testkit/testdata/provisionaps_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Assign hostname to a MAC & set it's country-code.", 3 | "description":"Day 0 provisioning.", 4 | "test_cases":[ 5 | { 6 | "name":"Push Hostname & CC", 7 | "description":"Initial assignment of hostname & CC.", 8 | "ops":[ 9 | { 10 | "type":"update", 11 | "path":"/provision-aps/provision-ap[mac=00:11:22:33:44:55]", 12 | "val":"@../tests/ap_provision.json" 13 | } 14 | ] 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /demo/radius/freeradius/sites-enabled/inner-tunnel: -------------------------------------------------------------------------------- 1 | ###################################################################### 2 | # 3 | # This is a virtual server that handles *only* inner tunnel 4 | # requests for EAP-TTLS and PEAP types. 5 | # 6 | # 7 | ###################################################################### 8 | 9 | server inner-tunnel { 10 | 11 | authorize { 12 | eap { 13 | ok = return 14 | } 15 | users 16 | } 17 | 18 | authenticate { 19 | eap 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demo/gasket/faucet/faucet.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | vlans: 3 | 100: 4 | 200: 5 | 300: 6 | dps: 7 | faucet-1: 8 | dp_id: 0x01 9 | hardware: "Open vSwitch" 10 | interfaces: 11 | 1: 12 | native_vlan: 100 13 | acl_in: port_faucet-1_1 14 | tagged_vlans: [200, 300] 15 | 2: 16 | native_vlan: 100 17 | 3: 18 | native_vlan: 100 19 | tagged_vlans: [200, 300] 20 | include: 21 | - /etc/ryu/faucet/faucet-acls.yaml 22 | 23 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | https://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. 11 | */ 12 | 13 | package link022 14 | -------------------------------------------------------------------------------- /demo/util/servers.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | NS=lk22 16 | -------------------------------------------------------------------------------- /demo/cert/generate_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p tls_cert_key 4 | 5 | mkdir -p tls_cert_key/server 6 | openssl req -x509 -newkey rsa:4096 -keyout tls_cert_key/server/server.key -out tls_cert_key/server/server.crt -days 3650 -nodes -subj "/C=US/ST=CA/L=Sunnyvale/O=GNMI/OU=Org/CN=www.example.com" -sha256 7 | 8 | mkdir -p tls_cert_key/client 9 | openssl req -x509 -newkey rsa:4096 -keyout tls_cert_key/client/client.key -out tls_cert_key/client/client.crt -days 3650 -nodes -subj "/C=US/ST=CA/L=Sunnyvale/O=GNMI/OU=Test/CN=AP" -sha256 10 | 11 | cp tls_cert_key/server/server.crt tls_cert_key/client/ca.crt 12 | cp tls_cert_key/client/client.crt tls_cert_key/server/ca.crt -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/ca.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBoTCCAQoCAQAwYTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH 3 | DA1Nb3VudGFpbiBWaWV3MREwDwYDVQQKDAhMSU5LMDAyMjENMAsGA1UECwwEVGVz 4 | dDELMAkGA1UEAwwCQVAwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKSlC08i 5 | gNJjUO4XOzyZy5NeU2l/G5GORofXMf/akI8U07yoj6itXUIW6ZiGtCx0Fo3Ed4lp 6 | FUfAaLAIdYBT5TT2KJ+C0F4wBB+fvRFPMZz1E5SVYFqOlEmcGvzdAMch5loq2iuf 7 | YMc63t69bC0EmQifyIjdgQ3btOUEABm5xWidAgMBAAGgADANBgkqhkiG9w0BAQsF 8 | AAOBgQCYypJYJ3CWqggZy7nS2UeWbWr9gNiHeWnl7vr69tc40HiA7ggj75SCqAk7 9 | gTwqlEBZYGN0RgvoKNlWIbeprDfGo95QpiSZzyxjgW+SddGqlmI8UYVoZH+jNNvm 10 | EEz781BWpM51uydVlasoU94s85Cj/eaO/dj9FhmIRPisNB44Rw== 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/server.csr: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE REQUEST----- 2 | MIIBoTCCAQoCAQAwYTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRYwFAYDVQQH 3 | DA1Nb3VudGFpbiBWaWV3MREwDwYDVQQKDAhMSU5LMDAyMjENMAsGA1UECwwEVGVz 4 | dDELMAkGA1UEAwwCQVAwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKUtbjHV 5 | DRnptj9T/VMnYhFmGkHNmcC6N4b4NIOKDqS5/HefFbAcyncBeOpDGgjVLv2v26wk 6 | tyL5NswbPz4DG+2DYoCcs+rWWLw1rPyE08FDjM3utbB0shVmYcgZ5hclPnIlqDxV 7 | ZjPxeCnjycnN6G83DE3GEYJiom8utDLhVkURAgMBAAGgADANBgkqhkiG9w0BAQsF 8 | AAOBgQBVz+H73MGWCrv6erH4yj4M/O0YZIvRTVlHjQkrPxh2iKlSt0byrZQFC77K 9 | UCQ6An/ph2pvnv/taILLbJp5PnruEtBo76wdeecJzE/s8r4WT4qF/FuFDXLA7Uym 10 | XNK0yePXkh5utFm5J4k/t22GGoAyfUq/ZWcPxchs4rlgEmS4Mw== 11 | -----END CERTIFICATE REQUEST----- 12 | -------------------------------------------------------------------------------- /demo/gasket/faucet/gasket/rules.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | 3 | acls: 4 | allow-all: 5 | _authport_: 6 | *allowall 7 | 8 | block-tcp: 9 | _authport_: 10 | - rule: 11 | _name_: _user-name_ 12 | _mac_: _user-mac_ 13 | dl_src: _user-mac_ 14 | dl_type: 0x800 15 | ip_proto: 6 16 | actions: 17 | allow: 0 18 | 19 | block-udp: 20 | _authport_: 21 | - rule: 22 | _name_: _user-name_ 23 | _mac_: _user-mac_ 24 | dl_src: _user-mac_ 25 | dl_type: 0x800 26 | ip_proto: 17 27 | actions: 28 | allow: 0 29 | 30 | -------------------------------------------------------------------------------- /demo/faucet_ovs/cleanup.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 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 | # https://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 | #!/bin/bash 16 | 17 | sudo killall dnsmasq 18 | sudo killall freeradius 19 | sudo ip netns del gw -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/generate_cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Create A Self-Signed CA Certificate 4 | openssl genrsa -out ca.key 1024 5 | openssl req -new -subj \ 6 | '/C=US/ST=CA/L=Mountain View/O=LINK0022/OU=Test/CN=AP' -key ca.key -out ca.csr 7 | openssl x509 -days 1095 -extfile ca.ext -signkey ca.key -in ca.csr -req -out ca.pem 8 | 9 | # Create A Server Certificate 10 | openssl genrsa -out server.key 1024 11 | echo -ne '01' > ca.serial 12 | openssl req -subj \ 13 | '/C=US/ST=CA/L=Mountain View/O=LINK0022/OU=Test/CN=AP' -new -key server.key -out server.csr 14 | openssl x509 -days 730 -extfile server.ext -CA ca.pem -CAkey ca.key -CAserial ca.serial -in server.csr -req -out server.pem 15 | 16 | # Create dh. 17 | openssl dhparam -out dh 1024 18 | -------------------------------------------------------------------------------- /demo/util/cleanup_servers.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | #!/bin/bash 16 | # A utility script to start clean up the servers created by servers.sh 17 | source servers.conf 18 | sudo killall dnsmasq 19 | sudo killall freeradius 20 | sudo ip netns del ${NS} 21 | -------------------------------------------------------------------------------- /demo/gasket/faucet/gasket/base-no-authed-acls.yaml: -------------------------------------------------------------------------------- 1 | acls: 2 | port_faucet-1_2: 3 | - authed-rules 4 | # drop unauthenticated hosts on the tagged auth VLAN. 5 | # this could possibly be removed, as the AP (hostapd) controls the access. 6 | # and if you're on the vlan, you must be allowed. 7 | - rule: 8 | vlan_vid: 300 9 | actions: 10 | allow: 0 11 | # allow all traffic on the guest VLAN. 12 | - rule: 13 | vlan_vid: 200 14 | actions: 15 | allow: 1 16 | # Allow the control (RADIUS, hostapd UDP, SSH to AP) 17 | - rule: 18 | dl_type: 0x800 19 | actions: 20 | allow: 1 21 | - rule: 22 | dl_type: 0x806 23 | actions: 24 | allow: 1 25 | -------------------------------------------------------------------------------- /demo/radius/freeradius/eap.conf: -------------------------------------------------------------------------------- 1 | eap { 2 | default_eap_type = md5 3 | 4 | md5 { 5 | } 6 | 7 | tls { 8 | certdir = ${confdir}/certs 9 | cadir = ${confdir}/certs 10 | private_key_file = ${certdir}/server.key 11 | certificate_file = ${certdir}/server.pem 12 | ca_file = ${cadir}/ca.pem 13 | dh_file = ${certdir}/dh 14 | random_file = /dev/urandom 15 | } 16 | 17 | ttls { 18 | default_eap_type = md5 19 | copy_request_to_tunnel = no 20 | use_tunneled_reply = no 21 | virtual_server = "inner-tunnel" 22 | } 23 | 24 | peap { 25 | default_eap_type = md5 26 | copy_request_to_tunnel = no 27 | use_tunneled_reply = no 28 | virtual_server = "inner-tunnel" 29 | } 30 | } -------------------------------------------------------------------------------- /agent/syscmd/states.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | // GetAPStates get ap states on target 19 | func (r *CommandRunner) GetAPStates() (string, error) { 20 | //log.Info("fetch latest AP states.") 21 | wlanInfo, err := r.ExecCommand(true, "iw", "dev") 22 | if err != nil { 23 | return "", err 24 | } 25 | return wlanInfo, nil 26 | } 27 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Copyright 2018 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | set -e 18 | echo "" > coverage.txt 19 | 20 | for d in $(go list ./... | grep -v vendor); do 21 | go test -race -coverprofile=profile.out -covermode=atomic $d 22 | if [ -f profile.out ]; then 23 | cat profile.out >> coverage.txt 24 | rm profile.out 25 | fi 26 | done -------------------------------------------------------------------------------- /testkit/testdata/dot11r_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Enable 802.11r", 3 | "description":"Enable and configure 802.11r.", 4 | "test_cases":[ 5 | { 6 | "name":"Push 802.11r", 7 | "description":"Push only 802.11r related configuration to AP device.", 8 | "ops":[ 9 | { 10 | "type":"replace", 11 | "path":"/access-points/access-point[hostname=link022-pi-ap]", 12 | "val":"@../tests/ap_dot11r.json" 13 | } 14 | ] 15 | }, 16 | { 17 | "name":"Update 802.11r Method", 18 | "ops":[ 19 | { 20 | "type":"update", 21 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Auth-Link022]/dot11r/config/dot11r-method", 22 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Auth-Link022]/dot11r/state/dot11r-method", 23 | "val": "ODS" 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/ca.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQCkpQtPIoDSY1DuFzs8mcuTXlNpfxuRjkaH1zH/2pCPFNO8qI+o 3 | rV1CFumYhrQsdBaNxHeJaRVHwGiwCHWAU+U09iifgtBeMAQfn70RTzGc9ROUlWBa 4 | jpRJnBr83QDHIeZaKtorn2DHOt7evWwtBJkIn8iI3YEN27TlBAAZucVonQIDAQAB 5 | AoGAFUGNlTjiIsdZsnNjXW6JnswFvs//MiqJJo580QhZs28/6tJkQaWoLZkhrWRu 6 | rHKMWt8rg50Cd1H68gcVjGzIYXoAyw1Jo2GL4zr+iDAAvRqRw2FuODB6zGqoe7IV 7 | Msslr/IRJyi9WNgFYLwL60yXLhcw1hMoBtwHhmgKF1DW6KECQQDT1lAOl9cNK235 8 | wcbNP41DcFR4Vhuw8g5TcdW/+Z3mYbtFwB6iLjzvgNllPLD2FJuHSJt+WLtHoRjH 9 | AOrMCDqpAkEAxvgVeuq336tBZoK+Z2mxMZDqog8/19Mo0EX2HG0Rj7zN3EOORzg9 10 | tEJ+sLn1P6tn+JySc4Rcvi7F/QUDFtIK1QJAFkb3bFFzgCMNEbLbIUKJrInnmDAW 11 | AWmzL/tMqz+o501n0FH/5rZOGjHGqurTAU3xfSyZjZPvPf0nCeaCyqoi4QJABXPf 12 | AtNT3qlOzYRfu23GEVmrVG2ejkeXIr4IFJdYl9vQuGyRhZog9sOrvSQbeHc2DjYc 13 | PVYvRHb7rgdpIkL3UQJBAKknDfQ2zKI3j3DXlLvr0F3cTiQAGVIEs9fAVd8QJwJ2 14 | HECoOcDNZSMk0dVFkndeLOFB/PfMm3EtRZqaxx0lwy8= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXAIBAAKBgQClLW4x1Q0Z6bY/U/1TJ2IRZhpBzZnAujeG+DSDig6kufx3nxWw 3 | HMp3AXjqQxoI1S79r9usJLci+TbMGz8+Axvtg2KAnLPq1li8Naz8hNPBQ4zN7rWw 4 | dLIVZmHIGeYXJT5yJag8VWYz8Xgp48nJzehvNwxNxhGCYqJvLrQy4VZFEQIDAQAB 5 | AoGAJv4diFgCGr7oTfTW1X1zkiXnRudxR2TuXMdVJkQ+Brb9GYQNi6CQTx2i3LJw 6 | f+tLh4mvMukL7o6kV/SKrmnvenYA47yRRJSxhueF1LU9Xo3TQfiMNZ+YG+i3jVBH 7 | gngc8CSb/+8gh2j/JfHFTidYpept8NX23dZeC1THDhuASsECQQDbi8X6lZkZuXh5 8 | OJpc6155wqfayAJJqjJxFkVVGojBcCAsfLMHFJE8EGeDmb7OzsrEUjMave5vCKgb 9 | sUEpQF7ZAkEAwJqeJe61+s3mpDNy5n7vSU37lpCRKg8XvwvFW2SjMh/1lPVioh9K 10 | WuIs9PVaB/kB1rC3s+LhBiblbLC5QE+k+QJBANYLnPblWUH6UJmm9OB43UBK5snn 11 | zEszs0AMHYaOexxVDBkzbnwNsldkCvgEMkKA3LGxDoAZAtigEP4Gv+fPCWkCQGhO 12 | U/9u+5bME3AxfLRfYe9tECdGO5dvfQi1szf/lHH85N661aO/FmGJw5cVmKBgtiIy 13 | qgn9gKf4QJ9QRcMSXuECQBZJE7yEQO3+gxYHF0bQKtU2Wbl2z99Sndahmp+KF/yB 14 | qEyvOczPNksogOsK7mtaPf7cR0TsvlB/eFLa9I4cz0o= 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /demo/facuet_hw/cleanup.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 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 | # https://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 | #!/bin/bash 16 | 17 | sudo killall dnsmasq 18 | sudo killall freeradius 19 | sudo ip netns del gw 20 | for br in $(sudo ovs-vsctl list-br); do 21 | sudo ovs-vsctl del-br $br 22 | done 23 | 24 | IFS=$'\n'; 25 | for i in $(sudo ip netns list); do 26 | 27 | NS=$(echo $i | cut -d ' ' -f1) 28 | sudo ip netns delete $NS; 29 | done 30 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIChjCCAe+gAwIBAgIBAjANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJVUzEL 3 | MAkGA1UECAwCQ0ExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxETAPBgNVBAoMCExJ 4 | TkswMDIyMQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJBUDAeFw0xNzEwMTEwMTAz 5 | NTRaFw0xOTEwMTEwMTAzNTRaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEW 6 | MBQGA1UEBwwNTW91bnRhaW4gVmlldzERMA8GA1UECgwITElOSzAwMjIxDTALBgNV 7 | BAsMBFRlc3QxCzAJBgNVBAMMAkFQMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKB 8 | gQClLW4x1Q0Z6bY/U/1TJ2IRZhpBzZnAujeG+DSDig6kufx3nxWwHMp3AXjqQxoI 9 | 1S79r9usJLci+TbMGz8+Axvtg2KAnLPq1li8Naz8hNPBQ4zN7rWwdLIVZmHIGeYX 10 | JT5yJag8VWYz8Xgp48nJzehvNwxNxhGCYqJvLrQy4VZFEQIDAQABo04wTDARBglg 11 | hkgBhvhCAQEEBAMCBkAwCwYDVR0PBAQDAgXgMCoGA1UdJQQjMCEGCisGAQQBgjcK 12 | AwMGCWCGSAGG+EIEAQYIKwYBBQUHAwEwDQYJKoZIhvcNAQELBQADgYEAXiGbGp0W 13 | HE6Whyev8epCBlKmCxg/ddNCWgn1JzdRYRDTyiI1BcbG7xSSCAIIwX3KEtEtGKoL 14 | yOP22pDfzoDPqck6XYseCZLqllPunJqOW9SPthcqh7TSpH8GjCsGaY7Pq3qNM0Bh 15 | 5dopMHrhqQPz37MtUUNM+1C6YEumk+2ynGk= 16 | -----END CERTIFICATE----- 17 | -------------------------------------------------------------------------------- /testkit/testdata/apmanagers_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Assign AP Managers", 3 | "description":"Enable and configure AP Managers.", 4 | "test_cases":[ 5 | { 6 | "name":"Push AP Managers", 7 | "description":"Push assigned-ap-managers to AP device.", 8 | "ops":[ 9 | { 10 | "type":"replace", 11 | "path":"/access-points/access-point[hostname=link022-pi-ap]", 12 | "val":"@../tests/ap_apmanagers.json" 13 | } 14 | ] 15 | }, 16 | { 17 | "name":"Change secondary AP Manager IPv4 address", 18 | "ops":[ 19 | { 20 | "type":"update", 21 | "path":"/access-points/access-point[hostname=link022-pi-ap]/assigned-ap-managers/ap-manager[id=secondary]/config/ap-manager-ipv4-address", 22 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/assigned-ap-managers/ap-manager[id=secondary]/state/ap-manager-ipv4-address", 23 | "val": "192.168.3.100" 24 | } 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /demo/faucet_ovs/faucet.yaml: -------------------------------------------------------------------------------- 1 | vlans: 2 | vlan100: 3 | vid: 100 4 | vlan200: 5 | vid: 200 6 | vlan300: 7 | vid: 300 8 | dps: 9 | sw1: 10 | dp_id: 0x1 11 | hardware: "Open vSwitch" 12 | interfaces: 13 | 1: 14 | name: "gw" 15 | description: "gw for dhcp and dnsmasq" 16 | tagged_vlans: [vlan200,vlan300] 17 | native_vlan: vlan100 18 | 2: 19 | name: "host2" 20 | tagged_vlans: [vlan200,vlan300] 21 | native_vlan: vlan100 22 | 3: 23 | name: "host3" 24 | tagged_vlans: [vlan200,vlan300] 25 | native_vlan: vlan100 26 | 4: 27 | name: "host4" 28 | tagged_vlans: [vlan200,vlan300] 29 | native_vlan: vlan100 30 | 5: 31 | name: "host5" 32 | tagged_vlans: [vlan200,vlan300] 33 | native_vlan: vlan100 -------------------------------------------------------------------------------- /demo/faucet_ovs/gauge.yaml: -------------------------------------------------------------------------------- 1 | # Recommended configuration is Prometheus for all monitoring, with all_dps: True 2 | faucet_configs: 3 | - '/etc/faucet/faucet.yaml' 4 | watchers: 5 | port_status_poller: 6 | type: 'port_state' 7 | all_dps: True 8 | db: 'prometheus' 9 | port_stats_poller: 10 | type: 'port_stats' 11 | all_dps: True 12 | interval: 10 13 | db: 'prometheus' 14 | flow_table_poller: 15 | type: 'flow_table' 16 | all_dps: True 17 | interval: 60 18 | db: 'prometheus' 19 | dbs: 20 | prometheus: 21 | type: 'prometheus' 22 | prometheus_addr: '0.0.0.0' 23 | prometheus_port: 9303 24 | ft_file: 25 | type: 'text' 26 | compress: True 27 | file: 'flow_table.yaml.gz' 28 | influx: 29 | type: 'influx' 30 | influx_db: 'faucet' 31 | influx_host: 'influxdb' 32 | influx_port: 8086 33 | influx_user: 'faucet' 34 | influx_pwd: 'faucet' 35 | influx_timeout: 10 -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /demo/radius/freeradius/certs/ca.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC/DCCAmWgAwIBAgIJAOe9FfiPUDOVMA0GCSqGSIb3DQEBCwUAMGExCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJDQTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzERMA8G 4 | A1UECgwITElOSzAwMjIxDTALBgNVBAsMBFRlc3QxCzAJBgNVBAMMAkFQMB4XDTE3 5 | MTAxMTAxMDM1NFoXDTIwMTAxMDAxMDM1NFowYTELMAkGA1UEBhMCVVMxCzAJBgNV 6 | BAgMAkNBMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MREwDwYDVQQKDAhMSU5LMDAy 7 | MjENMAsGA1UECwwEVGVzdDELMAkGA1UEAwwCQVAwgZ8wDQYJKoZIhvcNAQEBBQAD 8 | gY0AMIGJAoGBAKSlC08igNJjUO4XOzyZy5NeU2l/G5GORofXMf/akI8U07yoj6it 9 | XUIW6ZiGtCx0Fo3Ed4lpFUfAaLAIdYBT5TT2KJ+C0F4wBB+fvRFPMZz1E5SVYFqO 10 | lEmcGvzdAMch5loq2iufYMc63t69bC0EmQifyIjdgQ3btOUEABm5xWidAgMBAAGj 11 | gbswgbgwDwYDVR0TBAgwBgEB/wIBADAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8v 12 | ZXNkbi5nb29nbGUuY29tMBEGCWCGSAGG+EIBAQQEAwIABzAlBglghkgBhvhCAQgE 13 | GBYWaHR0cDovL2VzZG4uZ29vZ2xlLmNvbTAlBglghkgBhvhCAQQEGBYWaHR0cDov 14 | L2VzZG4uZ29vZ2xlLmNvbTAbBglghkgBhvhCAQ0EDhYMU3Vubnl2YWxlIENBMA0G 15 | CSqGSIb3DQEBCwUAA4GBAHottq+TTJ689B/hXa+Q8FafLtbi0STCLRV/H9ZCMGjt 16 | p3PtSsb+FD74vqK2kqWAlME9jV1yfjGWfxrBsQXFbVGkg6zngrSKWPw+8GPwFH0m 17 | IkDOFukpAj7ERsWKPfHcXwC4q/PXBCnkX+Cr8RbZfdgqPZxnBGdb070TPcrXCQJ1 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /demo/facuet_hw/at.conf: -------------------------------------------------------------------------------- 1 | no service telnet 2 | no service http 3 | ssh server v2only 4 | ssh server allow-users manager 5 | service ssh ip 6 | clock timezone PST minus 8:00 7 | clock summer-time PDT recurring 2 Sun Mar 02:00 1 Sun Nov 02:00 60 8 | ! 9 | aaa authentication enable default local 10 | aaa authentication login default local 11 | ! 12 | no ip domain-lookup 13 | no service dhcp-server 14 | no ip multicast-routing 15 | no lldp run 16 | ! 17 | openflow controller tcp 192.168.12.1 6653 18 | openflow controller tcp 192.168.12.1 6654 19 | 20 | 21 | openflow native vlan 3021 22 | service power-inline 23 | no lacp global-passive-mode enable 24 | no spanning-tree rstp enable 25 | ! 26 | vlan database 27 | vlan 200,300,3020-3021 state enable 28 | ! 29 | interface port1.0.1-1.0.7 30 | openflow 31 | switchport 32 | switchport mode access 33 | ! 34 | interface port1.0.8 35 | switchport 36 | switchport mode access 37 | switchport access vlan 3020 38 | ! 39 | interface port1.0.9-1.0.10 40 | openflow 41 | switchport 42 | switchport mode access 43 | ! 44 | interface vlan3020 45 | ip address dhcp 46 | ! 47 | line con 0 48 | exec-timeout 0 0 49 | line vty 0 4 50 | exec-timeout 0 0 51 | ! 52 | end 53 | -------------------------------------------------------------------------------- /openconfig/models/gasket.yang: -------------------------------------------------------------------------------- 1 | module openconfig-gasket { 2 | 3 | yang-version "1"; 4 | 5 | // namespace 6 | namespace "http://google.com/yang/gasket"; 7 | 8 | // Assign this module a prefix to be used by other modules, when imported. 9 | prefix "gasket"; 10 | 11 | // meta 12 | organization "Google, Inc."; 13 | 14 | contact 15 | "Google, Inc. 16 | 1600 Amphitheatre Pkway 17 | Mountain View, CA 94043 18 | www.google.com"; 19 | 20 | description 21 | "This module defines the top level Gasket Configurations."; 22 | 23 | revision "2018-03-30" { 24 | description 25 | "Initial version"; 26 | reference "0.1.0"; 27 | } 28 | 29 | grouping gasket-top { 30 | description 31 | "Top-level grouping for Gasket configuration data."; 32 | 33 | container gasket { 34 | description 35 | "Top most container for configuration data for Gasket."; 36 | 37 | leaf ctrl-interface { 38 | type string; 39 | description 40 | "The interface for separate hostapd control prgram."; 41 | } 42 | 43 | leaf radius-attribute { 44 | type string; 45 | description 46 | "The gasket related radius attributes."; 47 | } 48 | } 49 | } 50 | 51 | uses gasket-top; 52 | } -------------------------------------------------------------------------------- /agent/context/context.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package context stores context data of the Link022 device. 17 | package context 18 | 19 | import ( 20 | "sync" 21 | ) 22 | 23 | // DeviceConfig contains the configuration specific to this device. 24 | type DeviceConfig struct { 25 | ETHINTFName string 26 | WLANINTFName string 27 | Hostname string 28 | ControllerAddr string 29 | GNMIServerAddr string 30 | } 31 | 32 | var ( 33 | deviceConfig *DeviceConfig 34 | once sync.Once 35 | ) 36 | 37 | // GetDeviceConfig returns the singleton DeviceConfig object. 38 | func GetDeviceConfig() *DeviceConfig { 39 | once.Do(func() { 40 | deviceConfig = &DeviceConfig{} 41 | }) 42 | return deviceConfig 43 | } 44 | -------------------------------------------------------------------------------- /demo/gasket/faucet/gasket/auth.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 0 3 | 4 | logger_location: /var/log/ryu/faucet/gasket/auth_app.log 5 | 6 | faucet: 7 | prometheus_port: 9302 8 | ip: 127.0.0.1 9 | 10 | files: 11 | # the location of files. pid should contain the process id (pid) of the main faucet-process (ryu-manager) 12 | # should be obtainable with "ps aux | grep faucet.faucet | head -n1 awk '{print $2}' > /etc/ryu/faucet/contr_pid" 13 | controller_pid: /var/run/faucet.pid 14 | faucet_config: /etc/ryu/faucet/faucet.yaml 15 | acl_config: /etc/ryu/faucet/faucet-acls.yaml 16 | base_config: /etc/ryu/faucet/gasket/base-acls.yaml 17 | 18 | # rules to be applied for a user once authenticated. 19 | auth-rules: 20 | file: /etc/ryu/faucet/gasket/rules.yaml 21 | dps: 22 | faucet-1: 23 | interfaces: 24 | 1: 25 | auth_mode: access 26 | 27 | hostapd: 28 | host: 192.168.11.6 29 | port: 8888 30 | # in hostapd.conf 'ctl_interface...:udp:8888' might be how you specifiy what to do 31 | # socket_path: /path/to/hostapd/ctrl/socket # may also be used instead of host & port, when using the unix socket interface 32 | 33 | request_timeout: 4 34 | unsolicited_timeout: 4 35 | request_socket_type: ping 36 | unsolicited_socket_type: ping 37 | 38 | -------------------------------------------------------------------------------- /demo/radius/freeradius/radiusd.conf: -------------------------------------------------------------------------------- 1 | # Minimal FreeRADIUS configuration. 2 | 3 | # directory where this file, at runtime, resides 4 | here = . 5 | 6 | 7 | prefix = /usr 8 | exec_prefix = /usr 9 | sysconfdir = /etc 10 | sbindir = ${exec_prefix}/sbin 11 | libdir = /usr/lib/freeradius 12 | 13 | name = freeradius 14 | 15 | raddbdir = ${here}/freeradius 16 | confdir = ${raddbdir} 17 | 18 | localstatedir = ${here}/var 19 | logdir = ${localstatedir} 20 | radacctdir = ${localstatedir} 21 | run_dir = ${localstatedir} 22 | db_dir = ${localstatedir} 23 | pidfile = ${run_dir}/${name}.pid 24 | 25 | 26 | listen { 27 | type = auth 28 | ipaddr = * 29 | port = 0 30 | } 31 | 32 | log { 33 | destination = files 34 | file = ${logdir}/radius.log 35 | } 36 | 37 | security { 38 | reject_delay = 0 39 | } 40 | 41 | client localhost { 42 | ipaddr = 127.0.0.1 43 | secret = "testing123" 44 | nastype = other 45 | } 46 | client aps { 47 | ipaddr = 192.168.0.0 48 | netmask = 16 49 | secret = "radiuspwd" 50 | nastype = other 51 | } 52 | 53 | modules { 54 | $INCLUDE eap.conf 55 | 56 | always reject { 57 | rcode = reject 58 | } 59 | files users { 60 | usersfile = ${confdir}/users 61 | } 62 | } 63 | 64 | instantiate { 65 | } 66 | 67 | # 68 | # Load virtual servers. 69 | # 70 | $INCLUDE sites-enabled/ 71 | -------------------------------------------------------------------------------- /agent/syscmd/device.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | import ( 19 | "errors" 20 | "net" 21 | "strings" 22 | 23 | log "github.com/golang/glog" 24 | ) 25 | 26 | // DeviceIPv4 fetches the first IPv4 address of the device. 27 | func (r *CommandRunner) DeviceIPv4() (string, error) { 28 | ipInfo, err := r.ExecCommand(true, "hostname", "-I") 29 | if err != nil { 30 | return "", err 31 | } 32 | 33 | // Find the first IPv4 address. 34 | ipList := strings.Fields(ipInfo) 35 | for _, ipString := range ipList { 36 | ipAddr := net.ParseIP(ipString) 37 | if ipAddr != nil && ipAddr.To4() != nil { 38 | log.Infof("The device has IPv4 address %s.", ipString) 39 | return ipString, nil 40 | } 41 | } 42 | return "", errors.New("no IPv4 address found on this device") 43 | } 44 | -------------------------------------------------------------------------------- /agent/syscmd/hostapd.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | import ( 19 | log "github.com/golang/glog" 20 | ) 21 | 22 | // StartHostapd starts a hostapd process link to the given WLAN interface. 23 | func (r *CommandRunner) StartHostapd(configFilePath string) error { 24 | log.Infof("Starting hostapd process with config file: %v...", configFilePath) 25 | if _, err := r.ExecCommand(false, "hostapd", configFilePath); err != nil { 26 | return err 27 | } 28 | log.Infof("Started a hostapd with config file: %v.", configFilePath) 29 | return nil 30 | } 31 | 32 | // StopAllHostapd kills all running hostapd processes. 33 | func (r *CommandRunner) StopAllHostapd() error { 34 | log.Info("Stopping all hostapd processes...") 35 | if _, err := r.ExecCommand(true, "killall", "-q", "hostapd"); err != nil { 36 | return err 37 | } 38 | log.Info("Stopped all hostapd processes.") 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /agent/monitoring/prometheus/exposition_server.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "flag" 21 | "net/http" 22 | 23 | log "github.com/golang/glog" 24 | "github.com/prometheus/client_golang/prometheus" 25 | "github.com/prometheus/client_golang/prometheus/promhttp" 26 | ) 27 | 28 | var ( 29 | targetAddr = flag.String("target_addr", "localhost:10161", "The target address in the format of host:port") 30 | targetName = flag.String("target_name", "hostname.com", "The target name is used to verify the hostname returned by TLS handshake") 31 | listenAddr = flag.String("listen_addr", "localhost:8080", "The address to listen on for HTTP requests.") 32 | ) 33 | 34 | func main() { 35 | flag.Parse() 36 | targetState := &TargetState{} 37 | ctx := context.Background() 38 | go monitoringAPStats(ctx, *targetAddr, *targetName, targetState) 39 | gnmiExport := newAPStateCollector(targetState) 40 | prometheus.MustRegister(gnmiExport) 41 | 42 | http.Handle("/metrics", promhttp.Handler()) 43 | log.Fatal(http.ListenAndServe(*listenAddr, nil)) 44 | } 45 | -------------------------------------------------------------------------------- /demo/faucet_ovs/README.md: -------------------------------------------------------------------------------- 1 | # Link022 Demo With Faucet controlled OVS switch 2 | This demo connects Link022 to an OpenVswitch controlled by the Faucet Openflow controller. 3 | The difference of this demo with the [Gasket](../README.gasket.md) demo is that we rely on Link022 to control the ACL instead 4 | of Faucet ACL through 802.1x. 5 | 6 | ## Hardware 7 | We use a computer with several network interfaces to run the Faucet controller, OVS and the services 8 | needed by Link022 (DHCP, Radius and NAT'ed routing). 9 | 10 | Here is the naming layout of the interfaces. 11 | ![alt text](./interface_layout.png "interface layout") 12 | 13 | 14 | ## OVS port configuration 15 | Here is the layout of the interfaces in the OVS bridge. 16 | ![alt text](./ovs.png "interface layout in OVS") 17 | 18 | ## Software setup 19 | 1. Install OVS. 20 | A good guide is [here](https://faucet.readthedocs.io/en/latest/tutorials/first_time.html#connect-your-first-datapath) 21 | 22 | 1. Create the OVS bridge and link022 services by running [setup.sh](./setup.sh) 23 | 24 | 1. Install docker and docker-compose. 25 | A good guide is [here](https://docs.docker.com/install/) 26 | 27 | 1. Install the configuration files. 28 | ``` 29 | sudo mkdir /etc/faucet 30 | sudo cp faucet.yaml /etc/faucet 31 | sudo cp guage.yaml /etc/faucet 32 | ``` 33 | 34 | 1. Bring up the faucet services. 35 | ``` 36 | git clone https://github.com/faucetsdn/faucet.git 37 | cd faucet 38 | sudo docker-compose pull 39 | sudo docker-compose up 40 | ``` 41 | 1. Follow the [directions](https://faucet.readthedocs.io/en/latest/docker.html#docker-compose) to configure prometheus and influxdb, and access the Grafana UI. 42 | -------------------------------------------------------------------------------- /demo/facuet_hw/README.md: -------------------------------------------------------------------------------- 1 | # Link022 Demo With Faucet controlled AT switch 2 | This demo connects Link022 to an Allied Telesis switch (AT-x230-10GP) or an OVS switch controlled by the Faucet Openflow controller. 3 | 4 | ## Hardware 5 | We use a computer with several network interfaces to run the Faucet controller, OVS and the services 6 | needed by Link022 (DHCP, Radius and NAT'ed routing). 7 | 8 | Here is the naming layout of the interfaces. 9 | ![alt text](./interface_layout.png "interface layout") 10 | 11 | 12 | ## OVS port configuration 13 | Here is the layout of the interfaces in the OVS bridge and the services container. 14 | ![alt text](./ovs_hw.png "interface layout in OVS") 15 | 16 | ## Software setup 17 | 1. Install OVS. 18 | A good guide is [here](https://faucet.readthedocs.io/en/latest/tutorials/first_time.html#connect-your-first-datapath) 19 | 20 | 1. Install docker and docker-compose. 21 | A good guide is [here](https://docs.docker.com/install/) 22 | 23 | 1. Create the OVS bridge and link022 services by running [setup.sh](./setup.sh) 24 | 25 | 1. Install the configuration files. 26 | ``` 27 | sudo mkdir /etc/faucet 28 | sudo cp faucet.yaml /etc/faucet 29 | sudo cp ../faucet_ovs/guage.yaml /etc/faucet 30 | ``` 31 | 1. Configure the hardware switch using the config at [here](at.conf) 32 | 33 | 1. Bring up the faucet services. 34 | ``` 35 | git clone https://github.com/faucetsdn/faucet.git 36 | cd faucet 37 | sudo docker-compose pull 38 | sudo docker-compose up 39 | ``` 40 | 1. Follow the [directions](https://faucet.readthedocs.io/en/latest/docker.html#docker-compose) to configure prometheus and influxdb, and access the Grafana UI. 41 | 42 | ## Troubleshoot 43 | * When rebooting the gateway machine, run docker-compose down/up to reset the network containers. 44 | 45 | -------------------------------------------------------------------------------- /agent/syscmd/bridge.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | import ( 19 | log "github.com/golang/glog" 20 | ) 21 | 22 | // CreateBridge creates a network bridge with a certain name. 23 | func (r *CommandRunner) CreateBridge(bridgeName string) error { 24 | log.Infof("Creating bridge %v...", bridgeName) 25 | if _, err := r.ExecCommand(true, "brctl", "addbr", bridgeName); err != nil { 26 | return err 27 | } 28 | log.Infof("Created bridge %v.", bridgeName) 29 | return nil 30 | } 31 | 32 | // DeleteBridge deletes a network bridge with a certain name. 33 | func (r *CommandRunner) DeleteBridge(bridgeName string) error { 34 | log.Infof("Deleting bridge %v...", bridgeName) 35 | if _, err := r.ExecCommand(true, "brctl", "delbr", bridgeName); err != nil { 36 | return err 37 | } 38 | log.Infof("Deleted bridge %v.", bridgeName) 39 | return nil 40 | } 41 | 42 | // AddBridgeIntf adds an interface to a network bridge. 43 | func (r *CommandRunner) AddBridgeIntf(bridgeName, intfName string) error { 44 | log.Infof("Adding interface %v to bridge %v...", intfName, bridgeName) 45 | if _, err := r.ExecCommand(true, "brctl", "addif", bridgeName, intfName); err != nil { 46 | return err 47 | } 48 | log.Infof("Added interface %v to bridge %v.", intfName, bridgeName) 49 | return nil 50 | } 51 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Link022 Integration tests 2 | This doc contains the steps to run the integration tests against the link022 agent. 3 | 4 | ## Setup testing environment 5 | The setup needs Python 2.7 environment and some additional packages. 6 | ``` 7 | apt-get install python python-netaddr mininet 8 | ``` 9 | 10 | ## Compiling Link022 binaries 11 | Follow the [instructions](../agent/README.md) to compile the Link022 binary and set up the certificates. 12 | 13 | ## Run the tests 14 | - Set up the required paths 15 | - GOPATH points to the Go directory for the compiled files of link022 and its dependencies. 16 | - LINK022 points to root directory of the LINK022 source code. 17 | - Run the tests with the Link022 emulator 18 | ``` 19 | sudo python integration.py \ 20 | --target_cmd "${GOPATH}/bin/agent -ca ${LINK022}/demo/cert/server/ca.crt \ 21 | -cert ${LINK022}/demo/cert/server/server.crt \ 22 | -key ${LINK022}/demo/cert/server/server.key -eth_intf_name target-eth1 \ 23 | -wlan_intf_name target-eth2" \ 24 | --gnmi_set ${GOPATH}/bin/gnmi_set \ 25 | --ca ${LINK022}/demo/cert/client/ca.crt \ 26 | --cert ${LINK022}/demo/cert/client/client.crt \ 27 | --key ${LINK022}/demo/cert/client/client.key \ 28 | --target_name www.example.com \ 29 | --target_addr 10.0.0.1:10162 \ 30 | --json_conf ${LINK022}/demo/ap_config.json 31 | 32 | ``` 33 | - Run the tests with an external target 34 | ``` 35 | sudo python integration.py \ 36 | --ext_target \ 37 | --gnmi_set ${GOPATH}/bin/gnmi_set \ 38 | --ca ${LINK022}/demo/cert/client/ca.crt \ 39 | --cert ${LINK022}/demo/cert/client/client.crt \ 40 | --key ${LINK022}/demo/cert/client/client.key \ 41 | --target_name www.example.com \ 42 | --target_addr 10.0.0.1:10162 \ 43 | --json_conf ${LINK022}/demo/ap_config.json 44 | 45 | ``` 46 | If your target device doesn't support multiple radios, Please replace /demo/ap_config.json with /tests/ap_config.json. 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 2 | [![GoDoc](https://godoc.org/github.com/google/link022?status.svg)](https://godoc.org/github.com/google/link022) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/google/link022)](https://goreportcard.com/report/github.com/google/link022) 4 | [![Build Status](https://travis-ci.org/google/link022.svg?branch=master)](https://travis-ci.org/google/link022) 5 | [![codecov](https://codecov.io/gh/google/link022/branch/master/graph/badge.svg)](https://codecov.io/gh/google/link022) 6 | 7 | # Link022: an open WiFi access point 8 | Link022 is an open reference implementation and experimental platform for an OpenConfig and gNMI 9 | controlled WiFi access point. 10 | 11 | The central part of Link022 is an gNMI agent that runs on a Linux host with WiFi capability. The 12 | agent turns the host into an gNMI capable wireless access point which can be configured using 13 | OpenConfig models. 14 | 15 | * See [gNMI Protocol documentation](https://github.com/openconfig/reference/tree/master/rpc/gnmi). 16 | * See [Openconfig documentation](http://www.openconfig.net/). 17 | 18 | ## Get Started 19 | This repository contains following components. 20 | 21 | ### Link022 agent 22 | A WiFi management component that runs on a Link022 AP, with OpenConfig and gNMI implemented. 23 | It supports gNMI "SET" and "GET" opertions for AP configuration. 24 | 25 | To run the agent on a Raspberry Pi device, see the [start guide](agent/README.md). 26 | 27 | ### Link022 demo 28 | A demo for configuring Link022 AP though gNMI. [demo guide](demo/README.md) 29 | 30 | ### Link022 emulator 31 | An emulator that runs Link022 agent inside a Linux namespace. [start guide](emulator/README.md) 32 | 33 | ### gNMI test kit. 34 | A tool to test the gNMI functionality of an AP device. [start guide](testkit/README.md) 35 | 36 | ## Disclaimer 37 | * This is not an official Google product. 38 | * See [how to contribute](CONTRIBUTING.md). 39 | -------------------------------------------------------------------------------- /openconfig/examples/openconfig.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // The openconfig program contains an example demonstrating how to use 17 | // the auto-generated wireless openconfig module. 18 | package main 19 | 20 | import ( 21 | "flag" 22 | 23 | log "github.com/golang/glog" 24 | "github.com/google/link022/agent/util/mock" 25 | "github.com/google/link022/generated/ocstruct" 26 | "github.com/openconfig/ygot/ygot" 27 | ) 28 | 29 | func main() { 30 | flag.Parse() 31 | apConfig := mock.GenerateConfig(true) 32 | 33 | jsonString, err := ygot.EmitJSON(apConfig, &ygot.EmitJSONConfig{ 34 | Format: ygot.RFC7951, 35 | Indent: " ", 36 | RFC7951Config: &ygot.RFC7951JSONConfig{ 37 | AppendModuleName: false, 38 | }, 39 | }) 40 | if err != nil { 41 | log.Exitf("Error outputting the configuration to JSON: %v", err) 42 | } 43 | log.Infof("Original AP configJSON output:\n%v\n", jsonString) 44 | 45 | loadedAP := &ocstruct.Device{} 46 | err = ocstruct.Unmarshal([]byte(jsonString), loadedAP) 47 | if err != nil { 48 | log.Exitf("Error unmarshal JSON: %v", err) 49 | } 50 | 51 | loadedJSONString, err := ygot.EmitJSON(loadedAP, &ygot.EmitJSONConfig{ 52 | Format: ygot.RFC7951, 53 | Indent: " ", 54 | RFC7951Config: &ygot.RFC7951JSONConfig{ 55 | AppendModuleName: false, 56 | }, 57 | }) 58 | if err != nil { 59 | log.Exitf("Error outputting the loaded configuration to JSON: %v", err) 60 | } 61 | log.Infof("Loaded config JSON output:\n%v\n", loadedJSONString) 62 | } 63 | -------------------------------------------------------------------------------- /demo/cert/client/client.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFiDCCA3CgAwIBAgIJAKkfmX89R4llMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMQ0wCwYDVQQK 4 | DARHTk1JMQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJBUDAeFw0xODA1MzExODA3 5 | MzZaFw0yODA1MjgxODA3MzZaMFkxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTES 6 | MBAGA1UEBwwJU3Vubnl2YWxlMQ0wCwYDVQQKDARHTk1JMQ0wCwYDVQQLDARUZXN0 7 | MQswCQYDVQQDDAJBUDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMwV 8 | Hpr2wZ+r/C4y0fmNb6TSCgM4wlanbcHAmeRZA09N0MaGpNgOto64bFtLfFPQmcnk 9 | op2jQWGB7avy1saEjfRnQZzI3YuMoLxph2zud8vQkVaSQ/Aett1llHlTNf/1FYdT 10 | yGrdmo7sy3KuO47GoDZ/UTRlwUhajKro7nQ4cri0EzmaaZB93WSqzCvDTFTHOSPa 11 | bXoVOWPKke1NloOvBHtPf7K9V0LH5i51FzfWZsL0m7Fna1DwmGEZPz7KKlP39GdM 12 | uomv92IpC+hz6DUtLz3saRh/4QvKmuuigTSp8wZostr5wsgO0nggDK3NJHVdLDBG 13 | QFOcoy3OzRt3o9eoR36k/eKaAzka1j5lxRuEQLNyAiTMqhuV23WMophBPWmmR1SL 14 | xzD7j5L4i3vEGFwkjivPTx30aqFqn+uvXPrvP8z9RHQln5mHFrDgs2JTZz09iOdx 15 | s+rmPDEIRUQrGNiKcYkr94S5mJZDiPffwbYupnEtLG/JJxeM06HH/L9rc57LXL/B 16 | HMy4SLKGRw20CckaF4WZnr1vJ2TSYzWLi4MM377eO2uqPu4NojkpK60wjYItdN3x 17 | CUuOTeqhNjQacbJNUReDUtI953p+rm4W49HzWPLw5eIPntV9YU7rM9Lyc1PiaSfY 18 | GRqv/CDL/HSHKP8Vaz3/EyU13C7QNF40bA4hOICbAgMBAAGjUzBRMB0GA1UdDgQW 19 | BBRjM0wnJXqS+MxBGKrHKxjEk8Sj9DAfBgNVHSMEGDAWgBRjM0wnJXqS+MxBGKrH 20 | KxjEk8Sj9DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCGODfp 21 | QFAByk9oD/r8TLmCzXE4yiwZgglio0kaXI4RFvZLDbJDkTCQotsieRr+zlSJBRIW 22 | Ac6cU3XEkaDAI0jJ+6WH/NKFSAHuTkKp063dnfqZkl0GPkxq4sGJ37XzHa78Q/F0 23 | OxYUTd9yKXdexbVJ5+4Y/p3EMSzWbuxnCB6V38czR6T/ny7nZ0K8+J0oafb9J5f9 24 | 1wN18k8wk7AA+BknGsm9b+btJBNPVYRfLlGIS8NFuCgE4lSoOKB9tJ9bK8xcB0QA 25 | WRsaaKRtMCx7/m60inkvmJqCAp6F+5r940FiPi1L7hdp6mBLk11dyzPQ2Gar1z2U 26 | iYLOm7y9Tk3kwsrDWwCsq3/V8y638E04JMaXB8G/OfAAdvBYfjqeITQyZbsXpDWt 27 | h70W1sAJeOgi2YeLh4i4x9f7zFeDDbOz/tM8E+BgVHoIFMlelR/psTSYo5BCXluE 28 | Hpbs7wmUCw/WRFTSN2W5U2M8i9QLk3Eg2uYzxPoedAhNOP/drfpl39U8oOlaTMR7 29 | uMN0a2JHw2gMmk8rX6O5coJMUHBZcDnRCunMiYBGMxeK2ygOQIV00OnR9PMTWIaO 30 | OrxJnK6bvoYvh9jkuxN1y3B6UpEKT4UMcHfIXiQRZogHdL4eKEn2JMl8Wl33dJZn 31 | EqLzuPqDc+I3w6yg0qC+5AIaHScfq1GozZuaow== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /demo/cert/server/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIFiDCCA3CgAwIBAgIJAKkfmX89R4llMA0GCSqGSIb3DQEBCwUAMFkxCzAJBgNV 3 | BAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU3Vubnl2YWxlMQ0wCwYDVQQK 4 | DARHTk1JMQ0wCwYDVQQLDARUZXN0MQswCQYDVQQDDAJBUDAeFw0xODA1MzExODA3 5 | MzZaFw0yODA1MjgxODA3MzZaMFkxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTES 6 | MBAGA1UEBwwJU3Vubnl2YWxlMQ0wCwYDVQQKDARHTk1JMQ0wCwYDVQQLDARUZXN0 7 | MQswCQYDVQQDDAJBUDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMwV 8 | Hpr2wZ+r/C4y0fmNb6TSCgM4wlanbcHAmeRZA09N0MaGpNgOto64bFtLfFPQmcnk 9 | op2jQWGB7avy1saEjfRnQZzI3YuMoLxph2zud8vQkVaSQ/Aett1llHlTNf/1FYdT 10 | yGrdmo7sy3KuO47GoDZ/UTRlwUhajKro7nQ4cri0EzmaaZB93WSqzCvDTFTHOSPa 11 | bXoVOWPKke1NloOvBHtPf7K9V0LH5i51FzfWZsL0m7Fna1DwmGEZPz7KKlP39GdM 12 | uomv92IpC+hz6DUtLz3saRh/4QvKmuuigTSp8wZostr5wsgO0nggDK3NJHVdLDBG 13 | QFOcoy3OzRt3o9eoR36k/eKaAzka1j5lxRuEQLNyAiTMqhuV23WMophBPWmmR1SL 14 | xzD7j5L4i3vEGFwkjivPTx30aqFqn+uvXPrvP8z9RHQln5mHFrDgs2JTZz09iOdx 15 | s+rmPDEIRUQrGNiKcYkr94S5mJZDiPffwbYupnEtLG/JJxeM06HH/L9rc57LXL/B 16 | HMy4SLKGRw20CckaF4WZnr1vJ2TSYzWLi4MM377eO2uqPu4NojkpK60wjYItdN3x 17 | CUuOTeqhNjQacbJNUReDUtI953p+rm4W49HzWPLw5eIPntV9YU7rM9Lyc1PiaSfY 18 | GRqv/CDL/HSHKP8Vaz3/EyU13C7QNF40bA4hOICbAgMBAAGjUzBRMB0GA1UdDgQW 19 | BBRjM0wnJXqS+MxBGKrHKxjEk8Sj9DAfBgNVHSMEGDAWgBRjM0wnJXqS+MxBGKrH 20 | KxjEk8Sj9DAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQCGODfp 21 | QFAByk9oD/r8TLmCzXE4yiwZgglio0kaXI4RFvZLDbJDkTCQotsieRr+zlSJBRIW 22 | Ac6cU3XEkaDAI0jJ+6WH/NKFSAHuTkKp063dnfqZkl0GPkxq4sGJ37XzHa78Q/F0 23 | OxYUTd9yKXdexbVJ5+4Y/p3EMSzWbuxnCB6V38czR6T/ny7nZ0K8+J0oafb9J5f9 24 | 1wN18k8wk7AA+BknGsm9b+btJBNPVYRfLlGIS8NFuCgE4lSoOKB9tJ9bK8xcB0QA 25 | WRsaaKRtMCx7/m60inkvmJqCAp6F+5r940FiPi1L7hdp6mBLk11dyzPQ2Gar1z2U 26 | iYLOm7y9Tk3kwsrDWwCsq3/V8y638E04JMaXB8G/OfAAdvBYfjqeITQyZbsXpDWt 27 | h70W1sAJeOgi2YeLh4i4x9f7zFeDDbOz/tM8E+BgVHoIFMlelR/psTSYo5BCXluE 28 | Hpbs7wmUCw/WRFTSN2W5U2M8i9QLk3Eg2uYzxPoedAhNOP/drfpl39U8oOlaTMR7 29 | uMN0a2JHw2gMmk8rX6O5coJMUHBZcDnRCunMiYBGMxeK2ygOQIV00OnR9PMTWIaO 30 | OrxJnK6bvoYvh9jkuxN1y3B6UpEKT4UMcHfIXiQRZogHdL4eKEn2JMl8Wl33dJZn 31 | EqLzuPqDc+I3w6yg0qC+5AIaHScfq1GozZuaow== 32 | -----END CERTIFICATE----- 33 | -------------------------------------------------------------------------------- /demo/cert/client/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | U32jzw== 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /demo/cert/server/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | 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 32 | U32jzw== 33 | -----END CERTIFICATE----- 34 | -------------------------------------------------------------------------------- /openconfig/scripts/generate_wifi_oc_schema.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | #!/bin/bash 16 | # 17 | # Generates the latest version of the OpenConfig textproto schema descriptor and 18 | # model visualization from the source YANG modules. 19 | # 20 | # To use this, run thr scripts directory: 21 | # ./generate_wifi_oc_schema.sh 22 | 23 | export GOPATH=$HOME/go 24 | 25 | # Tools 26 | YANG_CONVERTER=$GOPATH/src/github.com/openconfig/ygot/generator/generator.go 27 | 28 | # Download OpenConfig models from https://github.com/openconfig/public 29 | # Download ietf models from https://github.com/openconfig/yang/tree/master/standard/ietf/RFC 30 | # Move downloaded models to a specific folder. 31 | OC_FOLDER= 32 | 33 | # OpenConfig modules 34 | YANG_MODELS=$OC_FOLDER/public/release/models 35 | IETF_MODELS=$OC_FOLDER/yang/standard/ietf/RFC 36 | AP_TOP_MODULE=$OC_FOLDER/public/release/models/wifi/access-points/openconfig-access-points.yang 37 | IGNORED_MODULES=openconfig-wifi-phy,openconfig-wifi-mac,openconfig-system,openconfig-extensions,openconfig-inet-types,openconfig-platform 38 | GASKET_MODULES=../models/gasket.yang 39 | 40 | # Output path 41 | OUTPUT_PACKAGE_NAME=ocstruct 42 | OUTPUT_FILE_PATH=../../generated/$OUTPUT_PACKAGE_NAME/$OUTPUT_PACKAGE_NAME.go 43 | 44 | go run $YANG_CONVERTER \ 45 | -path=$YANG_MODELS,$IETF_MODELS,$GASKET_MODULES \ 46 | -generate_fakeroot -fakeroot_name=device \ 47 | -package_name=ocstruct -compress_paths=false \ 48 | -exclude_modules=$IGNORED_MODULES \ 49 | -output_file=$OUTPUT_FILE_PATH \ 50 | $AP_TOP_MODULE $GASKET_MODULES 51 | -------------------------------------------------------------------------------- /demo/facuet_hw/faucet.yaml: -------------------------------------------------------------------------------- 1 | vlans: 2 | vlan100: 3 | vid: 100 4 | vlan200: 5 | vid: 200 6 | vlan300: 7 | vid: 300 8 | dps: 9 | sw1: 10 | dp_id: 0x1 11 | hardware: "Open vSwitch" 12 | interfaces: 13 | 1: 14 | name: "gw" 15 | description: "gw for dhcp and dnsmasq" 16 | tagged_vlans: [vlan200,vlan300] 17 | native_vlan: vlan100 18 | 2: 19 | name: "host2" 20 | tagged_vlans: [vlan200,vlan300] 21 | native_vlan: vlan100 22 | 3: 23 | name: "host3" 24 | tagged_vlans: [vlan200,vlan300] 25 | native_vlan: vlan100 26 | 4: 27 | name: "host4" 28 | tagged_vlans: [vlan200,vlan300] 29 | native_vlan: vlan100 30 | 5: 31 | name: "host5" 32 | tagged_vlans: [vlan200,vlan300] 33 | native_vlan: vlan100 34 | sw2: 35 | dp_id: 0x0000001aeb965d18 36 | hardware: "Allied-Telesis" 37 | interfaces: 38 | 1: 39 | name: "gw" 40 | description: "gw for dhcp and dnsmasq" 41 | tagged_vlans: [vlan200,vlan300] 42 | native_vlan: vlan100 43 | 2: 44 | name: "host2" 45 | tagged_vlans: [vlan200,vlan300] 46 | native_vlan: vlan100 47 | 3: 48 | name: "host3" 49 | tagged_vlans: [vlan200,vlan300] 50 | native_vlan: vlan100 51 | 4: 52 | name: "host4" 53 | tagged_vlans: [vlan200,vlan300] 54 | native_vlan: vlan100 55 | 5: 56 | name: "host5" 57 | tagged_vlans: [vlan200,vlan300] 58 | native_vlan: vlan100 59 | 6: 60 | name: "host6" 61 | tagged_vlans: [vlan200,vlan300] 62 | native_vlan: vlan100 63 | 7: 64 | name: "host7" 65 | tagged_vlans: [vlan200,vlan300] 66 | native_vlan: vlan100 67 | -------------------------------------------------------------------------------- /agent/README.md: -------------------------------------------------------------------------------- 1 | # Agent 2 | 3 | This directory contains the WiFi management component that runs on the 4 | Link022 Pi AP. 5 | 6 | ## Get Started 7 | For documentation on installing a full demo system (with both a gNMI client and link022 access point) 8 | please see the [full demo documentation](../demo/README.md) which explains a fully complete system. 9 | 10 | The following instructions will get you a Link022 AP on a Linux-based device. 11 | 12 | ### Prerequisites 13 | Have a device set up. (Tested with Raspian Stretch) 14 | 15 | Install Golang. 16 | ``` 17 | If running on Raspberry Pi: 18 | wget https://storage.googleapis.com/golang/go1.7.linux-armv6l.tar.gz 19 | sudo tar -C /usr/local -xzf go1.7.linux-armv6l.tar.gz 20 | 21 | For other systems: 22 | Install golang 1.7+ (get it from: https://golang.org/doc/install#install) 23 | ``` 24 | 25 | Set up Path: 26 | ``` 27 | export PATH=$PATH:/usr/local/go/bin 28 | ``` 29 | 30 | Install dependencies. 31 | ``` 32 | sudo apt-get install udhcpc bridge-utils hostapd git 33 | ``` 34 | 35 | ### Download Link022 agent 36 | ``` 37 | export GOPATH=$HOME/go 38 | go get github.com/google/link022/agent 39 | ``` 40 | 41 | ### Download certificates 42 | Download sample certificates from [the demo directory](../demo/cert/server/). 43 | Or you can use your own cert. Sample commands to generate certificates can be found [here](../demo/cert/generate_cert.sh). 44 | 45 | ### Configuring network interfaces of device 46 | Editing the file /etc/network/interfaces on device. 47 | ``` 48 | auto lo 49 | iface lo inet loopback 50 | 51 | auto eth0 52 | iface eth0 inet dhcp 53 | 54 | # Disable all WLAN interfaces. 55 | auto wlan0 56 | iface wlan0 inet static 57 | address 0.0.0.0 58 | 59 | # Repeat for other WLAN interfaces. 60 | ``` 61 | Note: Reboot the device to make change take effect. 62 | 63 | ### Running Link022 agent 64 | ``` 65 | export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin 66 | sudo env PATH=$PATH agent -ca= -cert= -key= -eth_intf_name= -wlan_intf_name= -gnmi_port= 67 | ``` 68 | 69 | The default log file is "/tmp/agent.INFO". It can be modified by "-log_dir" option. 70 | 71 | Note: Make sure the chosen wireless device supports AP mode and has enough 72 | capability. -------------------------------------------------------------------------------- /agent/service/base.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package service contains methods that manage Link022 AP device. 17 | package service 18 | 19 | import ( 20 | log "github.com/golang/glog" 21 | "github.com/google/link022/agent/syscmd" 22 | "github.com/google/link022/agent/util/ocutil" 23 | "github.com/google/link022/generated/ocstruct" 24 | ) 25 | 26 | var ( 27 | cmdRunner = syscmd.Runner() 28 | runFolder = "/var/run/link022" 29 | ) 30 | 31 | // ApplyConfig configures this device to a Link022 AP based on the given configuration. 32 | func ApplyConfig(officeAP *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint, gasketConfig *ocstruct.OpenconfigGasket_Gasket, setupIntf bool, ethIntfName, wlanINTFName string) error { 33 | log.Infof("Configuring AP %s...", *officeAP.Hostname) 34 | 35 | if setupIntf { 36 | // Configure eth interface. 37 | if err := configEthIntf(ethIntfName, ocutil.VLANIDs(officeAP)); err != nil { 38 | return err 39 | } 40 | 41 | //Configure WLAN interface. 42 | if err := configWLANIntf(wlanINTFName); err != nil { 43 | return err 44 | } 45 | } 46 | 47 | // Configure hostapd. 48 | return configHostapd(officeAP, gasketConfig, wlanINTFName) 49 | } 50 | 51 | // CleanupConfig cleans up the current AP configuration on this device. 52 | // It goes through all cleanup steps even if some failures are detected, and returns all errors. 53 | func CleanupConfig(ethIntfName string, vlanIDs []int) []error { 54 | var errs []error 55 | 56 | // Stop hostapd processes. 57 | if err := cmdRunner.StopAllHostapd(); err != nil { 58 | errs = append(errs, err) 59 | } 60 | 61 | // Clean up eth interfaces. 62 | if len(vlanIDs) > 0 { 63 | errs = append(errs, cleanupEthIntf(ethIntfName, vlanIDs)...) 64 | } 65 | 66 | log.Infof("Cleaned up AP. Number of errors = %d.", len(errs)) 67 | return errs 68 | } 69 | -------------------------------------------------------------------------------- /tests/ap_dot11r.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:hostname":"link022-pi-ap", 3 | "openconfig-access-points:system":{ 4 | "aaa":{ 5 | "server-groups":{ 6 | "server-group":[ 7 | { 8 | "servers":{ 9 | "server":[ 10 | { 11 | "address":"192.168.11.1", 12 | "config":{ 13 | "address":"192.168.11.1", 14 | "timeout":5, 15 | "name":"radius-server" 16 | }, 17 | "radius":{ 18 | "config":{ 19 | "auth-port":1812, 20 | "secret-key":"radiuspwd" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | "name":"freeradius", 27 | "config":{ 28 | "name":"freeradius", 29 | "type":"openconfig-aaa-radius:RADIUS" 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | "openconfig-access-points:radios":{ 37 | "radio":[ 38 | { 39 | "config":{ 40 | "channel":157, 41 | "channel-width":40, 42 | "dca":false, 43 | "enabled":true, 44 | "id":0, 45 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 46 | "scanning":false, 47 | "scanning-defer-traffic":true, 48 | "transmit-power":9 49 | }, 50 | "id":0 51 | }, 52 | { 53 | "config":{ 54 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 55 | "channel-width":20, 56 | "transmit-power":3, 57 | "id":1, 58 | "channel":6 59 | }, 60 | "id":1 61 | } 62 | ] 63 | }, 64 | "openconfig-access-points:ssids":{ 65 | "ssid":[ 66 | { 67 | "dot11r":{ 68 | "config":{ 69 | "dot11r-domainid":5, 70 | "dot11r-method":"OVA", 71 | "dot11r":true 72 | } 73 | }, 74 | "config":{ 75 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 76 | "name":"Auth-Link022", 77 | "default-vlan":300, 78 | "enabled":true, 79 | "server-group":"freeradius", 80 | "opmode":"WPA2_ENTERPRISE" 81 | }, 82 | "name":"Auth-Link022" 83 | } 84 | ] 85 | } 86 | } -------------------------------------------------------------------------------- /tests/ap_apmanagers.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:hostname":"link022-pi-ap", 3 | "openconfig-access-points:system":{ 4 | "aaa":{ 5 | "server-groups":{ 6 | "server-group":[ 7 | { 8 | "servers":{ 9 | "server":[ 10 | { 11 | "address":"192.168.11.1", 12 | "config":{ 13 | "address":"192.168.11.1", 14 | "timeout":5, 15 | "name":"radius-server" 16 | }, 17 | "radius":{ 18 | "config":{ 19 | "auth-port":1812, 20 | "secret-key":"radiuspwd" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | "name":"freeradius", 27 | "config":{ 28 | "name":"freeradius", 29 | "type":"openconfig-aaa-radius:RADIUS" 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | "openconfig-access-points:radios":{ 37 | "radio":[ 38 | { 39 | "config":{ 40 | "channel":157, 41 | "channel-width":40, 42 | "dca":false, 43 | "enabled":true, 44 | "id":0, 45 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 46 | "scanning":false, 47 | "scanning-defer-traffic":true, 48 | "transmit-power":9 49 | }, 50 | "id":0 51 | }, 52 | { 53 | "config":{ 54 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 55 | "channel-width":20, 56 | "transmit-power":3, 57 | "id":1, 58 | "channel":6 59 | }, 60 | "id":1 61 | } 62 | ] 63 | }, 64 | "openconfig-access-points:ssids":{ 65 | "ssid":[ 66 | { 67 | "config":{ 68 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 69 | "name":"Auth-Link022", 70 | "default-vlan":300, 71 | "enabled":true, 72 | "server-group":"freeradius", 73 | "opmode":"WPA2_ENTERPRISE" 74 | }, 75 | "name":"Auth-Link022" 76 | } 77 | ] 78 | }, 79 | "openconfig-access-points:assigned-ap-managers":{ 80 | "ap-manager":[ 81 | { 82 | "config":{ 83 | "ap-manager-ipv4-address":"192.168.1.2", 84 | "id":"primary", 85 | "fqdn":"primary-manager.example.com" 86 | }, 87 | "id":"primary" 88 | }, 89 | { 90 | "config":{ 91 | "ap-manager-ipv4-address":"192.168.2.2", 92 | "id":"secondary", 93 | "fqdn":"secondary-manager.example.com" 94 | }, 95 | "id":"secondary" 96 | } 97 | ] 98 | } 99 | } -------------------------------------------------------------------------------- /agent/util/mock/openconfig_test.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package mock 17 | 18 | import ( 19 | "testing" 20 | 21 | "github.com/google/link022/generated/ocstruct" 22 | "github.com/openconfig/ygot/ygot" 23 | ) 24 | 25 | // TestYGOT verifies the current version of ygot library works with Link022 agent. 26 | func TestYGOT(t *testing.T) { 27 | verifyOfficeConfig(t, GenerateConfig(true)) 28 | verifyOfficeConfig(t, GenerateConfig(false)) 29 | verifyOfficeConfig(t, GenerateConfig(true)) 30 | verifyOfficeConfig(t, GenerateConfig(false)) 31 | verifyOfficeConfig(t, GenerateConfig(true)) 32 | verifyOfficeConfig(t, GenerateConfig(false)) 33 | } 34 | 35 | func verifyOfficeConfig(t *testing.T, apConfig *ocstruct.Device) { 36 | // Test validation. 37 | if err := apConfig.Validate(); err != nil { 38 | t.Errorf("AP configuration is not valid. Error: %v.", err) 39 | } 40 | 41 | // Test marshalling. 42 | jsonString, err := ygot.EmitJSON(apConfig, &ygot.EmitJSONConfig{ 43 | Format: ygot.RFC7951, 44 | Indent: " ", 45 | RFC7951Config: &ygot.RFC7951JSONConfig{ 46 | AppendModuleName: true, 47 | }, 48 | }) 49 | if err != nil { 50 | t.Errorf("Marshalling the AP configuration to JSON failed. Error: %v.", err) 51 | } 52 | 53 | // Test unmarshalling. 54 | loadedConfig := &ocstruct.Device{} 55 | if err = ocstruct.Unmarshal([]byte(jsonString), loadedConfig); err != nil { 56 | t.Errorf("Unmarshalling AP configuration JSON string to OpenConfig structs failed. Error: %v.", err) 57 | } 58 | 59 | // Test validation. 60 | if err := loadedConfig.Validate(); err != nil { 61 | t.Errorf("The loaded AP configuration is not valid. Error: %v.", err) 62 | } 63 | 64 | // Test marshalling again. 65 | loadedJSONString, err := ygot.EmitJSON(loadedConfig, &ygot.EmitJSONConfig{ 66 | Format: ygot.RFC7951, 67 | Indent: " ", 68 | RFC7951Config: &ygot.RFC7951JSONConfig{ 69 | AppendModuleName: true, 70 | }, 71 | }) 72 | if err != nil { 73 | t.Errorf("Marshalling the loaded AP configuration to JSON failed. Error: %v.", err) 74 | } 75 | 76 | if jsonString != loadedJSONString { 77 | t.Errorf("The loaded AP configuration does not match the original one, got: %s, expected: %s.", jsonString, loadedJSONString) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /agent/monitoring/prometheus/README.md: -------------------------------------------------------------------------------- 1 | # OpenConfig Telemetry Exporter (Prometheus) 2 | 3 | This directory contains an exporter that periodically collects AP's info via gNMI and exposes converted metrics to a web page. Prometheus can monitor AP's status by scraping that web page. 4 | 5 | ## Get Started 6 | 7 | Follow steps below to set up environment and start exposition server 8 | 9 | ### Prerequisites 10 | 11 | Download entire Link022 repository. 12 | Install golang 1.10+ (get it from: https://golang.org/doc/install#install) 13 | Install dependencies: 14 | Change directory in terminal to this folder, then run below command. 15 | 16 | ``` 17 | go get -t ./... 18 | ``` 19 | 20 | ### Compile Exporter 21 | 22 | ``` 23 | go build exposition_server.go openconfig_ap_exporter.go 24 | ``` 25 | 26 | This command will generate binary file named exposition_server. 27 | 28 | ### Start Exporter 29 | Run the exporter binary. It takes three categories of input parameters: 30 | 31 | 1. gNMI client certs config: 32 | * ca: CA certificate file 33 | * cert: Certificate file 34 | * key: Private key file 35 | 2. gNMI target config: 36 | * target_addr: the target address in the format of host:port 37 | * target_name: the target name for verifing the hostname returned by TLS handshake 38 | 3. Exposition server config 39 | * listen_addr: the address for server to listen HTTP requests. The HTTP resource for scraping is /metrics. 40 | 41 | Here is one example: 42 | 43 | ``` 44 | ./exposition_server \ 45 | -ca ../../../demo/cert/client/ca.crt \ 46 | -cert ../../../demo/cert/client/client.crt \ 47 | -key ../../../demo/cert/client/client.key \ 48 | -target_name www.example.com \ 49 | -target_addr 10.0.0.1:8080 \ 50 | -listen_addr 127.0.0.1:8080 51 | ``` 52 | 53 | Note: The default location of exporter log file is "/tmp/exposition_server.INFO" 54 | 55 | ## Monitoring In Prometheus 56 | 57 | Follow steps below to set up Prometheus and monitoring AP status. 58 | 59 | ### Prometheus Getting Started 60 | 61 | Download [Prometheus](https://prometheus.io/download/). Follow official Prometheus [tutorial](https://prometheus.io/docs/prometheus/latest/getting_started/) to learn how to configure and start it. 62 | 63 | ### Configuring Prometheus to monitor AP 64 | 65 | If exposition server is listening on 127.0.0.1:8080 66 | Save the following basic Prometheus configuration as a file named prometheus.yml 67 | 68 | ``` 69 | global: 70 | scrape_interval: 15s # Exposition server sends gNMI request every 15 seconds. 71 | 72 | scrape_configs: 73 | # The job name is added as a label `job=` to any timeseries scraped from this config. 74 | - job_name: 'link022-pi-ap' 75 | static_configs: 76 | - targets: ['127.0.0.1:8080'] 77 | ``` 78 | 79 | ### Start Prometheus 80 | 81 | Start Prometheus according to its official tutorial. 82 | By default, Prometheus admin page is localhost:9090. 83 | You can see all exported AP status metrics in that page. -------------------------------------------------------------------------------- /agent/controller/controller.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package controller contains methods that are related to AP controller. 17 | package controller 18 | 19 | import ( 20 | "errors" 21 | "time" 22 | 23 | devctx "github.com/google/link022/agent/context" 24 | "golang.org/x/net/context" 25 | "google.golang.org/grpc" 26 | "google.golang.org/grpc/metadata" 27 | 28 | log "github.com/golang/glog" 29 | pb "github.com/openconfig/gnmi/proto/gnmi" 30 | ) 31 | 32 | const ( 33 | hostnameHeader = "hostname" 34 | gnmiAddrHeader = "gnmi_address" 35 | resyncRequiredHeader = "resync_required" 36 | 37 | heartbeatInterval = 15 * time.Second 38 | ) 39 | 40 | // ReportAPInfo reports the current AP information to the assigned controller. 41 | // It returns error if failures detected. 42 | func ReportAPInfo(syncRequried bool) error { 43 | deviceConfig := devctx.GetDeviceConfig() 44 | if deviceConfig.ControllerAddr == "" { 45 | return errors.New("no controller assigned, unable to report AP information") 46 | } 47 | 48 | // Creating a gNMI client. 49 | conn, err := grpc.Dial(deviceConfig.ControllerAddr, grpc.WithInsecure()) 50 | if err != nil { 51 | return err 52 | } 53 | defer conn.Close() 54 | 55 | getRequest := &pb.GetRequest{ 56 | Path: []*pb.Path{}, 57 | } 58 | 59 | // Add AP's gNMI information to the request header. 60 | headerMap := map[string]string{ 61 | hostnameHeader: deviceConfig.Hostname, 62 | gnmiAddrHeader: deviceConfig.GNMIServerAddr, 63 | } 64 | if syncRequried { 65 | headerMap[resyncRequiredHeader] = "True" 66 | } 67 | ctx := metadata.NewOutgoingContext(context.Background(), metadata.New(headerMap)) 68 | 69 | cli := pb.NewGNMIClient(conn) 70 | if _, err := cli.Get(ctx, getRequest); err != nil { 71 | return err 72 | } 73 | return nil 74 | } 75 | 76 | // Connect sends heartbeat messages to the assigned controller periodically. 77 | // The first (received) message contains a sync request to fetch the latest AP configuration. 78 | // This function never returns, should be run in the background. 79 | func Connect() { 80 | syncRequired := true 81 | for { 82 | if err := ReportAPInfo(syncRequired); err != nil { 83 | log.Errorf("Cannot connect to the controller, retry in %s. Error: %v.", heartbeatInterval, err) 84 | // Disconncetion detected, do a re-sync. 85 | syncRequired = true 86 | } else if syncRequired { 87 | log.Info("Controller connected and received the sync request.") 88 | syncRequired = false 89 | } 90 | time.Sleep(heartbeatInterval) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /agent/syscmd/base.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package syscmd contains methods that run external commands on device. 17 | package syscmd 18 | 19 | import ( 20 | "fmt" 21 | "io/ioutil" 22 | "os" 23 | "os/exec" 24 | "path" 25 | 26 | log "github.com/golang/glog" 27 | ) 28 | 29 | // CommandRunner contains methods executing external commands. 30 | type CommandRunner struct { 31 | // ExecCommand runs the given command with arguments. 32 | // It returns the content of stdout or stderr, and error if command failed. 33 | ExecCommand func(wait bool, cmd string, args ...string) (string, error) 34 | } 35 | 36 | // Runner executes external commands in the real environment. 37 | func Runner() *CommandRunner { 38 | return &CommandRunner{ 39 | ExecCommand: execute, 40 | } 41 | } 42 | 43 | func execute(wait bool, cmd string, args ...string) (string, error) { 44 | command := exec.Command(cmd, args...) 45 | var output []byte 46 | var err error 47 | 48 | if wait { 49 | output, err = command.CombinedOutput() 50 | } else { 51 | command.Stdout = os.Stdout 52 | err = command.Start() 53 | } 54 | 55 | outputString := string(output) 56 | if err != nil { 57 | log.Errorf("Command (%v %v) failed. Error: %v.\nOutput:\n%v", cmd, args, err, outputString) 58 | } else { 59 | log.V(2).Infof("Command (%v %v) succeeded.\nOutput:\n%v", cmd, args, outputString) 60 | } 61 | 62 | return outputString, err 63 | } 64 | 65 | func vlanINTFName(intfName string, vlanID int) string { 66 | return fmt.Sprintf("%s.%d", intfName, vlanID) 67 | } 68 | 69 | // SaveToFile saves the input string into a file in the file system. 70 | // It creates the file and all parent folder if not exist. 71 | func SaveToFile(folderPath, fileName, content string) error { 72 | fileFullPath := path.Join(folderPath, fileName) 73 | log.Infof("Saving content to file %v...", fileFullPath) 74 | 75 | fileInfo, err := os.Stat(folderPath) 76 | if os.IsNotExist(err) { 77 | // Config folder does not exist, create one. 78 | if err = os.MkdirAll(folderPath, 0755); err != nil { 79 | log.Errorf("Creating folder %v failed. Error: %v.", folderPath, err) 80 | return err 81 | } 82 | log.Infof("Created folder %v.", folderPath) 83 | } else if !fileInfo.Mode().IsDir() { 84 | log.Errorf("Unable to save content to %v, since %v points an existing file.", fileFullPath, folderPath) 85 | return fmt.Errorf("%s points an existing file", folderPath) 86 | } 87 | 88 | if err := ioutil.WriteFile(fileFullPath, []byte(content), 0600); err != nil { 89 | log.Errorf("Saving content to file %v failed. Error: %v.", fileFullPath, err) 90 | return err 91 | } 92 | log.Infof("Saved content to file %v.", fileFullPath) 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /tests/link022_ap_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:hostname":"link022-pi-ap", 3 | "openconfig-access-points:system":{ 4 | "aaa":{ 5 | "server-groups":{ 6 | "server-group":[ 7 | { 8 | "servers":{ 9 | "server":[ 10 | { 11 | "address":"192.168.11.1", 12 | "config":{ 13 | "address":"192.168.11.1", 14 | "timeout":5, 15 | "name":"radius-server" 16 | }, 17 | "radius":{ 18 | "config":{ 19 | "auth-port":1812, 20 | "secret-key":"radiuspwd" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | "name":"freeradius", 27 | "config":{ 28 | "name":"freeradius", 29 | "type":"openconfig-aaa-radius:RADIUS" 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | "openconfig-access-points:radios":{ 37 | "radio":[ 38 | { 39 | "config":{ 40 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 41 | "channel-width":20, 42 | "dca":false, 43 | "dtp":false, 44 | "transmit-power":3, 45 | "id":1, 46 | "channel":6 47 | }, 48 | "id":1 49 | } 50 | ] 51 | }, 52 | "openconfig-access-points:ssids":{ 53 | "ssid":[ 54 | { 55 | "config":{ 56 | "qbss-load":true, 57 | "dva":true, 58 | "station-isolation":true, 59 | "operating-frequency":"openconfig-wifi-types:FREQ_2_5_GHZ", 60 | "name":"Auth-Link022", 61 | "default-vlan":300, 62 | "dot11k":true, 63 | "supported-data-rates":[ 64 | "openconfig-wifi-types:RATE_36MB", 65 | "openconfig-wifi-types:RATE_48MB", 66 | "openconfig-wifi-types:RATE_54MB" 67 | ], 68 | "basic-data-rates":[ 69 | "openconfig-wifi-types:RATE_36MB", 70 | "openconfig-wifi-types:RATE_48MB", 71 | "openconfig-wifi-types:RATE_54MB" 72 | ], 73 | "broadcast-filter":true, 74 | "enabled":true, 75 | "server-group":"freeradius", 76 | "csa":true, 77 | "hidden":false, 78 | "opmode":"WPA2_ENTERPRISE" 79 | }, 80 | "name":"Auth-Link022" 81 | }, 82 | { 83 | "config":{ 84 | "advertise-apname":true, 85 | "basic-data-rates":[ 86 | "openconfig-wifi-types:RATE_11MB", 87 | "openconfig-wifi-types:RATE_24MB" 88 | ], 89 | "broadcast-filter":true, 90 | "csa":false, 91 | "dot11k":false, 92 | "dva":false, 93 | "enabled":true, 94 | "hidden":false, 95 | "name":"Guest-Link022", 96 | "operating-frequency":"openconfig-wifi-types:FREQ_2_5_GHZ", 97 | "opmode":"OPEN", 98 | "supported-data-rates":[ 99 | "openconfig-wifi-types:RATE_11MB", 100 | "openconfig-wifi-types:RATE_24MB" 101 | ], 102 | "default-vlan":200 103 | }, 104 | "name":"Guest-Link022" 105 | } 106 | ] 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /demo/cert/server/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | pKbgDYwzysCuagMLjAY5nkUfLrd7 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /testkit/gnmitest/testfile/ap_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:hostname":"link022-pi-ap", 3 | "openconfig-access-points:system":{ 4 | "aaa":{ 5 | "server-groups":{ 6 | "server-group":[ 7 | { 8 | "servers":{ 9 | "server":[ 10 | { 11 | "address":"192.168.11.1", 12 | "config":{ 13 | "address":"192.168.11.1", 14 | "timeout":5, 15 | "name":"radius-server" 16 | }, 17 | "radius":{ 18 | "config":{ 19 | "auth-port":1812, 20 | "secret-key":"radiuspwd" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | "name":"freeradius", 27 | "config":{ 28 | "name":"freeradius", 29 | "type":"openconfig-aaa-radius:RADIUS" 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | "openconfig-access-points:radios":{ 37 | "radio":[ 38 | { 39 | "config":{ 40 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 41 | "channel-width":20, 42 | "dca":false, 43 | "dtp":false, 44 | "transmit-power":3, 45 | "id":1, 46 | "channel":6 47 | }, 48 | "id":1 49 | } 50 | ] 51 | }, 52 | "openconfig-access-points:ssids":{ 53 | "ssid":[ 54 | { 55 | "config":{ 56 | "qbss-load":true, 57 | "dva":true, 58 | "station-isolation":true, 59 | "operating-frequency":"openconfig-wifi-types:FREQ_2_5_GHZ", 60 | "name":"Auth-Link022", 61 | "default-vlan":300, 62 | "dot11k":true, 63 | "supported-data-rates":[ 64 | "openconfig-wifi-types:RATE_36MB", 65 | "openconfig-wifi-types:RATE_48MB", 66 | "openconfig-wifi-types:RATE_54MB" 67 | ], 68 | "basic-data-rates":[ 69 | "openconfig-wifi-types:RATE_36MB", 70 | "openconfig-wifi-types:RATE_48MB", 71 | "openconfig-wifi-types:RATE_54MB" 72 | ], 73 | "broadcast-filter":true, 74 | "enabled":true, 75 | "server-group":"freeradius", 76 | "csa":true, 77 | "hidden":false, 78 | "opmode":"WPA2_ENTERPRISE" 79 | }, 80 | "name":"Auth-Link022" 81 | }, 82 | { 83 | "config":{ 84 | "advertise-apname":true, 85 | "basic-data-rates":[ 86 | "openconfig-wifi-types:RATE_11MB", 87 | "openconfig-wifi-types:RATE_24MB" 88 | ], 89 | "broadcast-filter":true, 90 | "csa":false, 91 | "dot11k":false, 92 | "dva":false, 93 | "enabled":true, 94 | "hidden":false, 95 | "name":"Guest-Link022", 96 | "operating-frequency":"openconfig-wifi-types:FREQ_2_5_GHZ", 97 | "opmode":"OPEN", 98 | "supported-data-rates":[ 99 | "openconfig-wifi-types:RATE_11MB", 100 | "openconfig-wifi-types:RATE_24MB" 101 | ], 102 | "default-vlan":200 103 | }, 104 | "name":"Guest-Link022" 105 | } 106 | ] 107 | } 108 | } -------------------------------------------------------------------------------- /demo/cert/client/client.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | 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 51 | OeDa/b+KF+Ri3ggvBzMkNUxQpAZ3w3p0 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /tests/ap_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:hostname":"link022-pi-ap", 3 | "openconfig-access-points:system":{ 4 | "aaa":{ 5 | "server-groups":{ 6 | "server-group":[ 7 | { 8 | "servers":{ 9 | "server":[ 10 | { 11 | "address":"192.168.11.1", 12 | "config":{ 13 | "address":"192.168.11.1", 14 | "timeout":5, 15 | "name":"radius-server" 16 | }, 17 | "radius":{ 18 | "config":{ 19 | "auth-port":1812, 20 | "secret-key":"radiuspwd" 21 | } 22 | } 23 | } 24 | ] 25 | }, 26 | "name":"freeradius", 27 | "config":{ 28 | "name":"freeradius", 29 | "type":"openconfig-aaa-radius:RADIUS" 30 | } 31 | } 32 | ] 33 | } 34 | } 35 | }, 36 | "openconfig-access-points:radios":{ 37 | "radio":[ 38 | { 39 | "config":{ 40 | "channel":157, 41 | "channel-width":40, 42 | "dca":false, 43 | "enabled":true, 44 | "id":0, 45 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 46 | "scanning":false, 47 | "scanning-defer-traffic":true, 48 | "transmit-power":9 49 | }, 50 | "id":0 51 | }, 52 | { 53 | "config":{ 54 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 55 | "channel-width":20, 56 | "dca":false, 57 | "enabled":true, 58 | "dtp":false, 59 | "transmit-power":3, 60 | "id":1, 61 | "channel":6 62 | }, 63 | "id":1 64 | } 65 | ] 66 | }, 67 | "openconfig-access-points:ssids":{ 68 | "ssid":[ 69 | { 70 | "config":{ 71 | "qbss-load":true, 72 | "dva":true, 73 | "station-isolation":true, 74 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 75 | "name":"Auth-Link022", 76 | "default-vlan":300, 77 | "dot11k":true, 78 | "supported-data-rates":[ 79 | "openconfig-wifi-types:RATE_36MB", 80 | "openconfig-wifi-types:RATE_48MB", 81 | "openconfig-wifi-types:RATE_54MB" 82 | ], 83 | "basic-data-rates":[ 84 | "openconfig-wifi-types:RATE_36MB", 85 | "openconfig-wifi-types:RATE_48MB", 86 | "openconfig-wifi-types:RATE_54MB" 87 | ], 88 | "broadcast-filter":true, 89 | "enabled":true, 90 | "server-group":"freeradius", 91 | "csa":true, 92 | "hidden":false, 93 | "opmode":"WPA2_ENTERPRISE" 94 | }, 95 | "name":"Auth-Link022" 96 | }, 97 | { 98 | "config":{ 99 | "advertise-apname":true, 100 | "basic-data-rates":[ 101 | "openconfig-wifi-types:RATE_11MB", 102 | "openconfig-wifi-types:RATE_24MB" 103 | ], 104 | "broadcast-filter":true, 105 | "csa":false, 106 | "dot11k":false, 107 | "dva":false, 108 | "enabled":true, 109 | "hidden":false, 110 | "name":"Guest-Link022", 111 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 112 | "opmode":"OPEN", 113 | "supported-data-rates":[ 114 | "openconfig-wifi-types:RATE_11MB", 115 | "openconfig-wifi-types:RATE_24MB" 116 | ], 117 | "default-vlan":200 118 | }, 119 | "name":"Guest-Link022" 120 | } 121 | ] 122 | } 123 | } -------------------------------------------------------------------------------- /testkit/common/common.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google Inc. 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 | https://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 | 16 | // Package common contains functions and models shared by all components. 17 | package common 18 | 19 | // OPType is the type of gNMI operation. 20 | type OPType string 21 | 22 | const ( 23 | // OPReplace is the gNMI replace operation. 24 | OPReplace OPType = "replace" 25 | // OPUpdate is the gNMI update operation. 26 | OPUpdate OPType = "update" 27 | // OPDelete is the gNMI delete operation. 28 | OPDelete OPType = "delete" 29 | // OPGet is the gNMI get operation. 30 | OPGet OPType = "get" 31 | // OPSubscribe is the gNMI subscribe operation. 32 | OPSubscribe OPType = "subscribe" 33 | ) 34 | 35 | // GNMITest is top-level model of gNMI test. 36 | type GNMITest struct { 37 | // Name is the test name. 38 | Name string `json:"name"` 39 | // Description is the detail description of this test. 40 | Description string `json:"description"` 41 | // GNMITestCase is the list of test cases to run in this test. 42 | GNMITestCase []*TestCase `json:"test_cases"` 43 | } 44 | 45 | // TestCase describes a gNMI test cases. 46 | type TestCase struct { 47 | // Name is the test case name. 48 | Name string `json:"name"` 49 | // Description is the detail description of this test case. 50 | Description string `json:"description"` 51 | // Model is used to construct the UseModels property in gNMI requests. 52 | // If not specified, all gNMI requests are sent without UseModels. 53 | Model *ModelData `json:"model"` 54 | // OPs contains a list of operations need to be processed in this test case. 55 | // All operations are processed in one single gNMI message. 56 | OPs []*Operation `json:"ops"` 57 | } 58 | 59 | // ModelData describes the OpenConfig model used in this test case. 60 | type ModelData struct { 61 | Name string `json:"name"` 62 | Organization string `json:"organization"` 63 | Version string `json:"version"` 64 | } 65 | 66 | // Operation represents a gNMI operation. 67 | type Operation struct { 68 | // Type is the gNMI operation type. 69 | Type OPType `json:"type"` 70 | // Path is the xPath of the target field/branch. 71 | Path string `json:"path"` 72 | // StatePath is the xPath of the corresponding state field/branch. 73 | // If specified, testkit will verify the state update. 74 | StatePath string `json:"state_path"` 75 | // Val is the string format of the desired value. 76 | // Val should be unset for gNMI delete operation. 77 | // Supported types: 78 | // Integer: "1", "2" 79 | // Float: "1.5", "2.4" 80 | // String: "abc", "defg" 81 | // Boolean: "true", "false" 82 | // IETF JSON from file: "@ap_config.json" 83 | Val string `json:"val"` 84 | } 85 | 86 | // TestResult contains the result of one gNMI test. 87 | type TestResult struct { 88 | // Name is the name of the test. 89 | Name string 90 | // PassedNum is the number of passed test cases. 91 | PassedNum int 92 | // FailedNum is the number of failed test cases. 93 | FailedNum int 94 | // Details contains detailed test results of each test cases. 95 | Details []*TestCaseResult 96 | } 97 | 98 | // TestCaseResult contains the result of one gNMI test case. 99 | type TestCaseResult struct { 100 | // Name is the test case name. 101 | Name string 102 | // Err is the error detected in this test case. nil if test passed. 103 | Err error 104 | } 105 | -------------------------------------------------------------------------------- /emulator/emulator.py: -------------------------------------------------------------------------------- 1 | """ Copyright 2018 Google Inc. 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 | https://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 | 16 | """Emulator for link022. Refer to the README.md file for instructions. 17 | """ 18 | 19 | import argparse 20 | import netaddr 21 | import logging 22 | 23 | import mininet.net 24 | import mininet.node 25 | import mininet.cli 26 | 27 | 28 | FLAGS = None 29 | logging.basicConfig(filename='/tmp/link022_emulator.log', level=logging.INFO) 30 | logger = logging.getLogger() 31 | 32 | 33 | def set_flags(): 34 | """Set the global FLAGS.""" 35 | global FLAGS 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument( 38 | '--target_cmd', 39 | help='Command line to start the target.') 40 | FLAGS = parser.parse_args() 41 | 42 | 43 | TARGET_NAME = 'target' 44 | CONTROLLER_NAME = 'ctrlr' 45 | DUMMY_NAME = 'dummy' 46 | 47 | 48 | def get_ip_spec(addr, subnet=None): 49 | """Get the IP address with the subnet prefix length. 50 | 51 | Args: 52 | addr: network.addr object 53 | subnet: network.net object 54 | 55 | Returns: 56 | A string of the IP address with prefix length. 57 | 58 | Raises: 59 | Exception: if ip not in subnet 60 | """ 61 | if subnet is None: 62 | if addr.version == 6: 63 | ip_spec = str(addr) + '/128' 64 | else: 65 | ip_spec = str(addr) + '/32' 66 | elif addr in subnet: 67 | ip_spec = '%s/%s' % (addr, subnet.prefixlen) 68 | else: 69 | raise Exception('ip %s is not in subnet %s' % (addr, subnet)) 70 | return ip_spec 71 | 72 | 73 | class Emulator(object): 74 | """Link022 emulator.""" 75 | 76 | def __init__(self): 77 | self._net = None 78 | self._ctrlr = None 79 | self._target = None 80 | self._target_popen = None 81 | 82 | def start(self): 83 | self._start_topo() 84 | logger.info('Emulator started.') 85 | mininet.cli.CLI(self._net) 86 | 87 | def _start_topo(self): 88 | """Create an empty network and add nodes to it. 89 | """ 90 | 91 | subnet = netaddr.IPNetwork('10.0.0.0/24') 92 | hosts_iter = subnet.iter_hosts() 93 | self._net = mininet.net.Mininet(controller=None) 94 | 95 | self._net.addHost(TARGET_NAME) 96 | self._net.addHost(CONTROLLER_NAME) 97 | # We add a dummy host to create the eth and wlan interfaces for the 98 | # Target 99 | self._net.addHost(DUMMY_NAME) 100 | params1 = {'ip': get_ip_spec(hosts_iter.next(), subnet)} 101 | params2 = {'ip': get_ip_spec(hosts_iter.next(), subnet)} 102 | self._net.addLink(TARGET_NAME, CONTROLLER_NAME, 103 | params1=params1, params2=params2) 104 | self._net.addLink(TARGET_NAME, DUMMY_NAME) 105 | self._net.addLink(TARGET_NAME, DUMMY_NAME) 106 | 107 | self._target = self._net[TARGET_NAME] 108 | self._ctrlr = self._net[CONTROLLER_NAME] 109 | 110 | self._net.start() 111 | logger.info('Running Link022 target command: %s', FLAGS.target_cmd) 112 | self._target_popen = self._target.popen(FLAGS.target_cmd) 113 | 114 | def cleanup(self): 115 | """Clean up emulator.""" 116 | if self._target_popen: 117 | self._target_popen.kill() 118 | self._target_popen = None 119 | if self._net: 120 | self._net.stop() 121 | logger.info('Emulator cleaned up.') 122 | 123 | if __name__ == '__main__': 124 | set_flags() 125 | emulator = Emulator() 126 | try: 127 | emulator.start() 128 | finally: 129 | emulator.cleanup() 130 | 131 | -------------------------------------------------------------------------------- /agent/gnmi/handler.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package gnmi 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "time" 22 | 23 | "github.com/google/link022/agent/context" 24 | "github.com/google/link022/agent/service" 25 | "github.com/google/link022/agent/syscmd" 26 | "github.com/google/link022/agent/util/ocutil" 27 | "github.com/google/link022/generated/ocstruct" 28 | "github.com/openconfig/ygot/ygot" 29 | 30 | log "github.com/golang/glog" 31 | ) 32 | 33 | var ( 34 | cmdRunner = syscmd.Runner() 35 | ) 36 | 37 | // handleSet is the callback function of the GNMI SET call. 38 | // It is triggered by the GNMI server. 39 | func handleSet(updatedConfig ygot.ValidatedGoStruct) (err error) { 40 | // Recover the panic and return error. 41 | defer func() { 42 | if r := recover(); r != nil { 43 | err = fmt.Errorf("panic detected when handling updated config: %v", r) 44 | } 45 | }() 46 | 47 | return handleSetInternal(updatedConfig) 48 | } 49 | 50 | func handleSetInternal(updatedConfig ygot.ValidatedGoStruct) error { 51 | // TODO: Handle delta change. Currently the GNMI server only supports replacing root. 52 | officeAPs, ok := updatedConfig.(*ocstruct.Device) 53 | if !ok { 54 | return errors.New("new configuration has invalid type") 55 | } 56 | 57 | configString, err := ygot.EmitJSON(officeAPs, &ygot.EmitJSONConfig{ 58 | Format: ygot.RFC7951, 59 | Indent: " ", 60 | RFC7951Config: &ygot.RFC7951JSONConfig{ 61 | AppendModuleName: false, 62 | }, 63 | }) 64 | if err != nil { 65 | return err 66 | } 67 | log.Infof("Received a new configuration:\n%v\n", configString) 68 | 69 | // TODO: Validate the OpenConfig module. 70 | deviceConfig := context.GetDeviceConfig() 71 | 72 | // Fetch the target AP configuration. 73 | apConfig := ocutil.FindAPConfig(officeAPs, deviceConfig.Hostname) 74 | if apConfig == nil { 75 | return fmt.Errorf("not found the configuration for this AP (hostname = %s)", deviceConfig.Hostname) 76 | } 77 | 78 | // Check and clean up the existing configuration. 79 | var changedVLANIDs []int 80 | existingVLANIDs, err := cmdRunner.VLANOnIntf(deviceConfig.ETHINTFName) 81 | if err != nil { 82 | return fmt.Errorf("unable to fetch the existing VLAN with error (%v), may need to reboot the device.", err) 83 | } 84 | 85 | resetIntf := false 86 | newVLANIDs := ocutil.VLANIDs(apConfig) 87 | if ocutil.VLANChanged(existingVLANIDs, newVLANIDs) { 88 | log.Infof("VLAN changes (%v -> %v) on interface %s.", existingVLANIDs, newVLANIDs, deviceConfig.ETHINTFName) 89 | changedVLANIDs = existingVLANIDs 90 | resetIntf = true 91 | } else { 92 | log.Infof("No VLAN change on interface %s.", deviceConfig.ETHINTFName) 93 | } 94 | 95 | // Clean up the existing configuration. 96 | service.CleanupConfig(deviceConfig.ETHINTFName, changedVLANIDs) 97 | 98 | // Wait for link to be available again. 99 | time.Sleep(5 * time.Second) 100 | 101 | // Process the incoming configuration. 102 | if err = service.ApplyConfig(apConfig, officeAPs.Gasket, resetIntf, deviceConfig.ETHINTFName, 103 | deviceConfig.WLANINTFName); err != nil { 104 | return err 105 | } 106 | log.Info("Device configuration succeeded.") 107 | 108 | // Save the succeeded config file. 109 | if err := syscmd.SaveToFile(runFolder, apConfigFileName, configString); err != nil { 110 | return err 111 | } 112 | log.Info("Saved the configuration to file.") 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /demo/util/server.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2017 Google Inc. 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 | # https://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 | #!/bin/bash 16 | # A utility script to start services on another machine, from which the operator 17 | # can manage the link022 device. 18 | # A typical setup is to create a peer to peer ethernet link between link022 and 19 | # the service machine. 20 | # Interface connected to the link022 host 21 | source servers.conf 22 | INTF=eth0 23 | OUT_INTF=wlan0 # Interface to connect to the Internet 24 | CTRL_INTF=eth1 # Interface to connect to a controlling machine, ssh is enabled here. 25 | GWIP=192.168.11.1 26 | 27 | sudo ip netns add ${NS} 28 | sudo ip link set dev ${INTF} netns ${NS} 29 | sudo ip netns exec ${NS} ip addr add ${GWIP}/24 dev ${INTF} 30 | sudo ip netns exec ${NS} ip link set dev ${INTF} up 31 | 32 | # Start DHCP 33 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 34 | -F set:s0,192.168.11.2,192.168.11.10 \ 35 | -O tag:s0,3,192.168.11.1 -O option:dns-server,8.8.8.8 -I lo -z \ 36 | -l /tmp/link022.leases -8 /tmp/link022.dhcp.log -i ${INTF} -a ${GWIP} --conf-file= & 37 | 38 | ########### Get Internet access for link022 39 | TO_DEF=to_def 40 | TO_NS=to_${NS} 41 | 42 | # enable forwarding 43 | sudo sysctl net.ipv4.ip_forward=1 44 | sudo ip netns exec ${NS} sysctl net.ipv4.ip_forward=1 45 | 46 | # create veth pair 47 | sudo ip link add name ${TO_NS} type veth peer name ${TO_DEF} netns ${NS} 48 | # configure interfaces and routes 49 | sudo ip addr add 192.168.22.1/30 dev ${TO_NS} 50 | sudo ip link set ${TO_NS} up 51 | # sudo ip route add 192.168.22.0/30 dev ${TO_NS} 52 | sudo ip netns exec ${NS} ip addr add 192.168.22.2/30 dev ${TO_DEF} 53 | sudo ip netns exec ${NS} ip link set ${TO_DEF} up 54 | sudo ip netns exec ${NS} ip route add default via 192.168.22.1 55 | # NAT in LK22 56 | sudo ip netns exec ${NS} iptables -t nat -F 57 | sudo ip netns exec ${NS} iptables -t nat -A POSTROUTING -o ${TO_DEF} -j MASQUERADE 58 | # NAT in default 59 | sudo iptables -P FORWARD DROP 60 | sudo iptables -F FORWARD 61 | # Assuming the host does not have other NAT rules. 62 | sudo iptables -t nat -F 63 | sudo iptables -t nat -A POSTROUTING -s 192.168.22.0/30 -o ${OUT_INTF} -j MASQUERADE 64 | sudo iptables -A FORWARD -i ${OUT_INTF} -o ${TO_NS} -j ACCEPT 65 | sudo iptables -A FORWARD -i ${TO_NS} -o ${OUT_INTF} -j ACCEPT 66 | 67 | ########### Adding vlans 68 | function add_vlan { 69 | vlan_name=$1 70 | vlan_id=$2 71 | vlan_net=$3 72 | vlan_gw=${vlan_net}.1 73 | sudo ip netns exec ${NS} ip link add link ${INTF} name ${vlan_name} type vlan id ${vlan_id} 74 | sudo ip netns exec ${NS} ip addr add ${vlan_gw}/24 dev ${vlan_name} 75 | sudo ip netns exec ${NS} ip link set dev ${vlan_name} up 76 | 77 | # Start DHCP 78 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 79 | -F set:s0,${vlan_net}.2,${vlan_net}.100 \ 80 | -O tag:s0,3,${vlan_gw} -O option:dns-server,8.8.8.8 -I lo -z \ 81 | -l /tmp/link022.${vlan_name}.leases -8 /tmp/link022.${vlan_name}.dhcp.log -i ${vlan_name} -a ${vlan_gw} --conf-file= & 82 | } 83 | add_vlan guest 200 192.168.33 84 | add_vlan auth 300 192.168.44 85 | 86 | RADIUS_PATH=../radius/freeradius 87 | sudo ip netns exec ${NS} freeradius -X -d ${RADIUS_PATH} > /tmp/radius.log & 88 | 89 | ########### Set up controlling machine interface 90 | CTRL_NET=192.168.55 91 | CTRL_GW=${CTRL_NET}.1 92 | sudo ip addr add ${CTRL_GW}/24 dev ${CTRL_INTF} 93 | sudo ip link set dev ${CTRL_INTF} up 94 | # Start DHCP 95 | sudo dnsmasq --no-ping -p 0 -k \ 96 | -F set:s0,${CTRL_NET}.2,${CTRL_NET}.10 \ 97 | -O option:dns-server,8.8.8.8 -I lo -z \ 98 | -l /tmp/link022.ctrl.leases -8 /tmp/link022.dhcp.ctrl.log -i ${CTRL_INTF} -a ${CTRL_GW} --conf-file= & 99 | -------------------------------------------------------------------------------- /agent/agent.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // The agent program is the link022 AP agent. 17 | // It converts the target device to an Link022 AP and runs in the background as 18 | // a management daemon. 19 | package main 20 | 21 | import ( 22 | //ctx "context" 23 | "flag" 24 | "fmt" 25 | "net" 26 | "os" 27 | 28 | "github.com/google/gnxi/utils/credentials" 29 | "github.com/google/link022/agent/context" 30 | "github.com/google/link022/agent/controller" 31 | "github.com/google/link022/agent/gnmi" 32 | //"github.com/google/link022/agent/monitoring" 33 | "github.com/google/link022/agent/syscmd" 34 | "google.golang.org/grpc" 35 | "google.golang.org/grpc/reflection" 36 | 37 | log "github.com/golang/glog" 38 | pb "github.com/openconfig/gnmi/proto/gnmi" 39 | ) 40 | 41 | var ( 42 | ethINTFName = flag.String("eth_intf_name", "eth0", "The management network interface on this device.") 43 | wlanINTFName = flag.String("wlan_intf_name", "wlan0", "The WLAN interface on this device for AP radio.") 44 | gnmiPort = flag.Int("gnmi_port", 10162, "The port GNMI server listening on.") 45 | controllerAddr = flag.String("controller_address", "", "The WiFi Controller of this device.") 46 | 47 | cmdRunner = syscmd.Runner() 48 | ) 49 | 50 | func main() { 51 | flag.Parse() 52 | log.Info("Link022 agent started.") 53 | 54 | deviceConfig := context.GetDeviceConfig() 55 | // Load hostname 56 | hostname, err := os.Hostname() 57 | if err != nil { 58 | log.Exit("Failed to load the device hostname.") 59 | } 60 | deviceConfig.Hostname = hostname 61 | log.Infof("Hostname = %s.", hostname) 62 | 63 | // Load AP network interface configuration. 64 | deviceConfig.ETHINTFName = *ethINTFName 65 | deviceConfig.WLANINTFName = *wlanINTFName 66 | log.Infof("Eth interface = %s. WLAN interface = %s.", *ethINTFName, *wlanINTFName) 67 | 68 | // Get gNMI server address. 69 | deviceIPv4, err := cmdRunner.DeviceIPv4() 70 | if err != nil { 71 | log.Exit("Failed to load the device IPv4 address.") 72 | } 73 | gNMIServerAddr := fmt.Sprintf("%s:%d", deviceIPv4, *gnmiPort) 74 | deviceConfig.GNMIServerAddr = gNMIServerAddr 75 | 76 | // Load controlle Info. 77 | deviceConfig.ControllerAddr = *controllerAddr 78 | if *controllerAddr != "" { 79 | log.Infof("AP controller = %s", *controllerAddr) 80 | go controller.Connect() 81 | } else { 82 | log.Info("No AP controller assigned.") 83 | } 84 | 85 | // Create GNMI server. 86 | gnmiServer, err := gnmi.NewServer() 87 | if err != nil { 88 | log.Exitf("Failed to create the GNMI server. Error: %v.", err) 89 | } 90 | 91 | // Start a goroutine to collect states periodically 92 | // Monitoing service is disabled because it has a bug that causes internal OpenConfig model being invalid. 93 | //backgroundContext := ctx.Background() 94 | //go monitoring.UpdateDeviceStatus(backgroundContext, gnmiServer) 95 | 96 | // Start the GNMI server. 97 | var opts []grpc.ServerOption 98 | if *controllerAddr == "" { 99 | // Add credential check if no controller specified. 100 | opts = credentials.ServerCredentials() 101 | } 102 | g := grpc.NewServer(opts...) 103 | pb.RegisterGNMIServer(g, gnmiServer) 104 | reflection.Register(g) 105 | listen, err := net.Listen("tcp", gNMIServerAddr) 106 | if err != nil { 107 | log.Exitf("Failed to listen on %s. Error: %v.", gNMIServerAddr, err) 108 | } 109 | 110 | log.Infof("Running GNMI server. Listen on %s.", gNMIServerAddr) 111 | if err := g.Serve(listen); err != nil { 112 | log.Exitf("Failed to run GNMI server on %s. Error: %v.", gNMIServerAddr, err) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /testkit/gnmitest/worker_test.go: -------------------------------------------------------------------------------- 1 | package gnmitest 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | "time" 8 | 9 | "github.com/google/gnxi/gnmi" 10 | "github.com/google/link022/generated/ocstruct" 11 | "github.com/google/link022/testkit/common" 12 | pb "github.com/openconfig/gnmi/proto/gnmi" 13 | "github.com/openconfig/ygot/ygot" 14 | "google.golang.org/grpc" 15 | ) 16 | 17 | func fakeHandleSet(ygot.ValidatedGoStruct) error { 18 | return nil 19 | } 20 | 21 | func startMockServer(t *testing.T) (string, func()) { 22 | // Create the GNMI server. 23 | model := gnmi.NewModel([]*pb.ModelData{{ 24 | Name: "openconfig-access-points", 25 | Organization: "OpenConfig working group", 26 | Version: "0.1.0", 27 | }}, 28 | reflect.TypeOf((*ocstruct.Device)(nil)), 29 | ocstruct.SchemaTree["Device"], 30 | ocstruct.Unmarshal, 31 | ocstruct.ΛEnum) 32 | 33 | s, err := gnmi.NewServer(model, 34 | nil, 35 | fakeHandleSet) 36 | if err != nil { 37 | t.Fatalf("Failed to create gNMI server: %v", err) 38 | } 39 | 40 | g := grpc.NewServer() 41 | pb.RegisterGNMIServer(g, s) 42 | 43 | lis, err := net.Listen("tcp", "") 44 | if err != nil { 45 | t.Fatalf("Failed in net.Listen: %v", err) 46 | } 47 | 48 | go g.Serve(lis) 49 | return lis.Addr().String(), func() { 50 | lis.Close() 51 | } 52 | } 53 | 54 | func TestRunTest(t *testing.T) { 55 | // Start mock gNMI server. 56 | serverAddr, teardown := startMockServer(t) 57 | defer teardown() 58 | 59 | // Create gNMI client. 60 | conn, err := grpc.Dial(serverAddr, grpc.WithInsecure()) 61 | if err != nil { 62 | t.Fatalf("Dialing to %q failed: %v", serverAddr, err) 63 | } 64 | defer conn.Close() 65 | client := pb.NewGNMIClient(conn) 66 | 67 | // Define test cases. 68 | testCases := []struct { 69 | targetTest *common.TestCase 70 | succeeded bool 71 | }{ 72 | { 73 | targetTest: &common.TestCase{ 74 | Name: "push all configurations", 75 | OPs: []*common.Operation{ 76 | { 77 | Type: common.OPReplace, 78 | Path: "/access-points/access-point[hostname=link022-pi-ap]", 79 | Val: "@testfile/ap_config.json", 80 | }, 81 | }, 82 | }, 83 | succeeded: true, 84 | }, 85 | { 86 | targetTest: &common.TestCase{ 87 | Name: "update radio channel", 88 | OPs: []*common.Operation{ 89 | { 90 | Type: common.OPUpdate, 91 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel", 92 | Val: "11", 93 | }, 94 | { 95 | Type: common.OPUpdate, 96 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 97 | Val: "20", 98 | }, 99 | }, 100 | }, 101 | succeeded: true, 102 | }, 103 | { 104 | targetTest: &common.TestCase{ 105 | Name: "get radio config", 106 | OPs: []*common.Operation{ 107 | { 108 | Type: common.OPGet, 109 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel", 110 | Val: "11", 111 | }, 112 | { 113 | Type: common.OPGet, 114 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 115 | Val: "20", 116 | }, 117 | }, 118 | }, 119 | succeeded: true, 120 | }, 121 | { 122 | targetTest: &common.TestCase{ 123 | Name: "invalid config", 124 | OPs: []*common.Operation{ 125 | { 126 | Type: common.OPGet, 127 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel", 128 | Val: "11", 129 | }, 130 | { 131 | Type: common.OPUpdate, 132 | Path: "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 133 | Val: "20", 134 | }, 135 | }, 136 | }, 137 | succeeded: false, 138 | }, 139 | } 140 | 141 | // Run test cases. 142 | for _, testCase := range testCases { 143 | err := RunTest(client, testCase.targetTest, 10*time.Second, 10*time.Second) 144 | testResult := err == nil 145 | if testResult != testCase.succeeded { 146 | t.Errorf("[%s] incorrect test result, actual = %v [%v], expected = %v", testCase.targetTest.Name, testResult, err, testCase.succeeded) 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /demo/faucet_ovs/setup.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 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 | # https://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 | #!/bin/bash 16 | 17 | NS=gw 18 | OUT_INTF=wlx00c0ca902f38 # Interface to connect to the Internet 19 | CTRL_INTF=eth1 # Interface to connect to a controlling machine, ssh is enabled here. 20 | GWIP=192.168.11.1 21 | 22 | 23 | # Create veth pair, one side for ovs, the other side for the host itself 24 | INTF_ON_OVS=ovs2${NS} 25 | INTF=to_ovs # inside the host 26 | sudo ip netns add ${NS} 27 | sudo ip link add name ${INTF_ON_OVS} type veth peer name ${INTF} netns ${NS} 28 | 29 | sudo ip link set dev ${INTF_ON_OVS} up 30 | sudo ip netns exec ${NS} ip addr add ${GWIP}/24 dev ${INTF} 31 | sudo ip netns exec ${NS} ip link set dev ${INTF} up 32 | sudo ip netns exec ${NS} ip link set dev lo up 33 | 34 | # Start DHCP 35 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 36 | -F set:s0,192.168.11.2,192.168.11.10 \ 37 | -O tag:s0,3,192.168.11.1 -O option:dns-server,8.8.8.8 -I lo -z \ 38 | -l /tmp/link022.leases -8 /tmp/link022.dhcp.log -i ${INTF} -a ${GWIP} --conf-file= & 39 | 40 | ########### Get Internet access for the NS 41 | TO_DEF=to_def 42 | TO_NS=def2${NS} 43 | 44 | # enable forwarding 45 | sudo sysctl net.ipv4.ip_forward=1 46 | sudo ip netns exec ${NS} sysctl net.ipv4.ip_forward=1 47 | 48 | # create veth pair 49 | sudo ip link add name ${TO_NS} type veth peer name ${TO_DEF} netns ${NS} 50 | # configure interfaces and routes 51 | sudo ip addr add 192.168.22.1/30 dev ${TO_NS} 52 | sudo ip link set ${TO_NS} up 53 | # sudo ip route add 192.168.22.0/30 dev ${TO_NS} 54 | sudo ip netns exec ${NS} ip addr add 192.168.22.2/30 dev ${TO_DEF} 55 | sudo ip netns exec ${NS} ip link set ${TO_DEF} up 56 | sudo ip netns exec ${NS} ip route add default via 192.168.22.1 57 | # NAT in LK22 58 | sudo ip netns exec ${NS} iptables -t nat -F 59 | sudo ip netns exec ${NS} iptables -t nat -A POSTROUTING -o ${TO_DEF} -j MASQUERADE 60 | # NAT in default 61 | sudo iptables -P FORWARD DROP 62 | sudo iptables -F FORWARD 63 | # Assuming the host does not have other NAT rules. 64 | sudo iptables -t nat -F 65 | sudo iptables -t nat -A POSTROUTING -s 192.168.22.0/30 -o ${OUT_INTF} -j MASQUERADE 66 | sudo iptables -A FORWARD -i ${OUT_INTF} -o ${TO_NS} -j ACCEPT 67 | sudo iptables -A FORWARD -i ${TO_NS} -o ${OUT_INTF} -j ACCEPT 68 | 69 | ########### Adding vlans 70 | function add_vlan { 71 | vlan_name=$1 72 | vlan_id=$2 73 | vlan_net=$3 74 | vlan_gw=${vlan_net}.1 75 | sudo ip netns exec ${NS} ip link add link ${INTF} name ${vlan_name} type vlan id ${vlan_id} 76 | sudo ip netns exec ${NS} ip addr add ${vlan_gw}/24 dev ${vlan_name} 77 | sudo ip netns exec ${NS} ip link set dev ${vlan_name} up 78 | 79 | # Start DHCP 80 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 81 | -F set:s0,${vlan_net}.2,${vlan_net}.100 \ 82 | -O tag:s0,3,${vlan_gw} -O option:dns-server,8.8.8.8 -I lo -z \ 83 | -l /tmp/link022.${vlan_name}.leases -8 /tmp/link022.${vlan_name}.dhcp.log -i ${vlan_name} -a ${vlan_gw} --conf-file= & 84 | } 85 | add_vlan guest 200 192.168.33 86 | add_vlan auth 300 192.168.44 87 | 88 | RADIUS_PATH=../radius/freeradius 89 | sudo ip netns exec ${NS} freeradius -X -d ${RADIUS_PATH} > /tmp/${NS}_radius.log & 90 | 91 | #############create ovs 92 | sudo ovs-vsctl add-br br0 \ 93 | -- set bridge br0 other-config:datapath-id=0000000000000001 \ 94 | -- set bridge br0 other-config:disable-in-band=true \ 95 | -- set bridge br0 fail_mode=secure \ 96 | -- add-port br0 ${INTF_ON_OVS} -- set interface ${INTF_ON_OVS} ofport_request=1 \ 97 | -- add-port br0 enp2s0 -- set interface enp2s0 ofport_request=2 \ 98 | -- add-port br0 enp3s0 -- set interface enp3s0 ofport_request=3 \ 99 | -- add-port br0 enp4s0 -- set interface enp4s0 ofport_request=4 \ 100 | -- add-port br0 enp5s0 -- set interface enp5s0 ofport_request=5 \ 101 | -- set-controller br0 tcp:127.0.0.1:6653 tcp:127.0.0.1:6654 -------------------------------------------------------------------------------- /agent/service/interface.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package service 17 | 18 | import ( 19 | "fmt" 20 | 21 | log "github.com/golang/glog" 22 | ) 23 | 24 | // configEthIntf configures the network interfaces on this device based on the given configuration. 25 | func configEthIntf(ethIntfName string, vlanIDs []int) error { 26 | log.Infof("Configuring interface %v. VLAN: %v.", ethIntfName, vlanIDs) 27 | vlanIntfNames := make(map[int]string) // VLAN ID -> VLAN Intf name 28 | for _, vlanID := range vlanIDs { 29 | // Add VLAN interface 30 | vlanIntfName, err := cmdRunner.CreateVLAN(ethIntfName, vlanID) 31 | if err != nil { 32 | return err 33 | } 34 | vlanIntfNames[vlanID] = vlanIntfName 35 | } 36 | 37 | // Restart eth interface. 38 | if err := cmdRunner.RestartIntf(ethIntfName); err != nil { 39 | return err 40 | } 41 | 42 | for vlanID, vlanIntfName := range vlanIntfNames { 43 | // Wipe out IP on VLAN interface. 44 | if err := cmdRunner.WipeOutIntfIP(vlanIntfName); err != nil { 45 | return err 46 | } 47 | 48 | // Add a network bridge 49 | bridgeName := getBridgeName(vlanID) 50 | if err := cmdRunner.CreateBridge(bridgeName); err != nil { 51 | return err 52 | } 53 | 54 | // Link VLAN intf to bridge 55 | if err := cmdRunner.AddBridgeIntf(bridgeName, vlanIntfName); err != nil { 56 | return err 57 | } 58 | 59 | // Bring up bridge 60 | if err := cmdRunner.BringUpIntf(bridgeName); err != nil { 61 | return err 62 | } 63 | } 64 | 65 | log.Infof("Configured interface %v.", ethIntfName) 66 | return nil 67 | } 68 | 69 | // cleanupEthIntf cleans up the network interfaces on this device based on the given configuration. 70 | // It goes through all cleanup steps even if some failures are detected, and returns all errors. 71 | func cleanupEthIntf(ethIntfName string, vlanIDs []int) []error { 72 | log.Infof("Cleaning up interface %s. VLAN: %d.", ethIntfName, vlanIDs) 73 | var errs []error 74 | 75 | for _, vlanID := range vlanIDs { 76 | // Delete VLAN interface. 77 | if err := cmdRunner.DeleteVLAN(ethIntfName, vlanID); err != nil { 78 | errs = append(errs, err) 79 | } 80 | 81 | // Turn down network bridge. 82 | bridgeName := getBridgeName(vlanID) 83 | if err := cmdRunner.TurnDownIntf(bridgeName); err != nil { 84 | errs = append(errs, err) 85 | } 86 | 87 | // Remove network bridge. 88 | if err := cmdRunner.DeleteBridge(bridgeName); err != nil { 89 | errs = append(errs, err) 90 | } 91 | } 92 | 93 | log.Infof("Cleaned up interface %s. Number of errors = %d.", ethIntfName, len(errs)) 94 | return errs 95 | } 96 | 97 | // configWLANIntf configures the network interfaces to make it work with hostapd. 98 | func configWLANIntf(wlanIntfName string) error { 99 | log.Infof("Configuring WLAN interface %v.", wlanIntfName) 100 | 101 | // Wipe out IP on WLAN interface. 102 | if err := cmdRunner.WipeOutIntfIP(wlanIntfName); err != nil { 103 | return err 104 | } 105 | 106 | if err := cmdRunner.TurnDownIntf(wlanIntfName); err != nil { 107 | return err 108 | } 109 | 110 | wlanIntfMAC, err := cmdRunner.IntfMAC(wlanIntfName) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | // Change the MAC address of the wireless interface to avoid conflict with other devices. 116 | updatedMAC := generateWLANIntfMAC(wlanIntfMAC) 117 | if err = cmdRunner.UpdateIntfMAC(wlanIntfName, updatedMAC); err != nil { 118 | return err 119 | } 120 | 121 | if err = cmdRunner.BringUpIntf(wlanIntfName); err != nil { 122 | return err 123 | } 124 | 125 | log.Infof("Configured WLAN interface %v.", wlanIntfName) 126 | return nil 127 | } 128 | 129 | func getBridgeName(vlanID int) string { 130 | return fmt.Sprintf("br_%d", vlanID) 131 | } 132 | 133 | func generateWLANIntfMAC(originalMAC string) string { 134 | return fmt.Sprintf("02%s0", originalMAC[2:len(originalMAC)-1]) 135 | } 136 | -------------------------------------------------------------------------------- /agent/gnmi/server.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package gnmi contains GNMI server and related methods. 17 | package gnmi 18 | 19 | import ( 20 | "errors" 21 | "fmt" 22 | "io/ioutil" 23 | "os" 24 | "path" 25 | "reflect" 26 | "strings" 27 | 28 | "github.com/google/gnxi/gnmi" 29 | "github.com/google/link022/generated/ocstruct" 30 | 31 | log "github.com/golang/glog" 32 | pb "github.com/openconfig/gnmi/proto/gnmi" 33 | "github.com/openconfig/ygot/ygot" 34 | "github.com/openconfig/ygot/ytypes" 35 | ) 36 | 37 | const ( 38 | runFolder = "/var/run/link022" 39 | apConfigFileName = "link022.conf" 40 | ) 41 | 42 | var ( 43 | // link022ModelData is a list of models supported in this GNMI server. 44 | link022ModelData = []*pb.ModelData{{ 45 | Name: "openconfig-access-points", 46 | Organization: "OpenConfig working group", 47 | Version: "0.1.0", 48 | }} 49 | ) 50 | 51 | // Server is a GNMI server. 52 | type Server struct { 53 | *gnmi.Server 54 | } 55 | 56 | type serverStateOperator func(path *pb.Path, val interface{}, config ygot.ValidatedGoStruct) error 57 | 58 | // NewServer creates a GNMI server. 59 | func NewServer() (*Server, error) { 60 | // Load existing config 61 | initConfigContent, err := loadExistingConfigContent() 62 | if err != nil { 63 | log.Errorf("Failed to load the existing configuration. Error: %v.", err) 64 | initConfigContent = nil 65 | } 66 | 67 | // Create the GNMI server. 68 | model := gnmi.NewModel(link022ModelData, 69 | reflect.TypeOf((*ocstruct.Device)(nil)), 70 | ocstruct.SchemaTree["Device"], 71 | ocstruct.Unmarshal, 72 | ocstruct.ΛEnum) 73 | 74 | s, err := gnmi.NewServer(model, 75 | initConfigContent, 76 | handleSet) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | gnmiServer := &Server{s} 82 | log.Info("GNMI server created.") 83 | return gnmiServer, nil 84 | } 85 | 86 | func loadExistingConfigContent() ([]byte, error) { 87 | existingConfigFilePath := path.Join(runFolder, apConfigFileName) 88 | 89 | if _, err := os.Stat(existingConfigFilePath); os.IsNotExist(err) { 90 | log.Info("No existing configuration found.") 91 | return nil, nil 92 | } 93 | 94 | existingConfigContent, err := ioutil.ReadFile(existingConfigFilePath) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | log.Info("Loaded existing configuration.") 100 | return existingConfigContent, nil 101 | } 102 | 103 | // GNXIStateOptGenerator decorate a given function to a gNXI state operator function 104 | func GNXIStateOptGenerator(path *pb.Path, val interface{}, stateOpt serverStateOperator) func(config ygot.ValidatedGoStruct) error { 105 | fp := func(config ygot.ValidatedGoStruct) error { 106 | return stateOpt(path, val, config) 107 | } 108 | return fp 109 | } 110 | 111 | // InternalUpdateState update state node in Server config. When updating, 112 | // call server's InternalUpdate method and send this function as parameter. 113 | // The type of val must exactly matchs node's type. 114 | func InternalUpdateState(path *pb.Path, val interface{}, config ygot.ValidatedGoStruct) error { 115 | checkStateNode := false 116 | for _, i := range path.GetElem() { 117 | if strings.Compare(i.GetName(), "state") == 0 { 118 | checkStateNode = true 119 | break 120 | } 121 | } 122 | if !checkStateNode { 123 | log.Error("failed update state: target node is not state node") 124 | return errors.New("target node is not state node") 125 | } 126 | 127 | node, _, err := ytypes.GetOrCreateNode(ocstruct.SchemaTree["Device"], config, path) 128 | if err != nil { 129 | return fmt.Errorf("failed retrive target node: %v", err) 130 | } 131 | 132 | if reflect.ValueOf(node).Kind() != reflect.Ptr { 133 | return fmt.Errorf("type of node is %v, not go struct pointer", reflect.ValueOf(node).Kind()) 134 | } 135 | reflect.ValueOf(node).Elem().Set(reflect.ValueOf(val)) 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /agent/util/ocutil/openconfig.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | // Package ocutil contains helper functions related to OpenConfig models. 17 | package ocutil 18 | 19 | import ( 20 | "reflect" 21 | "sort" 22 | 23 | "github.com/google/link022/generated/ocstruct" 24 | ) 25 | 26 | // FindAPConfig finds the configuration of the AP with a specific hostname. 27 | // It returns nil if not matching AP found. 28 | func FindAPConfig(apConfigs *ocstruct.Device, hostname string) *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint { 29 | if apConfigs.AccessPoints == nil { 30 | return nil 31 | } 32 | 33 | apConfig, ok := apConfigs.AccessPoints.AccessPoint[hostname] 34 | if !ok { 35 | return nil 36 | } 37 | return apConfig 38 | } 39 | 40 | // VLANChanged checkes whether there is any difference between the given two VLAN ID lists. 41 | func VLANChanged(existingVLANIDs, updatedVLANIDs []int) bool { 42 | sort.Ints(existingVLANIDs) 43 | sort.Ints(updatedVLANIDs) 44 | return !reflect.DeepEqual(existingVLANIDs, updatedVLANIDs) 45 | } 46 | 47 | // VLANIDs fetches the ID of all VLANs appears in the given office configuration. 48 | func VLANIDs(apConfig *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint) []int { 49 | vlanIDs := []int{} 50 | 51 | if apConfig == nil { 52 | return vlanIDs 53 | } 54 | 55 | wlans := apConfig.Ssids 56 | if wlans == nil || len(wlans.Ssid) == 0 { 57 | return vlanIDs 58 | } 59 | 60 | for _, wlan := range wlans.Ssid { 61 | vlanIDs = append(vlanIDs, int(*wlan.Config.DefaultVlan)) 62 | } 63 | 64 | return vlanIDs 65 | } 66 | 67 | // RadiusServers fetches the radius server assigned to the given AP. 68 | // It returns a SSID -> RadiusServer map. 69 | func RadiusServers(ap *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint) map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server { 70 | wlanRadiusMap := make(map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server) 71 | if ap.Ssids == nil { 72 | return wlanRadiusMap 73 | } 74 | 75 | apServerGPs := aaaServerGroups(ap) 76 | if len(apServerGPs) == 0 { 77 | return wlanRadiusMap 78 | } 79 | 80 | for wlanName, wlan := range ap.Ssids.Ssid { 81 | if wlan.Config.ServerGroup == nil { 82 | continue 83 | } 84 | 85 | aaaServerGPName := *wlan.Config.ServerGroup 86 | if serverGP, ok := apServerGPs[aaaServerGPName]; ok { 87 | if radiusServer := aaaRadiusServer(serverGP); radiusServer != nil { 88 | wlanRadiusMap[wlanName] = radiusServer 89 | } 90 | } 91 | } 92 | 93 | return wlanRadiusMap 94 | } 95 | 96 | func aaaServerGroups(ap *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint) map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup { 97 | apSystemInfo := ap.System 98 | if apSystemInfo == nil { 99 | return nil 100 | } 101 | 102 | aaaInfo := apSystemInfo.Aaa 103 | if aaaInfo == nil { 104 | return nil 105 | } 106 | 107 | serverGP := aaaInfo.ServerGroups 108 | if serverGP == nil { 109 | return nil 110 | } 111 | 112 | return serverGP.ServerGroup 113 | } 114 | 115 | func aaaRadiusServer(serverGP *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup) *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server { 116 | if serverGP == nil || serverGP.Config == nil || serverGP.Config.Type != ocstruct.OpenconfigAaaTypes_AAA_SERVER_TYPE_RADIUS { 117 | return nil 118 | } 119 | 120 | if serverGP.Servers == nil { 121 | return nil 122 | } 123 | 124 | for _, radiusServer := range serverGP.Servers.Server { 125 | if radiusServer != nil { 126 | // Return the first radius server specified. 127 | return radiusServer 128 | } 129 | } 130 | // Not found Radius server 131 | return nil 132 | } 133 | -------------------------------------------------------------------------------- /demo/README.gasket.md: -------------------------------------------------------------------------------- 1 | # Gasket & [Link022](https://github.com/google/link022) 2 | 3 | ## Getting Started 4 | First *read* over [Gasket's readme](https://github.com/bairdo/gasket/blob/master/docs/README.authentication.md) for a general overview of Gasket's authentication, and the [link022 readme](./README.md). 5 | 6 | 7 | ![alt text](./link022-gasket-diagram.png) 8 | 9 | We will use the [link022 demo](https://github.com/google/link022/tree/master/demo) and add an OpenFlow switch controlled by Gasket & Faucet in between the gateway and AP. 10 | - The Controller is connected to both the dataplane (untagged) and the control plane. 11 | 12 | 13 | ## Link022 AP 14 | Setting up a Link022 AP for use with Gasket is similar however we require a patched version of hostapd, which fixes a bug and adds a feature to allow RADIUS attributes to be saved on Access-Accept. 15 | This is required so that we can apply ACL rules based on what user is authenticated. 16 | Follow the steps on the Link022 github to setup except do not install the 'hostapd' application. 17 | 18 | To install the patched hostapd: 19 | ```bash 20 | git clone https://github.com/bairdo/hostapd-d1xf.git -b faucet-con 21 | make 22 | sudo make install 23 | ``` 24 | 25 | This provides a default compile configuration, that includes the UDP-remote control interface, which is used by Gasket over the network. 26 | 27 | 28 | ## Link022 Gateway 29 | Follow the instructions [here](./README.md) to setup the gateway. 30 | 31 | Two more configurations changes are needed: 32 | 33 | 1. Add Faucet RADIUS type to 'dictionary', and 'Faucet-ACL-ID' attribute to users in the users file. 34 | Examples [here](https://github.com/bairdo/gasket/blob/master/docs/README.authentication.md#radius-server) 35 | 36 | 2. Add the 'vendor-config' configuration (see below) to the top level of [demo/ap_config.json](./ap_config.json) 37 | 38 | ```json 39 | { 40 | "openconfig-gasket:gasket": { 41 | "ctrl-interface": "udp:8888", 42 | "radius-attribute": "26:12345:1:s" 43 | }, 44 | 45 | ###### Rest of standard link022 config. 46 | "openconfig-access-points:access-points": { 47 | "access-point": [ 48 | { 49 | "hostname": "raspberrypi", 50 | "system": { 51 | "aaa": { 52 | "server-groups": { 53 | "server-group": [ 54 | { 55 | "servers": { 56 | "server": [ 57 | { 58 | "address": "192.168.11.1", 59 | "config": { 60 | "address": "192.168.11.1", 61 | "timeout": 5, 62 | "name": "radius-server" 63 | }, 64 | 65 | ``` 66 | 67 | 68 | ## Gasket. 69 | 70 | At the moment the location (which switch and port) of the AP is hard-coded. 71 | In gasket/auth_app.py change the return statement of _get_dp_name_and_port to "return , " 72 | 73 | e.g. "return "faucet-1", 1" 74 | 75 | 76 | The demo gasket/faucet/... folder provides a good starting configuration for the network in the diagram above. 77 | 78 | Configure [auth.yaml](./gasket/faucet/gasket/auth.yaml) to point to the hostapd on the Link022 AP. 79 | 80 | See [base-no-authed-acls.yaml](./gasket/faucet/gasket/base-no-authed-acls.yaml) for an example ACL configuration. 81 | 82 | 83 | 84 | Build and run Gasket & Faucet: 85 | ```bash 86 | docker build -t bairdo/gasket -f Dockerfile.auth . 87 | docker run --privileged -v ~/link022/demo/gasket/faucet:/etc/ryu/faucet/ -v :/var/log/ryu/faucet/ -p 6663:6663 -p 6653:6653 -p 9244:9244 -ti bairdo/gasket 88 | ``` 89 | If using a Raspberry pi use [Dockerfile.pi](https://github.com/bairdo/gasket/blob/master/Dockerfile.pi) instead of Dockerfile.auth. 90 | 91 | 92 | 93 | ## Notes: 94 | - Access (which VLAN do clients belong to) to the guest and auth VLANs is provided by hostapd. 95 | - No ACLs are applied when a user connects to the guest SSID. 96 | - As access is controlled by hostapd, in theory you do not *need* to have ACLs for each user, assuming you have the base-acl config setup to allow all traffic on the VLAN. 97 | - Rules will be added to the switch for users authenticated on the auth SSID. 98 | 99 | 100 | ## TODO: 101 | - Support multiple hostapd control interfaces in Gasket. 102 | This will allow multiple Auth SSIDs/VLANs - [issue 32](https://github.com/Bairdo/gasket/issues/32) 103 | - Remove the hard-coded switch and port. 104 | Will probably take into account which hostapd authentication has occurred on. 105 | -------------------------------------------------------------------------------- /demo/ap_config_1r.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:access-points": { 3 | "access-point": [ 4 | { 5 | "hostname": "link022-ap", 6 | "system": { 7 | "aaa": { 8 | "server-groups": { 9 | "server-group": [ 10 | { 11 | "servers": { 12 | "server": [ 13 | { 14 | "address": "192.168.11.1", 15 | "config": { 16 | "address": "192.168.11.1", 17 | "timeout": 5, 18 | "name": "radius-server" 19 | }, 20 | "radius": { 21 | "config": { 22 | "auth-port": 1812, 23 | "secret-key": "radiuspwd" 24 | } 25 | } 26 | } 27 | ] 28 | }, 29 | "name": "freeradius", 30 | "config": { 31 | "name": "freeradius", 32 | "type": "openconfig-aaa-radius:RADIUS" 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | }, 39 | "radios": { 40 | "radio": [ 41 | { 42 | "config": { 43 | "operating-frequency": "openconfig-wifi-types:FREQ_2GHZ", 44 | "channel-width": 20, 45 | "dca": false, 46 | "dtp": false, 47 | "transmit-power": 3, 48 | "id": 1, 49 | "scanning": false, 50 | "channel": 6 51 | }, 52 | "id": 1 53 | } 54 | ] 55 | }, 56 | "ssids": { 57 | "ssid": [ 58 | { 59 | "config": { 60 | "qbss-load": true, 61 | "dva": true, 62 | "gtk-timeout": 3600, 63 | "station-isolation": true, 64 | "multicast-filter": true, 65 | "ptk-timeout": 28800, 66 | "operating-frequency": "openconfig-wifi-types:FREQ_2_5_GHZ", 67 | "name": "Auth-Link022", 68 | "default-vlan": 300, 69 | "advertise-apname": true, 70 | "ipv6-ndp-filter-timer": 300, 71 | "dot11k": true, 72 | "supported-data-rates": [ 73 | "openconfig-wifi-types:RATE_36MB", 74 | "openconfig-wifi-types:RATE_48MB", 75 | "openconfig-wifi-types:RATE_54MB" 76 | ], 77 | "basic-data-rates": [ 78 | "openconfig-wifi-types:RATE_36MB", 79 | "openconfig-wifi-types:RATE_48MB", 80 | "openconfig-wifi-types:RATE_54MB" 81 | ], 82 | "broadcast-filter": true, 83 | "enabled": true, 84 | "server-group": "freeradius", 85 | "csa": true, 86 | "dhcp-required": false, 87 | "hidden": false, 88 | "opmode": "WPA2_ENTERPRISE", 89 | "ipv6-ndp-filter": true 90 | }, 91 | "name": "Auth-Link022" 92 | }, 93 | { 94 | "config": { 95 | "advertise-apname": true, 96 | "basic-data-rates": [ 97 | "openconfig-wifi-types:RATE_11MB", 98 | "openconfig-wifi-types:RATE_24MB" 99 | ], 100 | "broadcast-filter": true, 101 | "csa": false, 102 | "dhcp-required": true, 103 | "dot11k": false, 104 | "dva": false, 105 | "enabled": true, 106 | "gtk-timeout": 1000, 107 | "hidden": false, 108 | "multicast-filter": false, 109 | "name": "Guest-Link022", 110 | "operating-frequency": "openconfig-wifi-types:FREQ_2_5_GHZ", 111 | "opmode": "OPEN", 112 | "ptk-timeout": 1000, 113 | "supported-data-rates": [ 114 | "openconfig-wifi-types:RATE_11MB", 115 | "openconfig-wifi-types:RATE_24MB" 116 | ], 117 | "default-vlan": 200 118 | }, 119 | "name": "Guest-Link022" 120 | } 121 | ] 122 | } 123 | } 124 | ] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /agent/util/ocutil/openconfig_test.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package ocutil 17 | 18 | import ( 19 | "reflect" 20 | "sort" 21 | "testing" 22 | 23 | "github.com/google/link022/agent/util/mock" 24 | "github.com/google/link022/generated/ocstruct" 25 | ) 26 | 27 | func TestFindAPConfig(t *testing.T) { 28 | // Define test cases. 29 | tests := []struct { 30 | apConfigs *ocstruct.Device 31 | targetAPName string 32 | found bool 33 | }{{ 34 | apConfigs: mock.GenerateConfig(true), 35 | targetAPName: "fake AP", 36 | found: false, 37 | }, { 38 | apConfigs: mock.GenerateConfig(true), 39 | targetAPName: "test-pi-1", 40 | found: true, 41 | }} 42 | 43 | for _, test := range tests { 44 | matchedConfig := FindAPConfig(test.apConfigs, test.targetAPName) 45 | foundMatch := matchedConfig != nil 46 | 47 | if foundMatch != test.found { 48 | t.Errorf("Incorrect FindAPConfig result (got: %v, want:%v).", foundMatch, test.found) 49 | } 50 | } 51 | } 52 | 53 | func TestVLANIDs(t *testing.T) { 54 | // Define test cases. 55 | tests := []struct { 56 | apConfig *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint 57 | vlanIDs []int 58 | }{{ 59 | apConfig: mock.GenerateAPConfig(true), 60 | vlanIDs: []int{250, 666}, 61 | }, { 62 | apConfig: mock.GenerateAPConfig(false), 63 | vlanIDs: []int{666}, 64 | }, { 65 | apConfig: nil, 66 | vlanIDs: []int{}, 67 | }} 68 | 69 | for _, test := range tests { 70 | got := VLANIDs(test.apConfig) 71 | want := test.vlanIDs 72 | sort.Ints(got) 73 | sort.Ints(want) 74 | if !reflect.DeepEqual(got, want) { 75 | t.Errorf("Incorrect VLAN IDs (got: %v, want: %v).", got, want) 76 | } 77 | } 78 | } 79 | 80 | func TestVLANChanged(t *testing.T) { 81 | // Define test cases. 82 | tests := []struct { 83 | existingConfig *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint 84 | updatedConfigConfig *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint 85 | vlanChanged bool 86 | }{{ 87 | existingConfig: mock.GenerateAPConfig(true), 88 | updatedConfigConfig: mock.GenerateAPConfig(true), 89 | vlanChanged: false, 90 | }, { 91 | existingConfig: mock.GenerateAPConfig(true), 92 | updatedConfigConfig: mock.GenerateAPConfig(false), 93 | vlanChanged: true, 94 | }, { 95 | existingConfig: mock.GenerateAPConfig(false), 96 | updatedConfigConfig: mock.GenerateAPConfig(true), 97 | vlanChanged: true, 98 | }, { 99 | existingConfig: nil, 100 | updatedConfigConfig: mock.GenerateAPConfig(true), 101 | vlanChanged: true, 102 | }, { 103 | existingConfig: mock.GenerateAPConfig(true), 104 | updatedConfigConfig: nil, 105 | vlanChanged: true, 106 | }, { 107 | existingConfig: nil, 108 | updatedConfigConfig: nil, 109 | vlanChanged: false, 110 | }} 111 | 112 | for _, test := range tests { 113 | got := VLANChanged(VLANIDs(test.existingConfig), VLANIDs(test.updatedConfigConfig)) 114 | want := test.vlanChanged 115 | if got != want { 116 | t.Errorf("Incorrect result (got: %v, want: %v).", got, want) 117 | } 118 | } 119 | } 120 | 121 | func TestRadiusServers(t *testing.T) { 122 | // Define test cases. 123 | tests := []struct { 124 | apConfig *ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint 125 | radiusServers map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server 126 | }{{ 127 | apConfig: mock.GenerateAPConfig(false), 128 | radiusServers: make(map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server), 129 | }, { 130 | apConfig: mock.GenerateAPConfig(true), 131 | radiusServers: map[string]*ocstruct.OpenconfigAccessPoints_AccessPoints_AccessPoint_System_Aaa_ServerGroups_ServerGroup_Servers_Server{ 132 | mock.AuthWLANName: mock.RadiusServer(), 133 | }, 134 | }} 135 | 136 | for _, test := range tests { 137 | got := RadiusServers(test.apConfig) 138 | want := test.radiusServers 139 | if !reflect.DeepEqual(got, want) { 140 | t.Errorf("Incorrect result (got: %v, want: %v).", got, want) 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /demo/ap_config_2r.json: -------------------------------------------------------------------------------- 1 | { 2 | "openconfig-access-points:access-points": { 3 | "access-point": [ 4 | { 5 | "hostname": "link022-ap", 6 | "system": { 7 | "aaa": { 8 | "server-groups": { 9 | "server-group": [ 10 | { 11 | "servers": { 12 | "server": [ 13 | { 14 | "address": "192.168.11.1", 15 | "config": { 16 | "address": "192.168.11.1", 17 | "timeout": 5, 18 | "name": "radius-server" 19 | }, 20 | "radius": { 21 | "config": { 22 | "auth-port": 1812, 23 | "secret-key": "radiuspwd" 24 | } 25 | } 26 | } 27 | ] 28 | }, 29 | "name": "freeradius", 30 | "config": { 31 | "name": "freeradius", 32 | "type": "openconfig-aaa-radius:RADIUS" 33 | } 34 | } 35 | ] 36 | } 37 | } 38 | }, 39 | "radios": { 40 | "radio": [ 41 | { 42 | "config": { 43 | "operating-frequency": "openconfig-wifi-types:FREQ_5GHZ", 44 | "channel-width": 40, 45 | "dca": false, 46 | "dtp": false, 47 | "transmit-power": 3, 48 | "scanning": false, 49 | "id": 0, 50 | "channel": 44 51 | }, 52 | "id": 0 53 | }, 54 | { 55 | "config": { 56 | "operating-frequency": "openconfig-wifi-types:FREQ_2GHZ", 57 | "channel-width": 20, 58 | "dca": false, 59 | "dtp": false, 60 | "transmit-power": 3, 61 | "id": 1, 62 | "scanning": false, 63 | "channel": 6 64 | }, 65 | "id": 1 66 | } 67 | ] 68 | }, 69 | "ssids": { 70 | "ssid": [ 71 | { 72 | "config": { 73 | "qbss-load": true, 74 | "dva": true, 75 | "gtk-timeout": 3600, 76 | "station-isolation": true, 77 | "multicast-filter": true, 78 | "ptk-timeout": 28800, 79 | "operating-frequency": "openconfig-wifi-types:FREQ_2_5_GHZ", 80 | "name": "Auth-Link022", 81 | "default-vlan": 300, 82 | "advertise-apname": true, 83 | "ipv6-ndp-filter-timer": 300, 84 | "dot11k": true, 85 | "supported-data-rates": [ 86 | "openconfig-wifi-types:RATE_36MB", 87 | "openconfig-wifi-types:RATE_48MB", 88 | "openconfig-wifi-types:RATE_54MB" 89 | ], 90 | "basic-data-rates": [ 91 | "openconfig-wifi-types:RATE_36MB", 92 | "openconfig-wifi-types:RATE_48MB", 93 | "openconfig-wifi-types:RATE_54MB" 94 | ], 95 | "broadcast-filter": true, 96 | "enabled": true, 97 | "server-group": "freeradius", 98 | "csa": true, 99 | "dhcp-required": false, 100 | "hidden": false, 101 | "opmode": "WPA2_ENTERPRISE", 102 | "ipv6-ndp-filter": true 103 | }, 104 | "name": "Auth-Link022" 105 | }, 106 | { 107 | "config": { 108 | "advertise-apname": true, 109 | "basic-data-rates": [ 110 | "RATE_11MB", 111 | "RATE_24MB" 112 | ], 113 | "broadcast-filter": true, 114 | "csa": false, 115 | "dhcp-required": true, 116 | "dot11k": false, 117 | "dva": false, 118 | "enabled": true, 119 | "gtk-timeout": 1000, 120 | "hidden": false, 121 | "multicast-filter": false, 122 | "name": "Guest-Link022", 123 | "operating-frequency": "openconfig-wifi-types:FREQ_2_5_GHZ", 124 | "opmode": "OPEN", 125 | "ptk-timeout": 1000, 126 | "supported-data-rates": [ 127 | "openconfig-wifi-types:RATE_11MB", 128 | "openconfig-wifi-types:RATE_24MB" 129 | ], 130 | "default-vlan": 200 131 | }, 132 | "name": "Guest-Link022" 133 | } 134 | ] 135 | } 136 | } 137 | ] 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Link022 Demo 2 | This doc contains the steps to run a demo. 3 | 4 | ## Setup demo environment 5 | Here is the structure of the demo setup. 6 | ![alt text](./Link022_diagrams-demo.png "Demo setup architecture") 7 | 8 | The setup has two components, linked directly by an ethernet cable. 9 | - One Raspberry Pi device as the Link022 AP (gnmi target). 10 | - The other device (such as Raspberry Pi) as the gateway. It provides reqiured services (dhcp, radius and gnmi client). 11 | 12 | ### Setup Link022 AP 13 | On the Link022 AP device, run the commands in this [instruction](../agent/README.md) with the sample certificates. 14 | 15 | ### Setup Gateway 16 | On the device for gateway follow the steps below. 17 | 1. Install Golang. 18 | 19 | To install manually on ARM architecture: 20 | ``` 21 | wget https://storage.googleapis.com/golang/go1.7.linux-armv6l.tar.gz 22 | sudo tar -C /usr/local -xzf go1.7.linux-armv6l.tar.gz 23 | export PATH=$PATH:/usr/local/go/bin 24 | ``` 25 | To install automatically on Debian based systems: 26 | ``` 27 | sudo apt-get install golang-go 28 | ``` 29 | 2. Install dependencies. 30 | ``` 31 | sudo apt-get install --no-install-recommends dnsmasq freeradius git 32 | ``` 33 | 3. Download GNMI clients. 34 | ``` 35 | export GOPATH=$HOME/go 36 | go get github.com/google/gnxi/gnmi_set 37 | go get github.com/google/gnxi/gnmi_get 38 | ``` 39 | 4. Download the [demo folder](./). 40 | 5. Enter the demo folder. 41 | ``` 42 | cd 43 | ``` 44 | 6. Setup the gateway. 45 | ``` 46 | cd util 47 | ./server.sh 48 | ``` 49 | This script creates a network namespace (lk22 by default), which has access to the Link022 AP device. 50 | 51 | Note: Reboot the device or run 'cleanup_servers.sh' script to clean up the gateway. 52 | 53 | ## Demo 54 | *All demo commands are executed inside the lk22 namspace on the gateway device.* 55 | To enter the namespace, run 56 | ``` 57 | sudo ip netns exec lk22 bash 58 | ``` 59 | 60 | ### Push the full configuration to AP 61 | Pushing the entire configuration to AP. It wipes out the existing configuration and applies the incoming one. 62 | Use the [sample configuration](./ap_config.json) here. 63 | ``` 64 | export PATH=$PATH:/usr/local/go/bin:/home/pi/go/bin 65 | sudo env PATH=$PATH gnmi_set \ 66 | -ca=cert/client/ca.crt \ 67 | -cert=cert/client/client.crt \ 68 | -key=cert/client/client.key \ 69 | -target_name=www.example.com \ 70 | -target_addr=: \ 71 | -replace=/:@ap_config.json 72 | ``` 73 | After configuration pushed, two WIFI SSIDs should appear. 74 | - Auth-Link022: The authenticated network with Radius (username: host-authed, password: authedpwd). 75 | - Guest-Link022: The open network. No authentication requried. 76 | 77 | ### Update AP configuration 78 | Besides replacing the entire configuration, it is also able to update part of the existing configuration. 79 | 80 | For example, updating the radio channel. 81 | ``` 82 | export PATH=$PATH:/usr/local/go/bin:/home/pi/go/bin 83 | sudo env PATH=$PATH gnmi_set -logtostderr \ 84 | -ca=cert/client/ca.crt \ 85 | -cert=cert/client/client.crt \ 86 | -key=cert/client/client.key \ 87 | -target_name=www.example.com \ 88 | -target_addr=: \ 89 | -update=/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel:6 90 | ``` 91 | 92 | Note: Check [gnxi set client](https://github.com/google/gnxi/tree/master/gnmi_set) for more use cases. 93 | ### Fetch AP configuration 94 | Check the existing configuration on AP device with a specific path. 95 | ``` 96 | export PATH=$PATH:/usr/local/go/bin:/home/pi/go/bin 97 | sudo env PATH=$PATH gnmi_get -logtostderr \ 98 | -ca=cert/client/ca.crt \ 99 | -cert=cert/client/client.crt \ 100 | -key=cert/client/client.key \ 101 | -target_name=www.example.com \ 102 | -target_addr=link022 AP IP address>: \ 103 | -xpath="/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel" 104 | ``` 105 | The output should be similar to: 106 | ``` 107 | == getResponse: 108 | notification: < 109 | timestamp: 1521145574058185274 110 | update: < 111 | path: < 112 | elem: < 113 | name: "access-points" 114 | > 115 | elem: < 116 | name: "access-point" 117 | key: < 118 | key: "hostname" 119 | value: "link022-pi-ap" 120 | > 121 | > 122 | elem: < 123 | name: "radios" 124 | > 125 | elem: < 126 | name: "radio" 127 | key: < 128 | key: "id" 129 | value: "1" 130 | > 131 | > 132 | elem: < 133 | name: "config" 134 | > 135 | elem: < 136 | name: "channel" 137 | > 138 | > 139 | val: < 140 | uint_val: 8 141 | > 142 | > 143 | > 144 | ``` 145 | Note: Check [gnxi get client](https://github.com/google/gnxi/tree/master/gnmi_get) for more use cases. 146 | -------------------------------------------------------------------------------- /agent/syscmd/syscmd_test.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | import ( 19 | "fmt" 20 | "reflect" 21 | "sort" 22 | "testing" 23 | ) 24 | 25 | const ( 26 | testIPLinkInfo = `1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1\ link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 promiscuity 0 27 | 2: eth0: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ef:4e:b6 brd ff:ff:ff:ff:ff:ff promiscuity 2 28 | 3: wlan0: mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ba:1b:e3 brd ff:ff:ff:ff:ff:ff promiscuity 0 29 | 4: wlan1: mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000\ link/ether 02:c0:ca:90:2f:50 brd ff:ff:ff:ff:ff:ff promiscuity 0 30 | 5: eth0.250@eth0: mtu 1500 qdisc noqueue master br_250 state UP mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ef:4e:b6 brd ff:ff:ff:ff:ff:ff promiscuity 1 \ vlan protocol 802.1Q id 250 \ bridge_slave 31 | 6: eth0.666@eth0: mtu 1500 qdisc noqueue master br_666 state UP mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ef:4e:b6 brd ff:ff:ff:ff:ff:ff promiscuity 1 \ vlan protocol 802.1Q id 666 \ bridge_slave 32 | 7: br_250: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ef:4e:b6 brd ff:ff:ff:ff:ff:ff promiscuity 0 \ bridge 33 | 8: br_666: mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000\ link/ether b8:27:eb:ef:4e:b6 brd ff:ff:ff:ff:ff:ff promiscuity 0 \ bridge 34 | ` 35 | 36 | testIntf = "eth0" 37 | testWLANIntf = "wlan0" 38 | testVLANID = 10 39 | 40 | bridgeName = "br_0" 41 | ) 42 | 43 | var ( 44 | testIPVLANLink = []int{250, 666} 45 | 46 | runner = &CommandRunner{ 47 | ExecCommand: func(wait bool, command string, args ...string) (string, error) { 48 | if command == "ip" && reflect.DeepEqual(args, []string{"-o", "-d", "link", "show"}) { 49 | return testIPLinkInfo, nil 50 | } 51 | 52 | // No ops. 53 | return "", nil 54 | }, 55 | } 56 | ) 57 | 58 | // Testing Interface commands. 59 | 60 | func TestCreateVLAN(t *testing.T) { 61 | vlanIntfName, err := runner.CreateVLAN(testIntf, testVLANID) 62 | if err != nil { 63 | t.Errorf("Creating VLAN interface failed. Error: %v.", err) 64 | return 65 | } 66 | 67 | expectedVLANIntfName := fmt.Sprintf("%s.%d", testIntf, testVLANID) 68 | if vlanIntfName != expectedVLANIntfName { 69 | t.Errorf("Incorrect VLAN interface name. (actual: %v, expected: %v)", 70 | vlanIntfName, expectedVLANIntfName) 71 | } 72 | } 73 | 74 | func TestDeleteVLAN(t *testing.T) { 75 | if err := runner.DeleteVLAN(testIntf, testVLANID); err != nil { 76 | t.Errorf("Deleting VLAN interface failed. Error: %v.", err) 77 | } 78 | } 79 | 80 | func TestRestartIntf(t *testing.T) { 81 | if err := runner.RestartIntf(testIntf); err != nil { 82 | t.Errorf("Restarting interface failed. Error: %v.", err) 83 | } 84 | } 85 | 86 | func TestWipeOutIntfIP(t *testing.T) { 87 | if err := runner.WipeOutIntfIP(testIntf); err != nil { 88 | t.Errorf("Wiping out interface IP failed. Error: %v.", err) 89 | } 90 | } 91 | 92 | func TestVLANOnIntf(t *testing.T) { 93 | if vlanIDs, err := runner.VLANOnIntf(testIntf); err != nil { 94 | t.Errorf("Fetching VLAN interface failed. Error: %v.", err) 95 | } else { 96 | sort.Ints(vlanIDs) 97 | if !reflect.DeepEqual(vlanIDs, testIPVLANLink) { 98 | t.Errorf("Incorrect result of VLANOnIntf, actual: %v, expected: %v.", vlanIDs, testIPVLANLink) 99 | } 100 | } 101 | } 102 | 103 | // Testing bridging commands. 104 | 105 | func TestCreateBridge(t *testing.T) { 106 | if err := runner.CreateBridge(bridgeName); err != nil { 107 | t.Errorf("Creating bridge failed. Error: %v.", err) 108 | } 109 | } 110 | 111 | func TestDeleteBridge(t *testing.T) { 112 | if err := runner.DeleteBridge(bridgeName); err != nil { 113 | t.Errorf("Deleting bridge failed. Error: %v.", err) 114 | } 115 | } 116 | 117 | func TestAddBridgeIntf(t *testing.T) { 118 | if err := runner.AddBridgeIntf(bridgeName, testIntf); err != nil { 119 | t.Errorf("Adding bridge interface failed. Error: %v.", err) 120 | } 121 | } 122 | 123 | // Test hostapd commands. 124 | 125 | func TestStartHostapd(t *testing.T) { 126 | if err := runner.StartHostapd(testWLANIntf); err != nil { 127 | t.Errorf("Starting hostapd process failed. Error: %v.", err) 128 | } 129 | } 130 | 131 | func TestStopAllHostapd(t *testing.T) { 132 | if err := runner.StopAllHostapd(); err != nil { 133 | t.Errorf("Stopping hostapd processes failed. Error: %v.", err) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /testkit/testdata/simple_test.json: -------------------------------------------------------------------------------- 1 | { 2 | "name":"Simple Test", 3 | "description":"This is an example of gNMI test.", 4 | "test_cases":[ 5 | { 6 | "name":"Push entire config", 7 | "description":"Push the entire configuration to AP device.", 8 | "ops":[ 9 | { 10 | "type":"replace", 11 | "path":"/access-points/access-point[hostname=link022-pi-ap]", 12 | "val":"@../tests/ap_config.json" 13 | } 14 | ] 15 | }, 16 | { 17 | "name":"Update Radio Channel", 18 | "ops":[ 19 | { 20 | "type":"update", 21 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel", 22 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/channel", 23 | "val":"11" 24 | }, 25 | { 26 | "type":"update", 27 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 28 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/channel-width", 29 | "val":"20" 30 | } 31 | ] 32 | }, 33 | { 34 | "name":"Update Radio Transmit Power", 35 | "ops":[ 36 | { 37 | "type":"update", 38 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/transmit-power", 39 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/transmit-power", 40 | "val":"9" 41 | }, 42 | { 43 | "type":"update", 44 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 45 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/channel-width", 46 | "val":"20" 47 | } 48 | ] 49 | }, 50 | { 51 | "name":"Update Radio channel-width", 52 | "ops":[ 53 | { 54 | "type":"update", 55 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel-width", 56 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/channel-width", 57 | "val":"40" 58 | } 59 | ] 60 | }, 61 | { 62 | "name":"Disable scanning on the Radio", 63 | "ops":[ 64 | { 65 | "type":"update", 66 | "path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/scanning", 67 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/state/scanning", 68 | "val":"false" 69 | } 70 | ] 71 | }, 72 | { 73 | "name":"Disable broadcast filter on Guest SSID", 74 | "ops":[ 75 | { 76 | "type":"update", 77 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/broadcast-filter", 78 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/state/broadcast-filter", 79 | "val":"false" 80 | } 81 | ] 82 | }, 83 | { 84 | "name":"Disable station-isolation on Guest SSID", 85 | "ops":[ 86 | { 87 | "type":"update", 88 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/station-isolation", 89 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/state/station-isolation", 90 | "val":"false" 91 | } 92 | ] 93 | }, 94 | { 95 | "name":"Update SSID frequency", 96 | "ops":[ 97 | { 98 | "type":"update", 99 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/operating-frequency", 100 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/state/operating-frequency", 101 | "val":"FREQ_5GHZ" 102 | } 103 | ] 104 | }, 105 | { 106 | "name":"Change opmode of Guest SSID to WPA-Personal", 107 | "ops":[ 108 | { 109 | "type":"update", 110 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/opmode", 111 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/state/opmode", 112 | "val":"WPA2_PERSONAL" 113 | }, 114 | { 115 | "type":"update", 116 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/wpa2-psk", 117 | "state_path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/state/wpa2-psk", 118 | "val":"testing123" 119 | } 120 | ] 121 | }, 122 | { 123 | "name":"Fetch SSID Config", 124 | "ops":[ 125 | { 126 | "type":"get", 127 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Auth-Link022]/config/default-vlan", 128 | "val":"300" 129 | }, 130 | { 131 | "type":"get", 132 | "path":"/access-points/access-point[hostname=link022-pi-ap]/ssids/ssid[name=Guest-Link022]/config/default-vlan", 133 | "val":"200" 134 | } 135 | ] 136 | } 137 | ] 138 | } -------------------------------------------------------------------------------- /agent/syscmd/interface.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2017 Google Inc. 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 | https://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 | 16 | package syscmd 17 | 18 | import ( 19 | "fmt" 20 | "strconv" 21 | "strings" 22 | 23 | log "github.com/golang/glog" 24 | ) 25 | 26 | // CreateVLAN creates vlan with a specific ID on target interface. 27 | // It returns created interface name if succeeded, or error if failed. 28 | func (r *CommandRunner) CreateVLAN(intfName string, vlanID int) (string, error) { 29 | vlanINTFName := vlanINTFName(intfName, vlanID) 30 | log.Infof("Creating VLAN interface %v...", vlanINTFName) 31 | if _, err := r.ExecCommand(true, "ip", "link", "add", "link", intfName, "name", vlanINTFName, "type", "vlan", "id", strconv.Itoa(vlanID)); err != nil { 32 | return "", err 33 | } 34 | log.Infof("Created VLAN interface %v.", vlanINTFName) 35 | return vlanINTFName, nil 36 | } 37 | 38 | // DeleteVLAN deletes the vlan with a specific ID on target interface. 39 | func (r *CommandRunner) DeleteVLAN(intfName string, vlanID int) error { 40 | vlanINTFName := vlanINTFName(intfName, vlanID) 41 | log.Infof("Deleting VLAN interface %v...", vlanINTFName) 42 | if _, err := r.ExecCommand(true, "ip", "link", "delete", vlanINTFName); err != nil { 43 | return err 44 | } 45 | log.Infof("Deleted VLAN interface %v.", vlanINTFName) 46 | return nil 47 | } 48 | 49 | // RestartIntf restarts a specific network interface. 50 | func (r *CommandRunner) RestartIntf(intfName string) error { 51 | log.Infof("Restarting interface %v...", intfName) 52 | 53 | if err := r.TurnDownIntf(intfName); err != nil { 54 | return err 55 | } 56 | 57 | if err := r.BringUpIntf(intfName); err != nil { 58 | return err 59 | } 60 | 61 | log.Infof("Restarted the interface %v.", intfName) 62 | return nil 63 | } 64 | 65 | // BringUpIntf brings up a certain network interface. 66 | func (r *CommandRunner) BringUpIntf(intfName string) error { 67 | if _, err := r.ExecCommand(true, "ifconfig", intfName, "up"); err != nil { 68 | return err 69 | } 70 | log.Infof("Interface %v is UP.", intfName) 71 | return nil 72 | } 73 | 74 | // TurnDownIntf turns down a certain network interface. 75 | func (r *CommandRunner) TurnDownIntf(intfName string) error { 76 | if _, err := r.ExecCommand(true, "ifconfig", intfName, "down"); err != nil { 77 | return err 78 | } 79 | log.Infof("Interface %v is DOWN.", intfName) 80 | return nil 81 | } 82 | 83 | // WipeOutIntfIP cleans up the IP address on a certain network interface. 84 | func (r *CommandRunner) WipeOutIntfIP(intfName string) error { 85 | if _, err := r.ExecCommand(true, "ifconfig", intfName, "0.0.0.0"); err != nil { 86 | return err 87 | } 88 | log.Infof("Wiped out the IP on interface %v.", intfName) 89 | return nil 90 | } 91 | 92 | // IntfMAC returns the MAC address of a certain interface. 93 | func (r *CommandRunner) IntfMAC(intfName string) (string, error) { 94 | mac, err := r.ExecCommand(true, "cat", fmt.Sprintf("/sys/class/net/%s/address", intfName)) 95 | if err != nil { 96 | return "", err 97 | } 98 | mac = mac[0 : len(mac)-1] // The last character is '\n' 99 | log.Infof("MAC address of %v is %v.", intfName, mac) 100 | return mac, nil 101 | } 102 | 103 | // VLANOnIntf returns IDs of all VLAN on the given interface. 104 | func (r *CommandRunner) VLANOnIntf(intfName string) ([]int, error) { 105 | // Fetch all interface information on the device. 106 | linkInfo, err := r.ExecCommand(true, "ip", "-o", "-d", "link", "show") 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | // Parse the result to find the target VLAN interface. 112 | vlanIDs := vlanIDsInIPLinkResult(intfName, linkInfo) 113 | log.Infof("Interface %s has VLAN %v.", intfName, vlanIDs) 114 | return vlanIDs, nil 115 | } 116 | 117 | func vlanIDsInIPLinkResult(intfName, linkInfo string) []int { 118 | var vlanIDs []int 119 | for _, intfInfo := range strings.Split(linkInfo, "\n") { 120 | if !strings.Contains(intfInfo, fmt.Sprintf("@%s", intfName)) || !strings.Contains(intfInfo, "vlan") { 121 | continue 122 | } 123 | for _, infoElem := range strings.Split(intfInfo, "\\") { 124 | if !strings.Contains(infoElem, "vlan") { 125 | continue 126 | } 127 | foundID := false 128 | for _, vlanInfoElem := range strings.Fields(infoElem) { 129 | if foundID { 130 | if vlanID, err := strconv.Atoi(vlanInfoElem); err == nil { 131 | vlanIDs = append(vlanIDs, vlanID) 132 | } 133 | break 134 | } 135 | if vlanInfoElem == "id" { 136 | foundID = true 137 | } 138 | } 139 | } 140 | } 141 | return vlanIDs 142 | } 143 | 144 | // UpdateIntfMAC changes the MAC address of a certain interface to the inputed one. 145 | func (r *CommandRunner) UpdateIntfMAC(intfName, updatedMAC string) error { 146 | if _, err := r.ExecCommand(true, "ifconfig", intfName, "hw", "ether", updatedMAC); err != nil { 147 | return err 148 | } 149 | log.Infof("The MAC address of %v updated to %v.", intfName, updatedMAC) 150 | return nil 151 | } 152 | 153 | // SendDHCPRequest sends a DHCP request for a certain network interface with the hostname in parameter. 154 | func (r *CommandRunner) SendDHCPRequest(intfName, hostname string) error { 155 | if _, err := r.ExecCommand(false, "udhcpc", "-i", intfName, "-x", fmt.Sprintf("hostname:%s", hostname)); err != nil { 156 | return err 157 | } 158 | log.Infof("Send DHCP request on interface %s with hostname %s.", intfName, hostname) 159 | return nil 160 | } 161 | -------------------------------------------------------------------------------- /tests/integration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Integration tests for link022 gnmi functions. 3 | 4 | Refer to the README.md file for instructions on running the tests. 5 | """ 6 | 7 | import argparse 8 | import json 9 | import netaddr 10 | import logging 11 | import platform 12 | import sys 13 | import time 14 | import tempfile 15 | import unittest 16 | 17 | import mininet.net 18 | import mininet.node 19 | import mininet.cli 20 | 21 | 22 | FLAGS = None 23 | logging.basicConfig(stream=sys.stdout, level=logging.INFO) 24 | logger = logging.getLogger() 25 | 26 | 27 | def set_flags(): 28 | """Set the global FLAGS 29 | """ 30 | global FLAGS 31 | parser = argparse.ArgumentParser() 32 | parser.add_argument( 33 | "--target_cmd", 34 | help="Command line to start the target. Needed if ext_target is False") 35 | parser.add_argument( 36 | "--ext_target", action='store_true', default=False, 37 | help='Use an external target.') 38 | parser.add_argument( 39 | "--emulator", action='store_true', default=False, 40 | help='Start the emulator and run no tests.') 41 | parser.add_argument("--gnmi_set", help="Path to the gnmi_set command") 42 | parser.add_argument("--ca", help="CA Certificate") 43 | parser.add_argument("--cert", help="Client Certificate") 44 | parser.add_argument("--key", help="Client key") 45 | parser.add_argument( 46 | "--target_name", help="Target name for cert verification") 47 | parser.add_argument("--target_addr", help="Target IP:port") 48 | parser.add_argument("--json_conf", help="File name of JSON config") 49 | FLAGS = parser.parse_args() 50 | 51 | 52 | TARGET_NAME = 'target' 53 | CONTROLLER_NAME = 'ctrlr' 54 | DUMMY_NAME = 'dummy' 55 | DEFAULT_NS = 'lk022_def' 56 | 57 | 58 | def get_ip_spec(addr, subnet=None): 59 | """Get the IP address with the subnet prefix length. 60 | 61 | Args: 62 | addr: network.addr object 63 | subnet: network.net object 64 | 65 | Returns: 66 | A string of the IP address with prefix length. 67 | 68 | Raises: 69 | Exception: if ip not in subnet 70 | """ 71 | if subnet is None: 72 | if addr.version == 6: 73 | ip_spec = str(addr) + '/128' 74 | else: 75 | ip_spec = str(addr) + '/32' 76 | elif addr in subnet: 77 | ip_spec = '%s/%s' % (addr, subnet.prefixlen) 78 | else: 79 | raise Exception('ip %s is not in subnet %s' % (addr, subnet)) 80 | return ip_spec 81 | 82 | 83 | class ConfigTest(unittest.TestCase): 84 | """Define the test driver. 85 | """ 86 | @classmethod 87 | def setUpClass(cls): 88 | cls._target_popen = None 89 | if FLAGS.ext_target: 90 | cls._ctrlr = mininet.node.Node(DEFAULT_NS, inNamespace=False) 91 | else: 92 | cls._start_topo() 93 | 94 | @classmethod 95 | def _start_topo(cls): 96 | """Create an empty network and add nodes to it. 97 | """ 98 | 99 | subnet = netaddr.IPNetwork('10.0.0.0/24') 100 | hosts_iter = subnet.iter_hosts() 101 | cls._net = mininet.net.Mininet(controller=None) 102 | 103 | cls._net.addHost(TARGET_NAME) 104 | cls._net.addHost(CONTROLLER_NAME) 105 | # We add a dummy host to create the eth and wlan interfaces for the 106 | # Target 107 | cls._net.addHost(DUMMY_NAME) 108 | params1 = {'ip': get_ip_spec(hosts_iter.next(), subnet)} 109 | params2 = {'ip': get_ip_spec(hosts_iter.next(), subnet)} 110 | cls._net.addLink(TARGET_NAME, CONTROLLER_NAME, 111 | params1=params1, params2=params2) 112 | cls._net.addLink(TARGET_NAME, DUMMY_NAME) 113 | cls._net.addLink(TARGET_NAME, DUMMY_NAME) 114 | 115 | cls._target = cls._net[TARGET_NAME] 116 | cls._ctrlr = cls._net[CONTROLLER_NAME] 117 | 118 | cls._net.start() 119 | logger.info('Running target command: %s', FLAGS.target_cmd) 120 | cls._target_popen = cls._target.popen(FLAGS.target_cmd) 121 | 122 | if FLAGS.emulator: 123 | mininet.cli.CLI(cls._net) 124 | else: 125 | # Wait for the agent to start up 126 | time.sleep(20) 127 | 128 | @classmethod 129 | def tearDownClass(cls): 130 | """Test clean up. 131 | """ 132 | if cls._target_popen: 133 | cls._target_popen.kill() 134 | cls._target_popen = None 135 | 136 | def runTest(self): 137 | """Run the test 138 | """ 139 | if FLAGS.emulator: 140 | self.skipTest('Skip the test in emulator mode.') 141 | ap_conf_file = FLAGS.json_conf 142 | if not FLAGS.ext_target: 143 | # Rewrite the hostname for emulator based tests. 144 | ap_conf = json.load(open(ap_conf_file, 'r')) 145 | for ap in ap_conf['openconfig-access-points:access-points']['access-point']: 146 | ap['hostname'] = platform.node() 147 | json_h = tempfile.NamedTemporaryFile() 148 | json.dump(ap_conf, json_h) 149 | json_h.flush() 150 | ap_conf_file = json_h.name 151 | gnmi_set_cmd_list = ( 152 | FLAGS.gnmi_set, 153 | '-ca=' + FLAGS.ca, 154 | '-cert=' + FLAGS.cert, 155 | '-key=' + FLAGS.key, 156 | '-target_name=' + FLAGS.target_name, 157 | '-target_addr=' + FLAGS.target_addr, 158 | '-replace=' + '/:@' + ap_conf_file) 159 | gnmi_set_cmd = ' '.join(gnmi_set_cmd_list) 160 | 161 | logger.info('Running gnmi_set command: %s', gnmi_set_cmd) 162 | _, _, code = self._ctrlr.pexec(gnmi_set_cmd) 163 | self.assertEqual(code, 0) 164 | if not FLAGS.ext_target: 165 | json_h.close() 166 | 167 | 168 | if __name__ == '__main__': 169 | set_flags() 170 | tests = unittest.TestSuite() 171 | tests.addTest(ConfigTest()) 172 | unittest.TextTestRunner().run(tests) 173 | -------------------------------------------------------------------------------- /testkit/test_kit.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google Inc. 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 | https://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 | 16 | // The test_kit program is a tool that tests gNMI functionalities of an AP device. 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "encoding/json" 22 | "flag" 23 | "fmt" 24 | "io/ioutil" 25 | "os" 26 | "time" 27 | 28 | log "github.com/golang/glog" 29 | "google.golang.org/grpc" 30 | 31 | "github.com/google/gnxi/utils/credentials" 32 | 33 | "github.com/google/link022/testkit/common" 34 | "github.com/google/link022/testkit/gnmitest" 35 | 36 | pb "github.com/openconfig/gnmi/proto/gnmi" 37 | ) 38 | 39 | type arrayFlags []string 40 | 41 | func (i *arrayFlags) String() string { 42 | return "my string representation" 43 | } 44 | 45 | func (i *arrayFlags) Set(value string) error { 46 | *i = append(*i, value) 47 | return nil 48 | } 49 | 50 | var ( 51 | gnmiTests arrayFlags 52 | targetAddr = flag.String("target_addr", "localhost:10161", "The target address in the format of host:port") 53 | targetName = flag.String("target_name", "hostname.com", "The target name used to verify the hostname returned by TLS handshake") 54 | timeout = flag.Duration("time_out", 30*time.Second, "Timeout for each request, 30 seconds by default") 55 | stateUpdateMaxDelay = flag.Duration("state_update_max_delay", 30*time.Second, "The maximum delay of the corresponding state field updating after a config field changes") 56 | pauseMode = flag.Bool("pause_mode", false, "Pause after each test case") 57 | ) 58 | 59 | func loadTests(testFiles []string) ([]*common.GNMITest, error) { 60 | var tests []*common.GNMITest 61 | for _, testFile := range testFiles { 62 | testContent, err := ioutil.ReadFile(testFile) 63 | if err != nil { 64 | return nil, err 65 | } 66 | test := &common.GNMITest{} 67 | if err := json.Unmarshal(testContent, test); err != nil { 68 | return nil, err 69 | } 70 | tests = append(tests, test) 71 | log.Infof("Loaded [%s].", test.Name) 72 | } 73 | return tests, nil 74 | } 75 | 76 | func runTest(client pb.GNMIClient, gNMITest *common.GNMITest, timeout time.Duration, stateUpdateDelay time.Duration) *common.TestResult { 77 | var testCaseResults []*common.TestCaseResult 78 | 79 | // Run gNMI config tests. 80 | log.Infof("Running [%s].", gNMITest.Name) 81 | var passedNum, failedNum int 82 | totalNum := len(gNMITest.GNMITestCase) 83 | for i, testcase := range gNMITest.GNMITestCase { 84 | log.Infof("Started [%s].", testcase.Name) 85 | err := gnmitest.RunTest(client, testcase, timeout, stateUpdateDelay) 86 | if err != nil { 87 | failedNum += 1 88 | log.Errorf("[%d/%d] [%s] failed: %v.", i+1, totalNum, testcase.Name, err) 89 | } else { 90 | passedNum += 1 91 | log.Infof("[%d/%d] [%s] succeeded.", i+1, totalNum, testcase.Name) 92 | } 93 | 94 | result := &common.TestCaseResult{ 95 | Name: testcase.Name, 96 | Err: err, 97 | } 98 | testCaseResults = append(testCaseResults, result) 99 | 100 | if *pauseMode && i < totalNum { 101 | reader := bufio.NewReader(os.Stdin) 102 | // Pause until user triggers next test case manually. 103 | fmt.Println("Press ENTER to start the next test case.") 104 | _, _ = reader.ReadString('\n') 105 | } 106 | } 107 | 108 | if failedNum > 0 { 109 | log.Errorf("[%s] failed.", gNMITest.Name) 110 | } else { 111 | log.Infof("[%s] succeeded.", gNMITest.Name) 112 | } 113 | 114 | return &common.TestResult{ 115 | Name: gNMITest.Name, 116 | PassedNum: passedNum, 117 | FailedNum: failedNum, 118 | Details: testCaseResults, 119 | } 120 | } 121 | 122 | func resultString(passed bool) string { 123 | if passed { 124 | return "PASS" 125 | } 126 | return "FAIL" 127 | } 128 | 129 | func printResult(results []*common.TestResult) { 130 | fmt.Println("=Test results=") 131 | // Print details of each test. 132 | for _, test := range results { 133 | fmt.Println("--------------------") 134 | fmt.Printf("[%s] %s\n", resultString(test.FailedNum == 0), test.Name) 135 | for _, testCase := range test.Details { 136 | passed := testCase.Err == nil 137 | fmt.Printf("|-[%s] %s\n", resultString(passed), testCase.Name) 138 | if !passed { 139 | fmt.Printf(" \\- %v\n", testCase.Err) 140 | } 141 | } 142 | } 143 | // Print test result summay. 144 | fmt.Println("--------------------") 145 | for _, test := range results { 146 | fmt.Printf("[%s] [%s] Passed - %d, Failed - %d\n", resultString(test.FailedNum == 0), test.Name, test.PassedNum, test.FailedNum) 147 | } 148 | } 149 | 150 | func main() { 151 | flag.Var(&gnmiTests, "test_file", "The file containing gNMI test.") 152 | flag.Parse() 153 | 154 | log.Info("Test kit started.") 155 | 156 | // Load test cases. 157 | tests, err := loadTests(gnmiTests) 158 | if err != nil { 159 | log.Fatalf("Failed to load tests. Error: %v.", err) 160 | } 161 | log.Infof("Loaded %d test files..", len(tests)) 162 | 163 | // Create gNMI client. 164 | opts := credentials.ClientCredentials(*targetName) 165 | conn, err := grpc.Dial(*targetAddr, opts...) 166 | if err != nil { 167 | log.Fatalf("Dialing to %q failed: %v", *targetAddr, err) 168 | } 169 | defer conn.Close() 170 | client := pb.NewGNMIClient(conn) 171 | 172 | // Run all tests. 173 | var results []*common.TestResult 174 | for _, test := range tests { 175 | results = append(results, runTest(client, test, *timeout, *stateUpdateMaxDelay)) 176 | } 177 | 178 | // Print out the result. 179 | printResult(results) 180 | } 181 | -------------------------------------------------------------------------------- /demo/facuet_hw/setup.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google Inc. 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 | # https://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 | #!/bin/bash 16 | 17 | NS=gw 18 | OUT_OVS_DATA_INTF=wlx00c0ca902f38 # Interface to connect to the Internet 19 | GWIP=192.168.11.1 20 | 21 | # Create veth pair, one side for ovs, the other side for the host itself 22 | OVS_DATA_INTF_ON_OVS=ovs2${NS} 23 | OVS_DATA_INTF=to_ovs # inside the host 24 | sudo ip netns add ${NS} 25 | sudo ip link add name ${OVS_DATA_INTF_ON_OVS} type veth peer name ${OVS_DATA_INTF} netns ${NS} 26 | 27 | sudo ip link set dev ${OVS_DATA_INTF_ON_OVS} up 28 | sudo ip netns exec ${NS} ip addr add ${GWIP}/24 dev ${OVS_DATA_INTF} 29 | sudo ip netns exec ${NS} ip link set dev ${OVS_DATA_INTF} up 30 | sudo ip netns exec ${NS} ip link set dev lo up 31 | 32 | # Start DHCP 33 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 34 | -F set:s0,192.168.11.2,192.168.11.10 \ 35 | -O tag:s0,3,192.168.11.1 -O option:dns-server,8.8.8.8 -I lo -z \ 36 | -l /tmp/link022.leases -8 /tmp/link022.dhcp.log -i ${OVS_DATA_INTF} -a ${GWIP} --conf-file= & 37 | 38 | ########### Get Internet access for the NS 39 | TO_DEF=to_def 40 | TO_NS=def2${NS} 41 | 42 | # enable forwarding 43 | sudo sysctl net.ipv4.ip_forward=1 44 | sudo ip netns exec ${NS} sysctl net.ipv4.ip_forward=1 45 | 46 | # create veth pair 47 | sudo ip link add name ${TO_NS} type veth peer name ${TO_DEF} netns ${NS} 48 | # configure interfaces and routes 49 | sudo ip addr add 192.168.22.1/30 dev ${TO_NS} 50 | sudo ip link set ${TO_NS} up 51 | # sudo ip route add 192.168.22.0/30 dev ${TO_NS} 52 | sudo ip netns exec ${NS} ip addr add 192.168.22.2/30 dev ${TO_DEF} 53 | sudo ip netns exec ${NS} ip link set ${TO_DEF} up 54 | sudo ip netns exec ${NS} ip route add default via 192.168.22.1 55 | # NAT in LK22 56 | sudo ip netns exec ${NS} iptables -t nat -F 57 | sudo ip netns exec ${NS} iptables -t nat -A POSTROUTING -o ${TO_DEF} -j MASQUERADE 58 | # NAT in default 59 | sudo iptables -P FORWARD DROP 60 | sudo iptables -F FORWARD 61 | # Assuming the host does not have other NAT rules. 62 | sudo iptables -t nat -F 63 | sudo iptables -t nat -A POSTROUTING -s 192.168.22.0/30 -o ${OUT_OVS_DATA_INTF} -j MASQUERADE 64 | sudo iptables -A FORWARD -i ${OUT_OVS_DATA_INTF} -o ${TO_NS} -j ACCEPT 65 | sudo iptables -A FORWARD -i ${TO_NS} -o ${OUT_OVS_DATA_INTF} -j ACCEPT 66 | 67 | ########### Adding vlans 68 | function add_vlan { 69 | vlan_name=$1 70 | vlan_id=$2 71 | vlan_net=$3 72 | data_intf=$4 73 | vlan_gw=${vlan_net}.1 74 | sudo ip netns exec ${NS} ip link add link ${data_intf} name ${vlan_name} type vlan id ${vlan_id} 75 | sudo ip netns exec ${NS} ip addr add ${vlan_gw}/24 dev ${vlan_name} 76 | sudo ip netns exec ${NS} ip link set dev ${vlan_name} up 77 | 78 | # Start DHCP 79 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 80 | -F set:s0,${vlan_net}.2,${vlan_net}.100 \ 81 | -O tag:s0,3,${vlan_gw} -O option:dns-server,8.8.8.8 -I lo -z \ 82 | -l /tmp/link022.${vlan_name}.leases -8 /tmp/link022.${vlan_name}.dhcp.log -i ${vlan_name} -a ${vlan_gw} --conf-file= & 83 | } 84 | add_vlan guest 200 192.168.33 ${OVS_DATA_INTF} 85 | add_vlan auth 300 192.168.44 ${OVS_DATA_INTF} 86 | 87 | RADIUS_PATH=../radius/freeradius 88 | sudo ip netns exec ${NS} freeradius -X -d ${RADIUS_PATH} > /tmp/${NS}_radius.log & 89 | 90 | #############create ovs 91 | sudo ovs-vsctl add-br br0 \ 92 | -- set bridge br0 other-config:datapath-id=0000000000000001 \ 93 | -- set bridge br0 other-config:disable-in-band=true \ 94 | -- set bridge br0 fail_mode=secure \ 95 | -- add-port br0 ${OVS_DATA_INTF_ON_OVS} -- set interface ${OVS_DATA_INTF_ON_OVS} ofport_request=1 \ 96 | -- add-port br0 enp2s0 -- set interface enp2s0 ofport_request=2 \ 97 | -- add-port br0 enp3s0 -- set interface enp3s0 ofport_request=3 \ 98 | -- add-port br0 enp4s0 -- set interface enp4s0 ofport_request=4 \ 99 | -- set-controller br0 tcp:127.0.0.1:6653 tcp:127.0.0.1:6654 100 | 101 | #############hardware switch 102 | ln -f -s /proc/1/ns/net /var/run/netns/default 103 | HW_CPN_GW=192.168.12 104 | HW_CPN_GW_IP=${HW_CPN_GW}.1 105 | CPN_NS_INTF=enp6s0 106 | HW_DATA_INTF=enp5s0 107 | sudo ip link set dev ${CPN_NS_INTF} netns ${NS} 108 | sudo ip netns exec ${NS} ip addr add ${HW_CPN_GW_IP}/24 dev ${CPN_NS_INTF} 109 | sudo ip netns exec ${NS} ip link set dev ${CPN_NS_INTF} up 110 | for port in 6653 6654; do 111 | sudo ip netns exec ${NS} socat -d TCP-LISTEN:${port},bind=${HW_CPN_GW_IP},reuseaddr,fork EXEC:"ip netns exec default socat -d STDIO TCP\:127.0.0.1\:${port},keepalive" & 112 | done 113 | 114 | 115 | sudo ip netns exec gw dnsmasq --no-ping -p 0 -k -F set:s0,${HW_CPN_GW}.2,${HW_CPN_GW}.10 -O tag:s0,3,${HW_CPN_GW_IP} -O option:dns-server,8.8.8.8 -I lo -z -l /tmp/cpn_hw.leases -8 /tmp/cpn_hw.dhcp.log -i ${CPN_NS_INTF} -a ${HW_CPN_GW_IP} --conf-file= & 116 | 117 | 118 | HW_DATA_GW=192.168.13 119 | HW_DATA_GW_IP=${HW_DATA_GW}.1 120 | sudo ip link set dev ${HW_DATA_INTF} netns ${NS} 121 | sudo ip netns exec ${NS} ip addr add ${HW_DATA_GW_IP}/24 dev ${HW_DATA_INTF} 122 | # Start DHCP 123 | sudo ip netns exec ${NS} dnsmasq --no-ping -p 0 -k \ 124 | -F set:s0,${HW_DATA_GW}.2,${HW_DATA_GW}.10 \ 125 | -O tag:s0,3,${HW_DATA_GW_IP} -O option:dns-server,8.8.8.8 -I lo -z \ 126 | --dhcp-option-force=vendor:vendorclass,102,1.2.3.4 \ 127 | -l /tmp/hw_data.leases -8 /tmp/hw_data.dhcp.log -i ${HW_DATA_INTF} -a ${HW_DATA_GW_IP} --conf-file= & 128 | sudo ip netns exec ${NS} ip link set dev ${HW_DATA_INTF} up 129 | add_vlan guesthw 200 192.168.55 ${HW_DATA_INTF} 130 | add_vlan authhw 300 192.168.66 ${HW_DATA_INTF} 131 | -------------------------------------------------------------------------------- /testkit/util/gnmiutil/helper_test.go: -------------------------------------------------------------------------------- 1 | package gnmiutil 2 | 3 | import ( 4 | "testing" 5 | 6 | pb "github.com/openconfig/gnmi/proto/gnmi" 7 | ) 8 | 9 | const ( 10 | sampleConfigJSON = `{ 11 | "radio":[ 12 | { 13 | "config":{ 14 | "channel":157, 15 | "channel-width":40, 16 | "enabled":true, 17 | "id":0, 18 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 19 | "transmit-power":9 20 | }, 21 | "id":0 22 | }, 23 | { 24 | "config":{ 25 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 26 | "channel-width":20, 27 | "enabled":true, 28 | "dtp":false, 29 | "transmit-power":3, 30 | "id":1, 31 | "channel":6 32 | }, 33 | "id":1 34 | } 35 | ] 36 | }` 37 | sampleConfigStateJSON = `{ 38 | "radio":[ 39 | { 40 | "config":{ 41 | "channel":157, 42 | "channel-width":40, 43 | "enabled":true, 44 | "id":0, 45 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 46 | "transmit-power":9 47 | }, 48 | "id":0, 49 | "state":{ 50 | "base-radio-mac":"5c:5b:35:00:2b:00", 51 | "channel":157, 52 | "channel-width":40, 53 | "counters":{ 54 | "noise-floor":-86 55 | }, 56 | "enabled":true, 57 | "id":0, 58 | "operating-frequency":"openconfig-wifi-types:FREQ_5GHZ", 59 | "rx-noise-channel-utilization":19, 60 | "total-channel-utilization":20, 61 | "transmit-power":9 62 | } 63 | }, 64 | { 65 | "config":{ 66 | "channel":6, 67 | "channel-width":20, 68 | "dtp":false, 69 | "enabled":true, 70 | "id":1, 71 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 72 | "transmit-power":3 73 | }, 74 | "id":1, 75 | "state":{ 76 | "base-radio-mac":"5c:5b:35:00:2a:f0", 77 | "channel":6, 78 | "channel-width":20, 79 | "counters":{ 80 | "noise-floor":-73 81 | }, 82 | "dtp":false, 83 | "enabled":true, 84 | "id":1, 85 | "operating-frequency":"openconfig-wifi-types:FREQ_2GHZ", 86 | "rx-noise-channel-utilization":82, 87 | "total-channel-utilization":83, 88 | "transmit-power":3 89 | } 90 | } 91 | ] 92 | }` 93 | ) 94 | 95 | func TestValEqual(t *testing.T) { 96 | testCases := []struct { 97 | actual *pb.TypedValue 98 | expected *pb.TypedValue 99 | isEqual bool 100 | }{ 101 | { 102 | actual: &pb.TypedValue{ 103 | Value: &pb.TypedValue_IntVal{ 104 | IntVal: 6, 105 | }, 106 | }, 107 | expected: &pb.TypedValue{ 108 | Value: &pb.TypedValue_IntVal{ 109 | IntVal: 6, 110 | }, 111 | }, 112 | isEqual: true, 113 | }, 114 | { 115 | actual: &pb.TypedValue{ 116 | Value: &pb.TypedValue_IntVal{ 117 | IntVal: 6, 118 | }, 119 | }, 120 | expected: &pb.TypedValue{ 121 | Value: &pb.TypedValue_IntVal{ 122 | IntVal: 10, 123 | }, 124 | }, 125 | isEqual: false, 126 | }, 127 | { 128 | actual: &pb.TypedValue{ 129 | Value: &pb.TypedValue_FloatVal{ 130 | FloatVal: 3.33, 131 | }, 132 | }, 133 | expected: &pb.TypedValue{ 134 | Value: &pb.TypedValue_FloatVal{ 135 | FloatVal: 3.33, 136 | }, 137 | }, 138 | isEqual: true, 139 | }, 140 | { 141 | actual: &pb.TypedValue{ 142 | Value: &pb.TypedValue_FloatVal{ 143 | FloatVal: 3.33, 144 | }, 145 | }, 146 | expected: &pb.TypedValue{ 147 | Value: &pb.TypedValue_FloatVal{ 148 | FloatVal: 6.666, 149 | }, 150 | }, 151 | isEqual: false, 152 | }, 153 | { 154 | actual: &pb.TypedValue{ 155 | Value: &pb.TypedValue_BoolVal{ 156 | BoolVal: true, 157 | }, 158 | }, 159 | expected: &pb.TypedValue{ 160 | Value: &pb.TypedValue_BoolVal{ 161 | BoolVal: true, 162 | }, 163 | }, 164 | isEqual: true, 165 | }, 166 | { 167 | actual: &pb.TypedValue{ 168 | Value: &pb.TypedValue_BoolVal{ 169 | BoolVal: true, 170 | }, 171 | }, 172 | expected: &pb.TypedValue{ 173 | Value: &pb.TypedValue_BoolVal{ 174 | BoolVal: false, 175 | }, 176 | }, 177 | isEqual: false, 178 | }, 179 | { 180 | actual: &pb.TypedValue{ 181 | Value: &pb.TypedValue_StringVal{ 182 | StringVal: "One", 183 | }, 184 | }, 185 | expected: &pb.TypedValue{ 186 | Value: &pb.TypedValue_StringVal{ 187 | StringVal: "One", 188 | }, 189 | }, 190 | isEqual: true, 191 | }, 192 | { 193 | actual: &pb.TypedValue{ 194 | Value: &pb.TypedValue_StringVal{ 195 | StringVal: "One", 196 | }, 197 | }, 198 | expected: &pb.TypedValue{ 199 | Value: &pb.TypedValue_StringVal{ 200 | StringVal: "Two", 201 | }, 202 | }, 203 | isEqual: false, 204 | }, 205 | { 206 | actual: &pb.TypedValue{ 207 | Value: &pb.TypedValue_JsonIetfVal{ 208 | JsonIetfVal: []byte(sampleConfigStateJSON), 209 | }, 210 | }, 211 | expected: &pb.TypedValue{ 212 | Value: &pb.TypedValue_JsonIetfVal{ 213 | JsonIetfVal: []byte(sampleConfigJSON), 214 | }, 215 | }, 216 | isEqual: true, 217 | }, 218 | { 219 | actual: &pb.TypedValue{ 220 | Value: &pb.TypedValue_BoolVal{ 221 | BoolVal: true, 222 | }, 223 | }, 224 | expected: &pb.TypedValue{ 225 | Value: &pb.TypedValue_StringVal{ 226 | StringVal: "Two", 227 | }, 228 | }, 229 | isEqual: false, 230 | }, 231 | } 232 | 233 | for _, testCase := range testCases { 234 | if err := ValEqual(nil, testCase.actual, testCase.expected); (err == nil) != testCase.isEqual { 235 | t.Errorf("incorrect matching result, actual = %v, expected = %v. (matching candidate: %v, %v)", err == nil, testCase.isEqual, testCase.actual, testCase.expected) 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /emulator/README.md: -------------------------------------------------------------------------------- 1 | # Link022 emulator 2 | This doc contains steps to run a Link022 emulator. 3 | 4 | The emulator builds a local testing environment with Link022 agent running inside a mininet node. 5 | 6 | ### Prerequisites 7 | Download the Link022 [repository](../). 8 | 9 | ### 1. Setup environment 10 | The setup needs Python 2.7 environment and some additional packages. 11 | ``` 12 | apt-get install python python-netaddr mininet udhcpc bridge-utils hostapd 13 | go get github.com/openconfig/goyang/pkg/yang 14 | go get github.com/openconfig/ygot/experimental/ygotutils 15 | ``` 16 | 17 | ### 2. Compile Link022 agent 18 | Run the [build script](../build.sh) to compile the Link022 agent. 19 | It stores output binary file in the "binary" folder. 20 | ``` 21 | ./build.sh 22 | ``` 23 | 24 | ### 3. Start emulator 25 | Run the following command to start the emulator: 26 | ``` 27 | cd emulator 28 | 29 | sudo env -u TERM python emulator.py \ 30 | --target_cmd "../binary/link022_agent -ca ../demo/cert/server/ca.crt \ 31 | -cert ../demo/cert/server/server.crt \ 32 | -key ../demo/cert/server/server.key -eth_intf_name target-eth1 \ 33 | -wlan_intf_name target-eth2" 34 | ``` 35 | 36 | The mininet CLI should appear after emulator started. 37 | ``` 38 | mininet> 39 | ``` 40 | 41 | ### 4. Verify the setup 42 | 43 | ### Check mininet nodes 44 | ``` 45 | mininet> nodes 46 | available nodes are: 47 | ctrlr dummy target 48 | ``` 49 | 50 | There are three nodes in mininet. 51 | * ctrlr: where gNMI client runs on. 52 | * target: where Link022 agent runs on. 53 | * dummy: a dummy host to contain the emulated eth and wlan interfaces. 54 | 55 | ### Check Link022 agent 56 | 57 | The "target" node contains the following interfaces: 58 | ``` 59 | mininet> target ifconfig 60 | lo: flags=73 mtu 65536 61 | inet 127.0.0.1 netmask 255.0.0.0 62 | inet6 ::1 prefixlen 128 scopeid 0x10 63 | loop txqueuelen 1 (Local Loopback) 64 | RX packets 0 bytes 0 (0.0 B) 65 | RX errors 0 dropped 0 overruns 0 frame 0 66 | TX packets 0 bytes 0 (0.0 B) 67 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 68 | 69 | target-eth0: flags=4163 mtu 1500 70 | inet 10.0.0.1 netmask 255.0.0.0 broadcast 10.255.255.255 71 | inet6 fe80::7cfb:bff:fe9c:7355 prefixlen 64 scopeid 0x20 72 | ether 7e:fb:0b:9c:73:55 txqueuelen 1000 (Ethernet) 73 | RX packets 6 bytes 508 (508.0 B) 74 | RX errors 0 dropped 0 overruns 0 frame 0 75 | TX packets 5 bytes 418 (418.0 B) 76 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 77 | 78 | target-eth1: flags=4163 mtu 1500 79 | inet6 fe80::38b1:64ff:fee8:ef6c prefixlen 64 scopeid 0x20 80 | ether 3a:b1:64:e8:ef:6c txqueuelen 1000 (Ethernet) 81 | RX packets 6 bytes 508 (508.0 B) 82 | RX errors 0 dropped 0 overruns 0 frame 0 83 | TX packets 6 bytes 508 (508.0 B) 84 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 85 | 86 | target-eth2: flags=4163 mtu 1500 87 | inet6 fe80::1490:40ff:fe65:1c4 prefixlen 64 scopeid 0x20 88 | ether 16:90:40:65:01:c4 txqueuelen 1000 (Ethernet) 89 | RX packets 6 bytes 508 (508.0 B) 90 | RX errors 0 dropped 0 overruns 0 frame 0 91 | TX packets 6 bytes 508 (508.0 B) 92 | TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 93 | ``` 94 | "target-eth0" is the management interface of the emulated AP, where gNMI target listens on (default port 10162). 95 | 96 | The link022 agent process runs inside "target" node: 97 | ``` 98 | mininet> target ps aux | grep ../binary/link022_agent 99 | root 28499 0.0 0.0 777732 14664 ? Ssl 16:08 0:00 ../binary/link022_agent -ca ../demo/cert/server/ca.crt -cert ../demo/cert/server/server.crt -key ../demo/cert/server/server.key -eth_intf_name target-eth1 -wlan_intf_name target-eth2 100 | ``` 101 | 102 | The link022 log is "/tmp/link022_agent.INFO" by default. 103 | 104 | ### 5. Config Link022 AP 105 | All gNMI requests working on a physical Link022 AP should also work on the emulated one. 106 | Run gNMI client in "ctrlr" node. 107 | 108 | Here are some examples to start with: 109 | 110 | 1. Download GNMI clients. 111 | * Download and compile [gNXI "SET" client](https://github.com/google/gnxi/tree/master/gnmi_set). 112 | * Download and compile [gNXI "GET" client](https://github.com/google/gnxi/tree/master/gnmi_get). 113 | 114 | 2. Pushing the entire configuration to AP. It wipes out the existing configuration and applies the incoming one. 115 | ``` 116 | mininet> xterm ctrlr 117 | {path to gnmi_set binary} \ 118 | -ca ../demo/cert/client/ca.crt \ 119 | -cert ../demo/cert/client/client.crt \ 120 | -key ../demo/cert/client/client.key \ 121 | -target_name www.example.com \ 122 | -target_addr 10.0.0.1:10162 \ 123 | -replace=/:@../tests/ap_config.json 124 | ``` 125 | 126 | 3. Fetch AP configuration 127 | ``` 128 | mininet> xterm ctrlr 129 | {path to gnmi_get binary} \ 130 | -ca ../demo/cert/client/ca.crt \ 131 | -cert ../demo/cert/client/client.crt \ 132 | -key ../demo/cert/client/client.key \ 133 | -target_name www.example.com \ 134 | -target_addr 10.0.0.1:10162 \ 135 | -xpath "/access-points/access-point[hostname=link022-pi-ap]/radios/radio[id=1]/config/channel" 136 | ``` 137 | 138 | The output should be similar to: 139 | ``` 140 | == getResponse: 141 | notification: < 142 | timestamp: 1521145574058185274 143 | update: < 144 | path: < 145 | elem: < 146 | name: "access-points" 147 | > 148 | elem: < 149 | name: "access-point" 150 | key: < 151 | key: "hostname" 152 | value: "link022-pi-ap" 153 | > 154 | > 155 | elem: < 156 | name: "radios" 157 | > 158 | elem: < 159 | name: "radio" 160 | key: < 161 | key: "id" 162 | value: "1" 163 | > 164 | > 165 | elem: < 166 | name: "config" 167 | > 168 | elem: < 169 | name: "channel" 170 | > 171 | > 172 | val: < 173 | uint_val: 8 174 | > 175 | > 176 | > 177 | ``` 178 | -------------------------------------------------------------------------------- /testkit/util/gnmiutil/helper.go: -------------------------------------------------------------------------------- 1 | /* Copyright 2018 Google Inc. 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 | https://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 | 16 | // Package gnmiutil contains helper functions related to gNMI. 17 | package gnmiutil 18 | 19 | import ( 20 | "bytes" 21 | "encoding/json" 22 | "fmt" 23 | "io/ioutil" 24 | "reflect" 25 | "strconv" 26 | 27 | pb "github.com/openconfig/gnmi/proto/gnmi" 28 | ) 29 | 30 | // GNMIFullPath builds the full path from the prefix and path. 31 | func GNMIFullPath(prefix, path *pb.Path) *pb.Path { 32 | if prefix == nil { 33 | return path 34 | } 35 | 36 | fullPath := &pb.Path{Origin: path.Origin} 37 | if path.GetElem() != nil { 38 | fullPath.Elem = append(prefix.GetElem(), path.GetElem()...) 39 | } 40 | return fullPath 41 | } 42 | 43 | // ToPbVal convert string to TypedValue defined in gNMI proto. 44 | // Supported types: 45 | // Integer: "1", "2" 46 | // Float: "1.5", "2.4" 47 | // String: "abc", "defg" 48 | // Boolean: "true", "false" 49 | // IETF JSON from file: "@ap_config.json" 50 | func ToPbVal(stringVal string) (*pb.TypedValue, error) { 51 | if stringVal[0] == '@' { 52 | jsonFile := stringVal[1:] 53 | jsonConfig, err := ioutil.ReadFile(jsonFile) 54 | if err != nil { 55 | return nil, fmt.Errorf("cannot read data from file %v: %v", jsonFile, err) 56 | } 57 | jsonConfig = bytes.Trim(jsonConfig, " \r\n\t") 58 | return &pb.TypedValue{ 59 | Value: &pb.TypedValue_JsonIetfVal{ 60 | JsonIetfVal: jsonConfig, 61 | }, 62 | }, nil 63 | } 64 | 65 | if strVal, err := strconv.Unquote(stringVal); err == nil { 66 | return &pb.TypedValue{ 67 | Value: &pb.TypedValue_StringVal{ 68 | StringVal: strVal, 69 | }, 70 | }, nil 71 | } 72 | 73 | if intVal, err := strconv.ParseInt(stringVal, 10, 64); err == nil { 74 | return &pb.TypedValue{ 75 | Value: &pb.TypedValue_IntVal{ 76 | IntVal: intVal, 77 | }, 78 | }, nil 79 | } 80 | 81 | if floatVal, err := strconv.ParseFloat(stringVal, 32); err == nil { 82 | return &pb.TypedValue{ 83 | Value: &pb.TypedValue_FloatVal{ 84 | FloatVal: float32(floatVal), 85 | }, 86 | }, nil 87 | } 88 | 89 | if boolVal, err := strconv.ParseBool(stringVal); err == nil { 90 | return &pb.TypedValue{ 91 | Value: &pb.TypedValue_BoolVal{ 92 | BoolVal: boolVal, 93 | }, 94 | }, nil 95 | } 96 | 97 | return &pb.TypedValue{ 98 | Value: &pb.TypedValue_StringVal{ 99 | StringVal: stringVal, 100 | }, 101 | }, nil 102 | } 103 | 104 | // GNMIPathEquals checks whether the two given gNMI path equal to each other. 105 | func GNMIPathEquals(actual, expected *pb.Path) bool { 106 | if len(actual.Elem) != len(expected.Elem) { 107 | return false 108 | } 109 | 110 | for i := 0; i < len(actual.Elem); i++ { 111 | actualElem := actual.Elem[i] 112 | expectedElem := expected.Elem[i] 113 | if actualElem.Name != expectedElem.Name || !reflect.DeepEqual(actualElem.Key, expectedElem.Key) { 114 | return false 115 | } 116 | } 117 | 118 | return true 119 | } 120 | 121 | // ValEqual checks whether the given two input value equal. 122 | // It returns error if values are not equal. 123 | func ValEqual(gnmiPath *pb.Path, actual *pb.TypedValue, expected *pb.TypedValue) error { 124 | actualValue := actual.Value 125 | expectedValue := expected.Value 126 | 127 | // JSON value. 128 | actualJsonValue, okA := actualValue.(*pb.TypedValue_JsonIetfVal) 129 | expectedJsonValue, okE := expectedValue.(*pb.TypedValue_JsonIetfVal) 130 | if okA && okE { 131 | var actualJson, expectedJson interface{} 132 | if err := json.Unmarshal(actualJsonValue.JsonIetfVal, &actualJson); err != nil { 133 | return fmt.Errorf("invalid value %v: %v", string(actualJsonValue.JsonIetfVal), err) 134 | } 135 | if err := json.Unmarshal(expectedJsonValue.JsonIetfVal, &expectedJson); err != nil { 136 | return fmt.Errorf("invalid value %v: %v", string(expectedJsonValue.JsonIetfVal), err) 137 | } 138 | if err := jsonValEquals(actualJson, expectedJson); err != nil { 139 | return fmt.Errorf("incorrect json config on %v, actual = %v, expected = %v, detail = %v", gnmiPath, string(actualJsonValue.JsonIetfVal), string(expectedJsonValue.JsonIetfVal), err) 140 | } 141 | return nil 142 | } 143 | 144 | // Other value types. 145 | 146 | // No need to check uint64/int64 type matching. 147 | if uintValue, ok := actualValue.(*pb.TypedValue_UintVal); ok { 148 | actualValue = &pb.TypedValue_IntVal{ 149 | IntVal: int64(uintValue.UintVal), 150 | } 151 | } 152 | if !reflect.DeepEqual(actualValue, expectedValue) { 153 | return fmt.Errorf("incorrect config on %v, actual = %v, expected = %v", gnmiPath, actualValue, expectedValue) 154 | } 155 | return nil 156 | } 157 | 158 | func jsonValEquals(actual, expected interface{}) error { 159 | // Both are JSON array. 160 | actualJSONArr, okA := actual.([]interface{}) 161 | expectedJSONArr, okE := expected.([]interface{}) 162 | if okA && okE { 163 | if len(actualJSONArr) != len(expectedJSONArr) { 164 | return fmt.Errorf("array length no equal, actual = %v, expected = %v", actual, expected) 165 | } 166 | 167 | for _, expectedElem := range expectedJSONArr { 168 | found := false 169 | for _, actualElem := range actualJSONArr { 170 | if err := jsonValEquals(actualElem, actualElem); err == nil { 171 | found = true 172 | break 173 | } 174 | } 175 | if !found { 176 | return fmt.Errorf("not found the expected element %v in array, actual = %v, expected = %v", expectedElem, actual, expected) 177 | } 178 | } 179 | return nil 180 | } 181 | 182 | // Both are JSON blob. 183 | actualJSON, okA := actual.(map[string]interface{}) 184 | expectedJSON, okE := expected.(map[string]interface{}) 185 | if okA && okE { 186 | for key, expectedVal := range expectedJSON { 187 | actualVal, ok := actualJSON[key] 188 | if !ok { 189 | return fmt.Errorf("miss the property %s, expected = %v", key, expectedVal) 190 | } 191 | if err := jsonValEquals(actualVal, expectedVal); err != nil { 192 | return err 193 | } 194 | } 195 | return nil 196 | } 197 | 198 | // Other value types. 199 | if !reflect.DeepEqual(actual, expected) { 200 | return fmt.Errorf("incorrect value, actual = %v, expected = %v", actual, expected) 201 | } 202 | return nil 203 | } 204 | --------------------------------------------------------------------------------