├── .gitignore ├── .travis.yml ├── EXERCISE-1.md ├── EXERCISE-2.md ├── EXERCISE-3.md ├── EXERCISE-4.md ├── EXERCISE-5.md ├── EXERCISE-6.md ├── EXERCISE-7.md ├── EXERCISE-8.md ├── LICENSE ├── Makefile ├── README.md ├── app ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── onosproject │ └── ngsdn │ └── tutorial │ ├── AppConstants.java │ ├── Ipv6RoutingComponent.java │ ├── L2BridgingComponent.java │ ├── MainComponent.java │ ├── NdpReplyComponent.java │ ├── Srv6Component.java │ ├── cli │ ├── Srv6ClearCommand.java │ ├── Srv6InsertCommand.java │ ├── Srv6SidCompleter.java │ └── package-info.java │ ├── common │ ├── FabricDeviceConfig.java │ └── Utils.java │ └── pipeconf │ ├── InterpreterImpl.java │ ├── PipeconfLoader.java │ └── PipelinerImpl.java ├── docker-compose.yml ├── img ├── device-leaf1-details-panel.png ├── onos-gui-pipeconf-leaf1.png ├── routing-ecmp.png ├── srv6-ping-1.png ├── srv6-ping-2.png ├── topo-gtp.png ├── topo-v4.png ├── topo-v6.png └── trellis-features.png ├── mininet ├── flowrule-gtp.json ├── host-cmd ├── netcfg-gtp.json ├── netcfg-sr.json ├── netcfg.json ├── recv-gtp.py ├── send-udp.py ├── topo-gtp.py ├── topo-v4.py └── topo-v6.py ├── p4src ├── main.p4 └── snippets.p4 ├── ptf ├── lib │ ├── __init__.py │ ├── base_test.py │ ├── chassis_config.pb.txt │ ├── convert.py │ ├── helper.py │ ├── port_map.json │ ├── runner.py │ └── start_bmv2.sh ├── run_tests └── tests │ ├── bridging.py │ ├── packetio.py │ ├── routing.py │ └── srv6.py ├── solution ├── app │ └── src │ │ └── main │ │ └── java │ │ └── org │ │ └── onosproject │ │ └── ngsdn │ │ └── tutorial │ │ ├── Ipv6RoutingComponent.java │ │ ├── L2BridgingComponent.java │ │ ├── NdpReplyComponent.java │ │ ├── Srv6Component.java │ │ └── pipeconf │ │ └── InterpreterImpl.java ├── mininet │ ├── flowrule-gtp.json │ ├── netcfg-gtp.json │ └── netcfg-sr.json ├── p4src │ └── main.p4 └── ptf │ └── tests │ ├── bridging.py │ ├── packetio.py │ ├── routing.py │ └── srv6.py ├── util ├── docker │ ├── Makefile │ ├── Makefile.vars │ ├── mvn │ │ └── Dockerfile │ └── stratum_bmv2 │ │ └── Dockerfile ├── gnmi-cli ├── mn-cmd ├── mn-pcap ├── oc-pb-decoder ├── onos-cmd ├── p4rt-sh └── vm │ ├── .gitignore │ ├── README.md │ ├── Vagrantfile │ ├── build-vm.sh │ ├── cleanup.sh │ ├── root-bootstrap.sh │ └── user-bootstrap.sh └── yang └── demo-port.yang /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | tmp/ 3 | p4src/build 4 | app/target 5 | app/src/main/resources/p4info.txt 6 | app/src/main/resources/bmv2.json 7 | ptf/stratum_bmv2.log 8 | ptf/p4rt_write.log 9 | ptf/ptf.log 10 | ptf/ptf.pcap 11 | **/*.iml 12 | **/*.pyc 13 | **/*.bak 14 | **/.classpath 15 | **/.project 16 | **/.settings 17 | **/.factorypath 18 | util/.pipe_cfg 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: python 4 | 5 | services: 6 | - docker 7 | 8 | python: 9 | - "3.5" 10 | 11 | install: 12 | - make deps 13 | 14 | script: 15 | - make check check-sr check-gtp NGSDN_TUTORIAL_SUDO=sudo 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) 2 | curr_dir := $(patsubst %/,%,$(dir $(mkfile_path))) 3 | 4 | include util/docker/Makefile.vars 5 | 6 | onos_url := http://localhost:8181/onos 7 | onos_curl := curl --fail -sSL --user onos:rocks --noproxy localhost 8 | app_name := org.onosproject.ngsdn-tutorial 9 | 10 | NGSDN_TUTORIAL_SUDO ?= 11 | 12 | default: 13 | $(error Please specify a make target (see README.md)) 14 | 15 | _docker_pull_all: 16 | docker pull ${ONOS_IMG}@${ONOS_SHA} 17 | docker tag ${ONOS_IMG}@${ONOS_SHA} ${ONOS_IMG} 18 | docker pull ${P4RT_SH_IMG}@${P4RT_SH_SHA} 19 | docker tag ${P4RT_SH_IMG}@${P4RT_SH_SHA} ${P4RT_SH_IMG} 20 | docker pull ${P4C_IMG}@${P4C_SHA} 21 | docker tag ${P4C_IMG}@${P4C_SHA} ${P4C_IMG} 22 | docker pull ${STRATUM_BMV2_IMG}@${STRATUM_BMV2_SHA} 23 | docker tag ${STRATUM_BMV2_IMG}@${STRATUM_BMV2_SHA} ${STRATUM_BMV2_IMG} 24 | docker pull ${MVN_IMG}@${MVN_SHA} 25 | docker tag ${MVN_IMG}@${MVN_SHA} ${MVN_IMG} 26 | docker pull ${GNMI_CLI_IMG}@${GNMI_CLI_SHA} 27 | docker tag ${GNMI_CLI_IMG}@${GNMI_CLI_SHA} ${GNMI_CLI_IMG} 28 | docker pull ${YANG_IMG}@${YANG_SHA} 29 | docker tag ${YANG_IMG}@${YANG_SHA} ${YANG_IMG} 30 | docker pull ${SSHPASS_IMG}@${SSHPASS_SHA} 31 | docker tag ${SSHPASS_IMG}@${SSHPASS_SHA} ${SSHPASS_IMG} 32 | 33 | deps: _docker_pull_all 34 | 35 | _start: 36 | $(info *** Starting ONOS and Mininet (${NGSDN_TOPO_PY})... ) 37 | @mkdir -p tmp/onos 38 | @NGSDN_TOPO_PY=${NGSDN_TOPO_PY} docker-compose up -d 39 | 40 | start: NGSDN_TOPO_PY := topo-v6.py 41 | start: _start 42 | 43 | start-v4: NGSDN_TOPO_PY := topo-v4.py 44 | start-v4: _start 45 | 46 | start-gtp: NGSDN_TOPO_PY := topo-gtp.py 47 | start-gtp: _start 48 | 49 | stop: 50 | $(info *** Stopping ONOS and Mininet...) 51 | @NGSDN_TOPO_PY=foo docker-compose down -t0 52 | 53 | restart: reset start 54 | 55 | onos-cli: 56 | $(info *** Connecting to the ONOS CLI... password: rocks) 57 | $(info *** Top exit press Ctrl-D) 58 | @ssh -o "UserKnownHostsFile=/dev/null" -o "StrictHostKeyChecking=no" -o LogLevel=ERROR -p 8101 onos@localhost 59 | 60 | onos-log: 61 | docker-compose logs -f onos 62 | 63 | onos-ui: 64 | open ${onos_url}/ui 65 | 66 | mn-cli: 67 | $(info *** Attaching to Mininet CLI...) 68 | $(info *** To detach press Ctrl-D (Mininet will keep running)) 69 | -@docker attach --detach-keys "ctrl-d" $(shell docker-compose ps -q mininet) || echo "*** Detached from Mininet CLI" 70 | 71 | mn-log: 72 | docker logs -f mininet 73 | 74 | _netcfg: 75 | $(info *** Pushing ${NGSDN_NETCFG_JSON} to ONOS...) 76 | ${onos_curl} -X POST -H 'Content-Type:application/json' \ 77 | ${onos_url}/v1/network/configuration -d@./mininet/${NGSDN_NETCFG_JSON} 78 | @echo 79 | 80 | netcfg: NGSDN_NETCFG_JSON := netcfg.json 81 | netcfg: _netcfg 82 | 83 | netcfg-sr: NGSDN_NETCFG_JSON := netcfg-sr.json 84 | netcfg-sr: _netcfg 85 | 86 | netcfg-gtp: NGSDN_NETCFG_JSON := netcfg-gtp.json 87 | netcfg-gtp: _netcfg 88 | 89 | flowrule-gtp: 90 | $(info *** Pushing flowrule-gtp.json to ONOS...) 91 | ${onos_curl} -X POST -H 'Content-Type:application/json' \ 92 | ${onos_url}/v1/flows?appId=rest-api -d@./mininet/flowrule-gtp.json 93 | @echo 94 | 95 | flowrule-clean: 96 | $(info *** Removing all flows installed via REST APIs...) 97 | ${onos_curl} -X DELETE -H 'Content-Type:application/json' \ 98 | ${onos_url}/v1/flows/application/rest-api 99 | @echo 100 | 101 | reset: stop 102 | -$(NGSDN_TUTORIAL_SUDO) rm -rf ./tmp 103 | 104 | clean: 105 | -$(NGSDN_TUTORIAL_SUDO) rm -rf p4src/build 106 | -$(NGSDN_TUTORIAL_SUDO) rm -rf app/target 107 | -$(NGSDN_TUTORIAL_SUDO) rm -rf app/src/main/resources/bmv2.json 108 | -$(NGSDN_TUTORIAL_SUDO) rm -rf app/src/main/resources/p4info.txt 109 | 110 | p4-build: p4src/main.p4 111 | $(info *** Building P4 program...) 112 | @mkdir -p p4src/build 113 | docker run --rm -v ${curr_dir}:/workdir -w /workdir ${P4C_IMG} \ 114 | p4c-bm2-ss --arch v1model -o p4src/build/bmv2.json \ 115 | --p4runtime-files p4src/build/p4info.txt --Wdisable=unsupported \ 116 | p4src/main.p4 117 | @echo "*** P4 program compiled successfully! Output files are in p4src/build" 118 | 119 | p4-test: 120 | @cd ptf && PTF_DOCKER_IMG=$(STRATUM_BMV2_IMG) ./run_tests $(TEST) 121 | 122 | _copy_p4c_out: 123 | $(info *** Copying p4c outputs to app resources...) 124 | @mkdir -p app/src/main/resources 125 | cp -f p4src/build/p4info.txt app/src/main/resources/ 126 | cp -f p4src/build/bmv2.json app/src/main/resources/ 127 | 128 | _mvn_package: 129 | $(info *** Building ONOS app...) 130 | @mkdir -p app/target 131 | @docker run --rm -v ${curr_dir}/app:/mvn-src -w /mvn-src ${MVN_IMG} mvn -o clean package 132 | 133 | app-build: p4-build _copy_p4c_out _mvn_package 134 | $(info *** ONOS app .oar package created succesfully) 135 | @ls -1 app/target/*.oar 136 | 137 | app-install: 138 | $(info *** Installing and activating app in ONOS...) 139 | ${onos_curl} -X POST -HContent-Type:application/octet-stream \ 140 | '${onos_url}/v1/applications?activate=true' \ 141 | --data-binary @app/target/ngsdn-tutorial-1.0-SNAPSHOT.oar 142 | @echo 143 | 144 | app-uninstall: 145 | $(info *** Uninstalling app from ONOS (if present)...) 146 | -${onos_curl} -X DELETE ${onos_url}/v1/applications/${app_name} 147 | @echo 148 | 149 | app-reload: app-uninstall app-install 150 | 151 | yang-tools: 152 | docker run --rm -it -v ${curr_dir}/yang/demo-port.yang:/models/demo-port.yang ${YANG_IMG} 153 | 154 | solution-apply: 155 | mkdir working_copy 156 | cp -r app working_copy/app 157 | cp -r p4src working_copy/p4src 158 | cp -r ptf working_copy/ptf 159 | cp -r mininet working_copy/mininet 160 | rsync -r solution/ ./ 161 | 162 | solution-revert: 163 | test -d working_copy 164 | $(NGSDN_TUTORIAL_SUDO) rm -rf ./app/* 165 | $(NGSDN_TUTORIAL_SUDO) rm -rf ./p4src/* 166 | $(NGSDN_TUTORIAL_SUDO) rm -rf ./ptf/* 167 | $(NGSDN_TUTORIAL_SUDO) rm -rf ./mininet/* 168 | cp -r working_copy/* ./ 169 | $(NGSDN_TUTORIAL_SUDO) rm -rf working_copy/ 170 | 171 | check: 172 | make reset 173 | # P4 starter code and app should compile 174 | make p4-build 175 | make app-build 176 | # Check solution 177 | make solution-apply 178 | make start 179 | make p4-build 180 | make p4-test 181 | make app-build 182 | sleep 30 183 | make app-reload 184 | sleep 10 185 | make netcfg 186 | sleep 10 187 | # The first ping(s) might fail because of a known race condition in the 188 | # L2BridgingComponenet. Ping all hosts. 189 | -util/mn-cmd h1a ping -c 1 2001:1:1::b 190 | util/mn-cmd h1a ping -c 1 2001:1:1::b 191 | -util/mn-cmd h1b ping -c 1 2001:1:1::c 192 | util/mn-cmd h1b ping -c 1 2001:1:1::c 193 | -util/mn-cmd h2 ping -c 1 2001:1:1::b 194 | util/mn-cmd h2 ping -c 1 2001:1:1::b 195 | util/mn-cmd h2 ping -c 1 2001:1:1::a 196 | util/mn-cmd h2 ping -c 1 2001:1:1::c 197 | -util/mn-cmd h3 ping -c 1 2001:1:2::1 198 | util/mn-cmd h3 ping -c 1 2001:1:2::1 199 | util/mn-cmd h3 ping -c 1 2001:1:1::a 200 | util/mn-cmd h3 ping -c 1 2001:1:1::b 201 | util/mn-cmd h3 ping -c 1 2001:1:1::c 202 | -util/mn-cmd h4 ping -c 1 2001:1:2::1 203 | util/mn-cmd h4 ping -c 1 2001:1:2::1 204 | util/mn-cmd h4 ping -c 1 2001:1:1::a 205 | util/mn-cmd h4 ping -c 1 2001:1:1::b 206 | util/mn-cmd h4 ping -c 1 2001:1:1::c 207 | make stop 208 | make solution-revert 209 | 210 | check-sr: 211 | make reset 212 | make start-v4 213 | sleep 45 214 | util/onos-cmd app activate segmentrouting 215 | util/onos-cmd app activate pipelines.fabric 216 | sleep 15 217 | make netcfg-sr 218 | sleep 20 219 | util/mn-cmd h1a ping -c 1 172.16.1.3 220 | util/mn-cmd h1b ping -c 1 172.16.1.3 221 | util/mn-cmd h2 ping -c 1 172.16.2.254 222 | sleep 5 223 | util/mn-cmd h2 ping -c 1 172.16.1.1 224 | util/mn-cmd h2 ping -c 1 172.16.1.2 225 | util/mn-cmd h2 ping -c 1 172.16.1.3 226 | # ping from h3 and h4 should not work without the solution 227 | ! util/mn-cmd h3 ping -c 1 172.16.3.254 228 | ! util/mn-cmd h4 ping -c 1 172.16.4.254 229 | make solution-apply 230 | make netcfg-sr 231 | sleep 20 232 | util/mn-cmd h3 ping -c 1 172.16.3.254 233 | util/mn-cmd h4 ping -c 1 172.16.4.254 234 | sleep 5 235 | util/mn-cmd h3 ping -c 1 172.16.1.1 236 | util/mn-cmd h3 ping -c 1 172.16.1.2 237 | util/mn-cmd h3 ping -c 1 172.16.1.3 238 | util/mn-cmd h3 ping -c 1 172.16.2.1 239 | util/mn-cmd h3 ping -c 1 172.16.4.1 240 | util/mn-cmd h4 ping -c 1 172.16.1.1 241 | util/mn-cmd h4 ping -c 1 172.16.1.2 242 | util/mn-cmd h4 ping -c 1 172.16.1.3 243 | util/mn-cmd h4 ping -c 1 172.16.2.1 244 | make stop 245 | make solution-revert 246 | 247 | check-gtp: 248 | make reset 249 | make start-gtp 250 | sleep 45 251 | util/onos-cmd app activate segmentrouting 252 | util/onos-cmd app activate pipelines.fabric 253 | util/onos-cmd app activate netcfghostprovider 254 | sleep 15 255 | make solution-apply 256 | make netcfg-gtp 257 | sleep 20 258 | util/mn-cmd enodeb ping -c 1 10.0.100.254 259 | util/mn-cmd pdn ping -c 1 10.0.200.254 260 | util/onos-cmd route-add 17.0.0.0/24 10.0.100.1 261 | make flowrule-gtp 262 | # util/mn-cmd requires a TTY because it uses docker -it option 263 | # hence we use screen for putting it in the background 264 | screen -d -m util/mn-cmd pdn /mininet/send-udp.py 265 | util/mn-cmd enodeb /mininet/recv-gtp.py -e 266 | make stop 267 | make solution-revert 268 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next-Gen SDN Tutorial (Advanced) 2 | 3 | Welcome to the Next-Gen SDN tutorial! 4 | 5 | This tutorial is targeted at students and practitioners who want to learn about 6 | the building blocks of the next-generation SDN (NG-SDN) architecture, such as: 7 | 8 | * Data plane programming and control via P4 and P4Runtime 9 | * Configuration via YANG, OpenConfig, and gNMI 10 | * Stratum switch OS 11 | * ONOS SDN controller 12 | 13 | Tutorial sessions are organized around a sequence of hands-on exercises that 14 | show how to build a leaf-spine data center fabric based on IPv6, using P4, 15 | Stratum, and ONOS. Exercises assume an intermediate knowledge of the P4 16 | language, and a basic knowledge of Java and Python. Participants will be 17 | provided with a starter P4 program and ONOS app implementation. Exercises will 18 | focus on concepts such as: 19 | 20 | * Using Stratum APIs (P4Runtime, gNMI, OpenConfig, gNOI) 21 | * Using ONOS with devices programmed with arbitrary P4 programs 22 | * Writing ONOS applications to provide the control plane logic 23 | (bridging, routing, ECMP, etc.) 24 | * Testing using bmv2 in Mininet 25 | * PTF-based P4 unit tests 26 | 27 | ## Basic vs. advanced version 28 | 29 | This tutorial comes in two versions: basic (`master` branch), and advanced 30 | (this branch). 31 | 32 | The basic version contains fewer exercises, and it does not assume prior 33 | knowledge of the P4 language. Instead, it provides a gentle introduction to it. 34 | Check the `master` branch of this repo if you're interested in the basic 35 | version. 36 | 37 | If you're interested in the advanced version, keep reading. 38 | 39 | ## Slides 40 | 41 | Tutorial slides are available online: 42 | 43 | 44 | These slides provide an introduction to the topics covered in the tutorial. We 45 | suggest you look at it before starting to work on the exercises. 46 | 47 | ## System requirements 48 | 49 | If you are taking this tutorial at an event organized by ONF, you should have 50 | received credentials to access the **ONF Cloud Tutorial Platform**, in which 51 | case you can skip this section. Keep reading if you are interested in working on 52 | the exercises on your laptop. 53 | 54 | To facilitate access to the tools required to complete this tutorial, we provide 55 | two options for you to choose from: 56 | 57 | 1. Download a pre-packaged VM with all included; **OR** 58 | 2. Manually install Docker and other dependencies. 59 | 60 | ### Option 1 - Download tutorial VM 61 | 62 | Use the following link to download the VM (4 GB): 63 | * 64 | 65 | The VM is in .ova format and has been created using VirtualBox v5.2.32. To run 66 | the VM you can use any modern virtualization system, although we recommend using 67 | VirtualBox. For instructions on how to get VirtualBox and import the VM, use the 68 | following links: 69 | 70 | * 71 | * 72 | 73 | Alternatively, you can use the scripts in [util/vm](util/vm) to build a VM on 74 | your machine using Vagrant. 75 | 76 | **Recommended VM configuration:** 77 | The current configuration of the VM is 4 GB of RAM and 4 core CPU. These are the 78 | recommended minimum system requirements to complete the exercises. When 79 | imported, the VM takes approx. 8 GB of HDD space. For a smooth experience, we 80 | recommend running the VM on a host system that has at least the double of 81 | resources. 82 | 83 | **VM user credentials:** 84 | Use credentials `sdn`/`rocks` to log in the Ubuntu system. 85 | 86 | ### Option 2 - Manually install Docker and other dependencies 87 | 88 | All exercises can be executed by installing the following dependencies: 89 | 90 | * Docker v1.13.0+ (with docker-compose) 91 | * make 92 | * Python 3 93 | * Bash-like Unix shell 94 | * Wireshark (optional) 95 | 96 | **Note for Windows users**: all scripts have been tested on macOS and Ubuntu. 97 | Although we think they should work on Windows, we have not tested it. For this 98 | reason, we advise Windows users to prefer Option 1. 99 | 100 | ## Get this repo or pull latest changes 101 | 102 | To work on the exercises you will need to clone this repo: 103 | 104 | cd ~ 105 | git clone -b advanced https://github.com/opennetworkinglab/ngsdn-tutorial 106 | 107 | If the `ngsdn-tutorial` directory is already present, make sure to update its 108 | content: 109 | 110 | cd ~/ngsdn-tutorial 111 | git pull origin advanced 112 | 113 | ## Download / upgrade dependencies 114 | 115 | The VM may have shipped with an older version of the dependencies than we would 116 | like to use for the exercises. You can upgrade to the latest version using the 117 | following command: 118 | 119 | cd ~/ngsdn-tutorial 120 | make deps 121 | 122 | This command will download all necessary Docker images (~1.5 GB) allowing you to 123 | work off-line. For this reason, we recommend running this step ahead of the 124 | tutorial, with a reliable Internet connection. 125 | 126 | ## Using an IDE to work on the exercises 127 | 128 | During the exercises you will need to write code in multiple languages such as 129 | P4, Java, and Python. While the exercises do not prescribe the use of any 130 | specific IDE or code editor, the **ONF Cloud Tutorial Platform** provides access 131 | to a web-based version of Visual Studio Code (VS Code). 132 | 133 | If you are using the tutorial VM, you will find the Java IDE [IntelliJ IDEA 134 | Community Edition](https://www.jetbrains.com/idea/), already pre-loaded with 135 | plugins for P4 syntax highlighting and Python development. We suggest using 136 | IntelliJ IDEA especially when working on the ONOS app, as it provides code 137 | completion for all ONOS APIs. 138 | 139 | ## Repo structure 140 | 141 | This repo is structured as follows: 142 | 143 | * `p4src/` P4 implementation 144 | * `yang/` Yang model used in exercise 2 145 | * `app/` custom ONOS app Java implementation 146 | * `mininet/` Mininet script to emulate a 2x2 leaf-spine fabric topology of 147 | `stratum_bmv2` devices 148 | * `util/` Utility scripts 149 | * `ptf/` P4 data plane unit tests based on Packet Test Framework (PTF) 150 | 151 | ## Tutorial commands 152 | 153 | To facilitate working on the exercises, we provide a set of make-based commands 154 | to control the different aspects of the tutorial. Commands will be introduced in 155 | the exercises, here's a quick reference: 156 | 157 | | Make command | Description | 158 | |---------------------|------------------------------------------------------- | 159 | | `make deps` | Pull and build all required dependencies | 160 | | `make p4-build` | Build P4 program | 161 | | `make p4-test` | Run PTF tests | 162 | | `make start` | Start Mininet and ONOS containers | 163 | | `make stop` | Stop all containers | 164 | | `make restart` | Restart containers clearing any previous state | 165 | | `make onos-cli` | Access the ONOS CLI (password: `rocks`, Ctrl-D to exit)| 166 | | `make onos-log` | Show the ONOS log | 167 | | `make mn-cli` | Access the Mininet CLI (Ctrl-D to exit) | 168 | | `make mn-log` | Show the Mininet log (i.e., the CLI output) | 169 | | `make app-build` | Build custom ONOS app | 170 | | `make app-reload` | Install and activate the ONOS app | 171 | | `make netcfg` | Push netcfg.json file (network config) to ONOS | 172 | 173 | ## Exercises 174 | 175 | Click on the exercise name to see the instructions: 176 | 177 | 1. [P4Runtime basics](./EXERCISE-1.md) 178 | 2. [Yang, OpenConfig, and gNMI basics](./EXERCISE-2.md) 179 | 3. [Using ONOS as the control plane](./EXERCISE-3.md) 180 | 4. [Enabling ONOS built-in services](./EXERCISE-4.md) 181 | 5. [Implementing IPv6 routing with ECMP](./EXERCISE-5.md) 182 | 6. [Implementing SRv6](./EXERCISE-6.md) 183 | 7. [Trellis Basics](./EXERCISE-7.md) 184 | 8. [GTP termination with fabric.p4](./EXERCISE-8.md) 185 | 186 | ## Solutions 187 | 188 | You can find solutions for each exercise in the [solution](solution) directory. 189 | Feel free to compare your solution to the reference one whenever you feel stuck. 190 | 191 | [![Build Status](https://travis-ci.org/opennetworkinglab/ngsdn-tutorial.svg?branch=advanced)](https://travis-ci.org/opennetworkinglab/ngsdn-tutorial) 192 | -------------------------------------------------------------------------------- /app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 20 | 4.0.0 21 | 22 | 23 | org.onosproject 24 | onos-dependencies 25 | 2.2.2 26 | 27 | 28 | org.onosproject 29 | ngsdn-tutorial 30 | 1.0-SNAPSHOT 31 | bundle 32 | 33 | NG-SDN tutorial app 34 | http://www.onosproject.org 35 | 36 | 37 | org.onosproject.ngsdn-tutorial 38 | NG-SDN Tutorial App 39 | https://www.onosproject.org 40 | Traffic Steering 41 | https://www.onosproject.org 42 | 43 | Provides IPv6 routing capabilities to a leaf-spine network of 44 | Stratum switches 45 | 46 | 47 | 48 | 49 | 50 | org.onosproject 51 | onos-api 52 | ${onos.version} 53 | provided 54 | 55 | 56 | 57 | org.onosproject 58 | onos-protocols-p4runtime-model 59 | ${onos.version} 60 | provided 61 | 62 | 63 | 64 | org.onosproject 65 | onos-protocols-p4runtime-api 66 | ${onos.version} 67 | provided 68 | 69 | 70 | 71 | org.onosproject 72 | onos-protocols-grpc-api 73 | ${onos.version} 74 | provided 75 | 76 | 77 | 78 | org.onosproject 79 | onlab-osgi 80 | ${onos.version} 81 | provided 82 | 83 | 84 | 85 | org.onosproject 86 | onlab-misc 87 | ${onos.version} 88 | provided 89 | 90 | 91 | 92 | org.onosproject 93 | onos-cli 94 | ${onos.version} 95 | provided 96 | 97 | 98 | 99 | org.slf4j 100 | slf4j-api 101 | provided 102 | 103 | 104 | 105 | com.google.guava 106 | guava 107 | provided 108 | 109 | 110 | 111 | com.fasterxml.jackson.core 112 | jackson-databind 113 | provided 114 | 115 | 116 | 117 | junit 118 | junit 119 | test 120 | 121 | 122 | 123 | org.onosproject 124 | onos-api 125 | ${onos.version} 126 | test 127 | tests 128 | 129 | 130 | 131 | org.osgi 132 | org.osgi.service.component.annotations 133 | provided 134 | 135 | 136 | 137 | org.osgi 138 | org.osgi.core 139 | provided 140 | 141 | 142 | 143 | org.apache.karaf.shell 144 | org.apache.karaf.shell.console 145 | provided 146 | 147 | 148 | 149 | 150 | 151 | 152 | org.apache.felix 153 | maven-bundle-plugin 154 | 155 | 156 | 157 | org.onosproject.ngsdn.tutorial.cli 158 | 159 | 160 | 161 | 162 | 163 | org.onosproject 164 | onos-maven-plugin 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/AppConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial; 18 | 19 | import org.onosproject.net.pi.model.PiPipeconfId; 20 | 21 | public class AppConstants { 22 | 23 | public static final String APP_NAME = "org.onosproject.ngsdn-tutorial"; 24 | public static final PiPipeconfId PIPECONF_ID = new PiPipeconfId("org.onosproject.ngsdn-tutorial"); 25 | 26 | public static final int DEFAULT_FLOW_RULE_PRIORITY = 10; 27 | public static final int INITIAL_SETUP_DELAY = 2; // Seconds. 28 | public static final int CLEAN_UP_DELAY = 2000; // milliseconds 29 | public static final int DEFAULT_CLEAN_UP_RETRY_TIMES = 10; 30 | 31 | public static final int CPU_PORT_ID = 255; 32 | public static final int CPU_CLONE_SESSION_ID = 99; 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/MainComponent.java: -------------------------------------------------------------------------------- 1 | package org.onosproject.ngsdn.tutorial; 2 | 3 | import com.google.common.collect.Lists; 4 | import org.onlab.util.SharedScheduledExecutors; 5 | import org.onosproject.cfg.ComponentConfigService; 6 | import org.onosproject.core.ApplicationId; 7 | import org.onosproject.core.CoreService; 8 | import org.onosproject.net.Device; 9 | import org.onosproject.net.DeviceId; 10 | import org.onosproject.net.config.ConfigFactory; 11 | import org.onosproject.net.config.NetworkConfigRegistry; 12 | import org.onosproject.net.config.basics.SubjectFactories; 13 | import org.onosproject.net.device.DeviceService; 14 | import org.onosproject.net.flow.FlowRule; 15 | import org.onosproject.net.flow.FlowRuleService; 16 | import org.onosproject.net.group.Group; 17 | import org.onosproject.net.group.GroupService; 18 | import org.osgi.service.component.annotations.Activate; 19 | import org.osgi.service.component.annotations.Component; 20 | import org.osgi.service.component.annotations.Deactivate; 21 | import org.osgi.service.component.annotations.Reference; 22 | import org.osgi.service.component.annotations.ReferenceCardinality; 23 | import org.onosproject.ngsdn.tutorial.common.FabricDeviceConfig; 24 | import org.onosproject.ngsdn.tutorial.pipeconf.PipeconfLoader; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.util.Collection; 29 | import java.util.concurrent.ExecutorService; 30 | import java.util.concurrent.Executors; 31 | import java.util.concurrent.TimeUnit; 32 | 33 | import static org.onosproject.ngsdn.tutorial.AppConstants.APP_NAME; 34 | import static org.onosproject.ngsdn.tutorial.AppConstants.CLEAN_UP_DELAY; 35 | import static org.onosproject.ngsdn.tutorial.AppConstants.DEFAULT_CLEAN_UP_RETRY_TIMES; 36 | import static org.onosproject.ngsdn.tutorial.common.Utils.sleep; 37 | 38 | /** 39 | * A component which among other things registers the fabricDeviceConfig to the 40 | * netcfg subsystem. 41 | */ 42 | @Component(immediate = true, service = MainComponent.class) 43 | public class MainComponent { 44 | 45 | private static final Logger log = 46 | LoggerFactory.getLogger(MainComponent.class.getName()); 47 | 48 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 49 | private CoreService coreService; 50 | 51 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 52 | //Force activation of this component after the pipeconf has been registered. 53 | @SuppressWarnings("unused") 54 | protected PipeconfLoader pipeconfLoader; 55 | 56 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 57 | protected NetworkConfigRegistry configRegistry; 58 | 59 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 60 | private GroupService groupService; 61 | 62 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 63 | private DeviceService deviceService; 64 | 65 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 66 | private FlowRuleService flowRuleService; 67 | 68 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 69 | private ComponentConfigService compCfgService; 70 | 71 | private final ConfigFactory fabricConfigFactory = 72 | new ConfigFactory( 73 | SubjectFactories.DEVICE_SUBJECT_FACTORY, FabricDeviceConfig.class, FabricDeviceConfig.CONFIG_KEY) { 74 | @Override 75 | public FabricDeviceConfig createConfig() { 76 | return new FabricDeviceConfig(); 77 | } 78 | }; 79 | 80 | private ApplicationId appId; 81 | 82 | // For the sake of simplicity and to facilitate reading logs, use a 83 | // single-thread executor to serialize all configuration tasks. 84 | private final ExecutorService executorService = Executors.newSingleThreadExecutor(); 85 | 86 | @Activate 87 | protected void activate() { 88 | appId = coreService.registerApplication(APP_NAME); 89 | 90 | // Wait to remove flow and groups from previous executions. 91 | waitPreviousCleanup(); 92 | 93 | compCfgService.preSetProperty("org.onosproject.net.flow.impl.FlowRuleManager", 94 | "fallbackFlowPollFrequency", "4", false); 95 | compCfgService.preSetProperty("org.onosproject.net.group.impl.GroupManager", 96 | "fallbackGroupPollFrequency", "3", false); 97 | compCfgService.preSetProperty("org.onosproject.provider.host.impl.HostLocationProvider", 98 | "requestIpv6ND", "true", false); 99 | compCfgService.preSetProperty("org.onosproject.provider.lldp.impl.LldpLinkProvider", 100 | "useBddp", "false", false); 101 | 102 | configRegistry.registerConfigFactory(fabricConfigFactory); 103 | log.info("Started"); 104 | } 105 | 106 | @Deactivate 107 | protected void deactivate() { 108 | configRegistry.unregisterConfigFactory(fabricConfigFactory); 109 | 110 | cleanUp(); 111 | 112 | log.info("Stopped"); 113 | } 114 | 115 | /** 116 | * Returns the application ID. 117 | * 118 | * @return application ID 119 | */ 120 | ApplicationId getAppId() { 121 | return appId; 122 | } 123 | 124 | /** 125 | * Returns the executor service managed by this component. 126 | * 127 | * @return executor service 128 | */ 129 | public ExecutorService getExecutorService() { 130 | return executorService; 131 | } 132 | 133 | /** 134 | * Schedules a task for the future using the executor service managed by 135 | * this component. 136 | * 137 | * @param task task runnable 138 | * @param delaySeconds delay in seconds 139 | */ 140 | public void scheduleTask(Runnable task, int delaySeconds) { 141 | SharedScheduledExecutors.newTimeout( 142 | () -> executorService.execute(task), 143 | delaySeconds, TimeUnit.SECONDS); 144 | } 145 | 146 | /** 147 | * Triggers clean up of flows and groups from this app, returns false if no 148 | * flows or groups were found, true otherwise. 149 | * 150 | * @return false if no flows or groups were found, true otherwise 151 | */ 152 | private boolean cleanUp() { 153 | Collection flows = Lists.newArrayList( 154 | flowRuleService.getFlowEntriesById(appId).iterator()); 155 | 156 | Collection groups = Lists.newArrayList(); 157 | for (Device device : deviceService.getAvailableDevices()) { 158 | groupService.getGroups(device.id(), appId).forEach(groups::add); 159 | } 160 | 161 | if (flows.isEmpty() && groups.isEmpty()) { 162 | return false; 163 | } 164 | 165 | flows.forEach(flowRuleService::removeFlowRules); 166 | if (!groups.isEmpty()) { 167 | // Wait for flows to be removed in case those depend on groups. 168 | sleep(1000); 169 | groups.forEach(g -> groupService.removeGroup( 170 | g.deviceId(), g.appCookie(), g.appId())); 171 | } 172 | 173 | return true; 174 | } 175 | 176 | private void waitPreviousCleanup() { 177 | int retry = DEFAULT_CLEAN_UP_RETRY_TIMES; 178 | while (retry != 0) { 179 | 180 | if (!cleanUp()) { 181 | return; 182 | } 183 | 184 | log.info("Waiting to remove flows and groups from " + 185 | "previous execution of {}...", 186 | appId.name()); 187 | 188 | sleep(CLEAN_UP_DELAY); 189 | 190 | --retry; 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/cli/Srv6ClearCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onosproject.ngsdn.tutorial.cli; 17 | 18 | import org.apache.karaf.shell.api.action.Argument; 19 | import org.apache.karaf.shell.api.action.Command; 20 | import org.apache.karaf.shell.api.action.Completion; 21 | import org.apache.karaf.shell.api.action.lifecycle.Service; 22 | import org.onosproject.cli.AbstractShellCommand; 23 | import org.onosproject.cli.net.DeviceIdCompleter; 24 | import org.onosproject.net.Device; 25 | import org.onosproject.net.DeviceId; 26 | import org.onosproject.net.device.DeviceService; 27 | import org.onosproject.ngsdn.tutorial.Srv6Component; 28 | 29 | /** 30 | * SRv6 Transit Clear Command 31 | */ 32 | @Service 33 | @Command(scope = "onos", name = "srv6-clear", 34 | description = "Clears all t_insert rules from the SRv6 Transit table") 35 | public class Srv6ClearCommand extends AbstractShellCommand { 36 | 37 | @Argument(index = 0, name = "uri", description = "Device ID", 38 | required = true, multiValued = false) 39 | @Completion(DeviceIdCompleter.class) 40 | String uri = null; 41 | 42 | @Override 43 | protected void doExecute() { 44 | DeviceService deviceService = get(DeviceService.class); 45 | Srv6Component app = get(Srv6Component.class); 46 | 47 | Device device = deviceService.getDevice(DeviceId.deviceId(uri)); 48 | if (device == null) { 49 | print("Device \"%s\" is not found", uri); 50 | return; 51 | } 52 | app.clearSrv6InsertRules(device.id()); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/cli/Srv6InsertCommand.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onosproject.ngsdn.tutorial.cli; 17 | 18 | import org.apache.karaf.shell.api.action.Argument; 19 | import org.apache.karaf.shell.api.action.Command; 20 | import org.apache.karaf.shell.api.action.Completion; 21 | import org.apache.karaf.shell.api.action.lifecycle.Service; 22 | import org.onlab.packet.Ip6Address; 23 | import org.onlab.packet.IpAddress; 24 | import org.onosproject.cli.AbstractShellCommand; 25 | import org.onosproject.cli.net.DeviceIdCompleter; 26 | import org.onosproject.net.Device; 27 | import org.onosproject.net.DeviceId; 28 | import org.onosproject.net.device.DeviceService; 29 | import org.onosproject.ngsdn.tutorial.Srv6Component; 30 | 31 | import java.util.List; 32 | import java.util.stream.Collectors; 33 | 34 | /** 35 | * SRv6 Transit Insert Command 36 | */ 37 | @Service 38 | @Command(scope = "onos", name = "srv6-insert", 39 | description = "Insert a t_insert rule into the SRv6 Transit table") 40 | public class Srv6InsertCommand extends AbstractShellCommand { 41 | 42 | @Argument(index = 0, name = "uri", description = "Device ID", 43 | required = true, multiValued = false) 44 | @Completion(DeviceIdCompleter.class) 45 | String uri = null; 46 | 47 | @Argument(index = 1, name = "segments", 48 | description = "SRv6 Segments (space separated list); last segment is target IP address", 49 | required = false, multiValued = true) 50 | @Completion(Srv6SidCompleter.class) 51 | List segments = null; 52 | 53 | @Override 54 | protected void doExecute() { 55 | DeviceService deviceService = get(DeviceService.class); 56 | Srv6Component app = get(Srv6Component.class); 57 | 58 | Device device = deviceService.getDevice(DeviceId.deviceId(uri)); 59 | if (device == null) { 60 | print("Device \"%s\" is not found", uri); 61 | return; 62 | } 63 | if (segments.size() == 0) { 64 | print("No segments listed"); 65 | return; 66 | } 67 | List sids = segments.stream() 68 | .map(Ip6Address::valueOf) 69 | .collect(Collectors.toList()); 70 | Ip6Address destIp = sids.get(sids.size() - 1); 71 | 72 | print("Installing path on device %s: %s", 73 | uri, sids.stream() 74 | .map(IpAddress::toString) 75 | .collect(Collectors.joining(", "))); 76 | app.insertSrv6InsertRule(device.id(), destIp, 128, sids); 77 | 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/cli/Srv6SidCompleter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.onosproject.ngsdn.tutorial.cli; 17 | 18 | import org.apache.karaf.shell.api.action.lifecycle.Service; 19 | import org.apache.karaf.shell.api.console.CommandLine; 20 | import org.apache.karaf.shell.api.console.Completer; 21 | import org.apache.karaf.shell.api.console.Session; 22 | import org.apache.karaf.shell.support.completers.StringsCompleter; 23 | import org.onosproject.cli.AbstractShellCommand; 24 | import org.onosproject.net.config.NetworkConfigService; 25 | import org.onosproject.net.device.DeviceService; 26 | import org.onosproject.ngsdn.tutorial.common.FabricDeviceConfig; 27 | 28 | import java.util.List; 29 | import java.util.Objects; 30 | import java.util.SortedSet; 31 | 32 | import static com.google.common.collect.Streams.stream; 33 | 34 | /** 35 | * Completer for SIDs based on device config. 36 | */ 37 | @Service 38 | public class Srv6SidCompleter implements Completer { 39 | 40 | @Override 41 | public int complete(Session session, CommandLine commandLine, List candidates) { 42 | DeviceService deviceService = AbstractShellCommand.get(DeviceService.class); 43 | NetworkConfigService netCfgService = AbstractShellCommand.get(NetworkConfigService.class); 44 | 45 | // Delegate string completer 46 | StringsCompleter delegate = new StringsCompleter(); 47 | SortedSet strings = delegate.getStrings(); 48 | 49 | stream(deviceService.getDevices()) 50 | .map(d -> netCfgService.getConfig(d.id(), FabricDeviceConfig.class)) 51 | .filter(Objects::nonNull) 52 | .map(FabricDeviceConfig::mySid) 53 | .filter(Objects::nonNull) 54 | .forEach(sid -> strings.add(sid.toString())); 55 | 56 | // Now let the completer do the work for figuring out what to offer. 57 | return delegate.complete(session, commandLine, candidates); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/cli/package-info.java: -------------------------------------------------------------------------------- 1 | package org.onosproject.ngsdn.tutorial.cli; -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/common/FabricDeviceConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.common; 18 | 19 | import org.onlab.packet.Ip6Address; 20 | import org.onlab.packet.MacAddress; 21 | import org.onosproject.net.DeviceId; 22 | import org.onosproject.net.config.Config; 23 | 24 | /** 25 | * Device configuration object for the IPv6 fabric tutorial application. 26 | */ 27 | public class FabricDeviceConfig extends Config { 28 | 29 | public static final String CONFIG_KEY = "fabricDeviceConfig"; 30 | private static final String MY_STATION_MAC = "myStationMac"; 31 | private static final String MY_SID = "mySid"; 32 | private static final String IS_SPINE = "isSpine"; 33 | 34 | @Override 35 | public boolean isValid() { 36 | return hasOnlyFields(MY_STATION_MAC, MY_SID, IS_SPINE) && 37 | myStationMac() != null && 38 | mySid() != null; 39 | } 40 | 41 | /** 42 | * Gets the MAC address of the switch. 43 | * 44 | * @return MAC address of the switch. Or null if not configured. 45 | */ 46 | public MacAddress myStationMac() { 47 | String mac = get(MY_STATION_MAC, null); 48 | return mac != null ? MacAddress.valueOf(mac) : null; 49 | } 50 | 51 | /** 52 | * Gets the SRv6 segment ID (SID) of the switch. 53 | * 54 | * @return IP address of the router. Or null if not configured. 55 | */ 56 | public Ip6Address mySid() { 57 | String ip = get(MY_SID, null); 58 | return ip != null ? Ip6Address.valueOf(ip) : null; 59 | } 60 | 61 | /** 62 | * Checks if the switch is a spine switch. 63 | * 64 | * @return true if the switch is a spine switch. false if the switch is not 65 | * a spine switch, or if the value is not configured. 66 | */ 67 | public boolean isSpine() { 68 | String isSpine = get(IS_SPINE, null); 69 | return isSpine != null && Boolean.valueOf(isSpine); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/common/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.common; 18 | 19 | import org.onosproject.core.ApplicationId; 20 | import org.onosproject.net.DeviceId; 21 | import org.onosproject.net.PortNumber; 22 | import org.onosproject.net.flow.DefaultFlowRule; 23 | import org.onosproject.net.flow.DefaultTrafficSelector; 24 | import org.onosproject.net.flow.DefaultTrafficTreatment; 25 | import org.onosproject.net.flow.FlowRule; 26 | import org.onosproject.net.flow.criteria.PiCriterion; 27 | import org.onosproject.net.group.DefaultGroupBucket; 28 | import org.onosproject.net.group.DefaultGroupDescription; 29 | import org.onosproject.net.group.DefaultGroupKey; 30 | import org.onosproject.net.group.GroupBucket; 31 | import org.onosproject.net.group.GroupBuckets; 32 | import org.onosproject.net.group.GroupDescription; 33 | import org.onosproject.net.group.GroupKey; 34 | import org.onosproject.net.pi.model.PiActionProfileId; 35 | import org.onosproject.net.pi.model.PiTableId; 36 | import org.onosproject.net.pi.runtime.PiAction; 37 | import org.onosproject.net.pi.runtime.PiGroupKey; 38 | import org.onosproject.net.pi.runtime.PiTableAction; 39 | import org.slf4j.Logger; 40 | import org.slf4j.LoggerFactory; 41 | 42 | import java.nio.ByteBuffer; 43 | import java.util.Collection; 44 | import java.util.List; 45 | import java.util.stream.Collectors; 46 | 47 | import static com.google.common.base.Preconditions.checkArgument; 48 | import static com.google.common.base.Preconditions.checkNotNull; 49 | import static org.onosproject.net.group.DefaultGroupBucket.createAllGroupBucket; 50 | import static org.onosproject.net.group.DefaultGroupBucket.createCloneGroupBucket; 51 | import static org.onosproject.ngsdn.tutorial.AppConstants.DEFAULT_FLOW_RULE_PRIORITY; 52 | 53 | public final class Utils { 54 | 55 | private static final Logger log = LoggerFactory.getLogger(Utils.class); 56 | 57 | public static GroupDescription buildMulticastGroup( 58 | ApplicationId appId, 59 | DeviceId deviceId, 60 | int groupId, 61 | Collection ports) { 62 | return buildReplicationGroup(appId, deviceId, groupId, ports, false); 63 | } 64 | 65 | public static GroupDescription buildCloneGroup( 66 | ApplicationId appId, 67 | DeviceId deviceId, 68 | int groupId, 69 | Collection ports) { 70 | return buildReplicationGroup(appId, deviceId, groupId, ports, true); 71 | } 72 | 73 | private static GroupDescription buildReplicationGroup( 74 | ApplicationId appId, 75 | DeviceId deviceId, 76 | int groupId, 77 | Collection ports, 78 | boolean isClone) { 79 | 80 | checkNotNull(deviceId); 81 | checkNotNull(appId); 82 | checkArgument(!ports.isEmpty()); 83 | 84 | final GroupKey groupKey = new DefaultGroupKey( 85 | ByteBuffer.allocate(4).putInt(groupId).array()); 86 | 87 | final List bucketList = ports.stream() 88 | .map(p -> DefaultTrafficTreatment.builder() 89 | .setOutput(p).build()) 90 | .map(t -> isClone ? createCloneGroupBucket(t) 91 | : createAllGroupBucket(t)) 92 | .collect(Collectors.toList()); 93 | 94 | return new DefaultGroupDescription( 95 | deviceId, 96 | isClone ? GroupDescription.Type.CLONE : GroupDescription.Type.ALL, 97 | new GroupBuckets(bucketList), 98 | groupKey, groupId, appId); 99 | } 100 | 101 | public static FlowRule buildFlowRule(DeviceId switchId, ApplicationId appId, 102 | String tableId, PiCriterion piCriterion, 103 | PiTableAction piAction) { 104 | return DefaultFlowRule.builder() 105 | .forDevice(switchId) 106 | .forTable(PiTableId.of(tableId)) 107 | .fromApp(appId) 108 | .withPriority(DEFAULT_FLOW_RULE_PRIORITY) 109 | .makePermanent() 110 | .withSelector(DefaultTrafficSelector.builder() 111 | .matchPi(piCriterion).build()) 112 | .withTreatment(DefaultTrafficTreatment.builder() 113 | .piTableAction(piAction).build()) 114 | .build(); 115 | } 116 | 117 | public static GroupDescription buildSelectGroup(DeviceId deviceId, 118 | String tableId, 119 | String actionProfileId, 120 | int groupId, 121 | Collection actions, 122 | ApplicationId appId) { 123 | 124 | final GroupKey groupKey = new PiGroupKey( 125 | PiTableId.of(tableId), PiActionProfileId.of(actionProfileId), groupId); 126 | final List buckets = actions.stream() 127 | .map(action -> DefaultTrafficTreatment.builder() 128 | .piTableAction(action).build()) 129 | .map(DefaultGroupBucket::createSelectGroupBucket) 130 | .collect(Collectors.toList()); 131 | return new DefaultGroupDescription( 132 | deviceId, 133 | GroupDescription.Type.SELECT, 134 | new GroupBuckets(buckets), 135 | groupKey, 136 | groupId, 137 | appId); 138 | } 139 | 140 | public static void sleep(int millis) { 141 | try { 142 | Thread.sleep(millis); 143 | } catch (InterruptedException e) { 144 | log.error("Interrupted!", e); 145 | Thread.currentThread().interrupt(); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/pipeconf/InterpreterImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.pipeconf; 18 | 19 | import com.google.common.collect.ImmutableList; 20 | import com.google.common.collect.ImmutableMap; 21 | import org.onlab.packet.DeserializationException; 22 | import org.onlab.packet.Ethernet; 23 | import org.onlab.util.ImmutableByteSequence; 24 | import org.onosproject.net.ConnectPoint; 25 | import org.onosproject.net.DeviceId; 26 | import org.onosproject.net.Port; 27 | import org.onosproject.net.PortNumber; 28 | import org.onosproject.net.device.DeviceService; 29 | import org.onosproject.net.driver.AbstractHandlerBehaviour; 30 | import org.onosproject.net.flow.TrafficTreatment; 31 | import org.onosproject.net.flow.criteria.Criterion; 32 | import org.onosproject.net.packet.DefaultInboundPacket; 33 | import org.onosproject.net.packet.InboundPacket; 34 | import org.onosproject.net.packet.OutboundPacket; 35 | import org.onosproject.net.pi.model.PiMatchFieldId; 36 | import org.onosproject.net.pi.model.PiPacketMetadataId; 37 | import org.onosproject.net.pi.model.PiPipelineInterpreter; 38 | import org.onosproject.net.pi.model.PiTableId; 39 | import org.onosproject.net.pi.runtime.PiAction; 40 | import org.onosproject.net.pi.runtime.PiPacketMetadata; 41 | import org.onosproject.net.pi.runtime.PiPacketOperation; 42 | 43 | import java.nio.ByteBuffer; 44 | import java.util.Collection; 45 | import java.util.List; 46 | import java.util.Map; 47 | import java.util.Optional; 48 | 49 | import static java.lang.String.format; 50 | import static java.util.stream.Collectors.toList; 51 | import static org.onlab.util.ImmutableByteSequence.copyFrom; 52 | import static org.onosproject.net.PortNumber.CONTROLLER; 53 | import static org.onosproject.net.PortNumber.FLOOD; 54 | import static org.onosproject.net.flow.instructions.Instruction.Type.OUTPUT; 55 | import static org.onosproject.net.flow.instructions.Instructions.OutputInstruction; 56 | import static org.onosproject.net.pi.model.PiPacketOperationType.PACKET_OUT; 57 | import static org.onosproject.ngsdn.tutorial.AppConstants.CPU_PORT_ID; 58 | 59 | 60 | /** 61 | * Interpreter implementation. 62 | */ 63 | public class InterpreterImpl extends AbstractHandlerBehaviour 64 | implements PiPipelineInterpreter { 65 | 66 | 67 | // From v1model.p4 68 | private static final int V1MODEL_PORT_BITWIDTH = 9; 69 | 70 | // From P4Info. 71 | private static final Map CRITERION_MAP = 72 | new ImmutableMap.Builder() 73 | .put(Criterion.Type.IN_PORT, "standard_metadata.ingress_port") 74 | .put(Criterion.Type.ETH_DST, "hdr.ethernet.dst_addr") 75 | .put(Criterion.Type.ETH_SRC, "hdr.ethernet.src_addr") 76 | .put(Criterion.Type.ETH_TYPE, "hdr.ethernet.ether_type") 77 | .put(Criterion.Type.IPV6_DST, "hdr.ipv6.dst_addr") 78 | .put(Criterion.Type.IP_PROTO, "local_metadata.ip_proto") 79 | .put(Criterion.Type.ICMPV4_TYPE, "local_metadata.icmp_type") 80 | .put(Criterion.Type.ICMPV6_TYPE, "local_metadata.icmp_type") 81 | .build(); 82 | 83 | /** 84 | * Returns a collection of PI packet operations populated with metadata 85 | * specific for this pipeconf and equivalent to the given ONOS 86 | * OutboundPacket instance. 87 | * 88 | * @param packet ONOS OutboundPacket 89 | * @return collection of PI packet operations 90 | * @throws PiInterpreterException if the packet treatments cannot be 91 | * executed by this pipeline 92 | */ 93 | @Override 94 | public Collection mapOutboundPacket(OutboundPacket packet) 95 | throws PiInterpreterException { 96 | TrafficTreatment treatment = packet.treatment(); 97 | 98 | // Packet-out in main.p4 supports only setting the output port, 99 | // i.e. we only understand OUTPUT instructions. 100 | List outInstructions = treatment 101 | .allInstructions() 102 | .stream() 103 | .filter(i -> i.type().equals(OUTPUT)) 104 | .map(i -> (OutputInstruction) i) 105 | .collect(toList()); 106 | 107 | if (treatment.allInstructions().size() != outInstructions.size()) { 108 | // There are other instructions that are not of type OUTPUT. 109 | throw new PiInterpreterException("Treatment not supported: " + treatment); 110 | } 111 | 112 | ImmutableList.Builder builder = ImmutableList.builder(); 113 | for (OutputInstruction outInst : outInstructions) { 114 | if (outInst.port().isLogical() && !outInst.port().equals(FLOOD)) { 115 | throw new PiInterpreterException(format( 116 | "Packet-out on logical port '%s' not supported", 117 | outInst.port())); 118 | } else if (outInst.port().equals(FLOOD)) { 119 | // To emulate flooding, we create a packet-out operation for 120 | // each switch port. 121 | final DeviceService deviceService = handler().get(DeviceService.class); 122 | for (Port port : deviceService.getPorts(packet.sendThrough())) { 123 | builder.add(buildPacketOut(packet.data(), port.number().toLong())); 124 | } 125 | } else { 126 | // Create only one packet-out for the given OUTPUT instruction. 127 | builder.add(buildPacketOut(packet.data(), outInst.port().toLong())); 128 | } 129 | } 130 | return builder.build(); 131 | } 132 | 133 | /** 134 | * Builds a pipeconf-specific packet-out instance with the given payload and 135 | * egress port. 136 | * 137 | * @param pktData packet payload 138 | * @param portNumber egress port 139 | * @return packet-out 140 | * @throws PiInterpreterException if packet-out cannot be built 141 | */ 142 | private PiPacketOperation buildPacketOut(ByteBuffer pktData, long portNumber) 143 | throws PiInterpreterException { 144 | 145 | // Make sure port number can fit in v1model port metadata bitwidth. 146 | final ImmutableByteSequence portBytes; 147 | try { 148 | portBytes = copyFrom(portNumber).fit(V1MODEL_PORT_BITWIDTH); 149 | } catch (ImmutableByteSequence.ByteSequenceTrimException e) { 150 | throw new PiInterpreterException(format( 151 | "Port number %d too big, %s", portNumber, e.getMessage())); 152 | } 153 | 154 | // Create metadata instance for egress port. 155 | // *** TODO EXERCISE 4: modify metadata names to match P4 program 156 | // ---- START SOLUTION ---- 157 | final String outPortMetadataName = "ADD HERE METADATA NAME FOR THE EGRESS PORT"; 158 | // ---- END SOLUTION ---- 159 | final PiPacketMetadata outPortMetadata = PiPacketMetadata.builder() 160 | .withId(PiPacketMetadataId.of(outPortMetadataName)) 161 | .withValue(portBytes) 162 | .build(); 163 | 164 | // Build packet out. 165 | return PiPacketOperation.builder() 166 | .withType(PACKET_OUT) 167 | .withData(copyFrom(pktData)) 168 | .withMetadata(outPortMetadata) 169 | .build(); 170 | } 171 | 172 | /** 173 | * Returns an ONS InboundPacket equivalent to the given pipeconf-specific 174 | * packet-in operation. 175 | * 176 | * @param packetIn packet operation 177 | * @param deviceId ID of the device that originated the packet-in 178 | * @return inbound packet 179 | * @throws PiInterpreterException if the packet operation cannot be mapped 180 | * to an inbound packet 181 | */ 182 | @Override 183 | public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId) 184 | throws PiInterpreterException { 185 | 186 | // Find the ingress_port metadata. 187 | // *** TODO EXERCISE 4: modify metadata names to match P4Info 188 | // ---- START SOLUTION ---- 189 | final String inportMetadataName = "ADD HERE METADATA NAME FOR THE INGRESS PORT"; 190 | // ---- END SOLUTION ---- 191 | Optional inportMetadata = packetIn.metadatas() 192 | .stream() 193 | .filter(meta -> meta.id().id().equals(inportMetadataName)) 194 | .findFirst(); 195 | 196 | if (!inportMetadata.isPresent()) { 197 | throw new PiInterpreterException(format( 198 | "Missing metadata '%s' in packet-in received from '%s': %s", 199 | inportMetadataName, deviceId, packetIn)); 200 | } 201 | 202 | // Build ONOS InboundPacket instance with the given ingress port. 203 | 204 | // 1. Parse packet-in object into Ethernet packet instance. 205 | final byte[] payloadBytes = packetIn.data().asArray(); 206 | final ByteBuffer rawData = ByteBuffer.wrap(payloadBytes); 207 | final Ethernet ethPkt; 208 | try { 209 | ethPkt = Ethernet.deserializer().deserialize( 210 | payloadBytes, 0, packetIn.data().size()); 211 | } catch (DeserializationException dex) { 212 | throw new PiInterpreterException(dex.getMessage()); 213 | } 214 | 215 | // 2. Get ingress port 216 | final ImmutableByteSequence portBytes = inportMetadata.get().value(); 217 | final short portNum = portBytes.asReadOnlyBuffer().getShort(); 218 | final ConnectPoint receivedFrom = new ConnectPoint( 219 | deviceId, PortNumber.portNumber(portNum)); 220 | 221 | return new DefaultInboundPacket(receivedFrom, ethPkt, rawData); 222 | } 223 | 224 | @Override 225 | public Optional mapLogicalPortNumber(PortNumber port) { 226 | if (CONTROLLER.equals(port)) { 227 | return Optional.of(CPU_PORT_ID); 228 | } else { 229 | return Optional.empty(); 230 | } 231 | } 232 | 233 | @Override 234 | public Optional mapCriterionType(Criterion.Type type) { 235 | if (CRITERION_MAP.containsKey(type)) { 236 | return Optional.of(PiMatchFieldId.of(CRITERION_MAP.get(type))); 237 | } else { 238 | return Optional.empty(); 239 | } 240 | } 241 | 242 | @Override 243 | public PiAction mapTreatment(TrafficTreatment treatment, PiTableId piTableId) 244 | throws PiInterpreterException { 245 | throw new PiInterpreterException("Treatment mapping not supported"); 246 | } 247 | 248 | @Override 249 | public Optional mapFlowRuleTableId(int flowRuleTableId) { 250 | return Optional.empty(); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/pipeconf/PipeconfLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.pipeconf; 18 | 19 | import org.onosproject.net.behaviour.Pipeliner; 20 | import org.onosproject.net.driver.DriverAdminService; 21 | import org.onosproject.net.driver.DriverProvider; 22 | import org.onosproject.net.pi.model.DefaultPiPipeconf; 23 | import org.onosproject.net.pi.model.PiPipeconf; 24 | import org.onosproject.net.pi.model.PiPipelineInterpreter; 25 | import org.onosproject.net.pi.model.PiPipelineModel; 26 | import org.onosproject.net.pi.service.PiPipeconfService; 27 | import org.onosproject.p4runtime.model.P4InfoParser; 28 | import org.onosproject.p4runtime.model.P4InfoParserException; 29 | import org.osgi.service.component.annotations.Activate; 30 | import org.osgi.service.component.annotations.Component; 31 | import org.osgi.service.component.annotations.Deactivate; 32 | import org.osgi.service.component.annotations.Reference; 33 | import org.osgi.service.component.annotations.ReferenceCardinality; 34 | import org.slf4j.Logger; 35 | import org.slf4j.LoggerFactory; 36 | 37 | import java.net.URL; 38 | import java.util.List; 39 | import java.util.stream.Collectors; 40 | 41 | import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.BMV2_JSON; 42 | import static org.onosproject.net.pi.model.PiPipeconf.ExtensionType.P4_INFO_TEXT; 43 | import static org.onosproject.ngsdn.tutorial.AppConstants.PIPECONF_ID; 44 | 45 | /** 46 | * Component that builds and register the pipeconf at app activation. 47 | */ 48 | @Component(immediate = true, service = PipeconfLoader.class) 49 | public final class PipeconfLoader { 50 | 51 | private final Logger log = LoggerFactory.getLogger(getClass()); 52 | 53 | private static final String P4INFO_PATH = "/p4info.txt"; 54 | private static final String BMV2_JSON_PATH = "/bmv2.json"; 55 | 56 | 57 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 58 | private PiPipeconfService pipeconfService; 59 | 60 | @Reference(cardinality = ReferenceCardinality.MANDATORY) 61 | private DriverAdminService driverAdminService; 62 | 63 | @Activate 64 | public void activate() { 65 | // Registers the pipeconf at component activation. 66 | if (pipeconfService.getPipeconf(PIPECONF_ID).isPresent()) { 67 | // Remove first if already registered, to support reloading of the 68 | // pipeconf during the tutorial. 69 | pipeconfService.unregister(PIPECONF_ID); 70 | } 71 | removePipeconfDrivers(); 72 | try { 73 | pipeconfService.register(buildPipeconf()); 74 | } catch (P4InfoParserException e) { 75 | log.error("Unable to register " + PIPECONF_ID, e); 76 | } 77 | } 78 | 79 | @Deactivate 80 | public void deactivate() { 81 | // Do nothing. 82 | } 83 | 84 | private PiPipeconf buildPipeconf() throws P4InfoParserException { 85 | 86 | final URL p4InfoUrl = PipeconfLoader.class.getResource(P4INFO_PATH); 87 | final URL bmv2JsonUrlUrl = PipeconfLoader.class.getResource(BMV2_JSON_PATH); 88 | final PiPipelineModel pipelineModel = P4InfoParser.parse(p4InfoUrl); 89 | 90 | return DefaultPiPipeconf.builder() 91 | .withId(PIPECONF_ID) 92 | .withPipelineModel(pipelineModel) 93 | .addBehaviour(PiPipelineInterpreter.class, InterpreterImpl.class) 94 | .addBehaviour(Pipeliner.class, PipelinerImpl.class) 95 | .addExtension(P4_INFO_TEXT, p4InfoUrl) 96 | .addExtension(BMV2_JSON, bmv2JsonUrlUrl) 97 | .build(); 98 | } 99 | 100 | private void removePipeconfDrivers() { 101 | List driverProvidersToRemove = driverAdminService 102 | .getProviders().stream() 103 | .filter(p -> p.getDrivers().stream() 104 | .anyMatch(d -> d.name().endsWith(PIPECONF_ID.id()))) 105 | .collect(Collectors.toList()); 106 | 107 | if (driverProvidersToRemove.isEmpty()) { 108 | return; 109 | } 110 | 111 | log.info("Found {} outdated drivers for pipeconf '{}', removing...", 112 | driverProvidersToRemove.size(), PIPECONF_ID); 113 | 114 | driverProvidersToRemove.forEach(driverAdminService::unregisterProvider); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/org/onosproject/ngsdn/tutorial/pipeconf/PipelinerImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.pipeconf; 18 | 19 | import org.onosproject.net.DeviceId; 20 | import org.onosproject.net.PortNumber; 21 | import org.onosproject.net.behaviour.NextGroup; 22 | import org.onosproject.net.behaviour.Pipeliner; 23 | import org.onosproject.net.behaviour.PipelinerContext; 24 | import org.onosproject.net.driver.AbstractHandlerBehaviour; 25 | import org.onosproject.net.flow.DefaultFlowRule; 26 | import org.onosproject.net.flow.DefaultTrafficTreatment; 27 | import org.onosproject.net.flow.FlowRule; 28 | import org.onosproject.net.flow.FlowRuleService; 29 | import org.onosproject.net.flow.instructions.Instructions; 30 | import org.onosproject.net.flowobjective.FilteringObjective; 31 | import org.onosproject.net.flowobjective.ForwardingObjective; 32 | import org.onosproject.net.flowobjective.NextObjective; 33 | import org.onosproject.net.flowobjective.ObjectiveError; 34 | import org.onosproject.net.group.GroupDescription; 35 | import org.onosproject.net.group.GroupService; 36 | import org.onosproject.net.pi.model.PiActionId; 37 | import org.onosproject.net.pi.model.PiTableId; 38 | import org.onosproject.net.pi.runtime.PiAction; 39 | import org.onosproject.ngsdn.tutorial.common.Utils; 40 | import org.slf4j.Logger; 41 | 42 | import java.util.Collections; 43 | import java.util.List; 44 | 45 | import static org.onosproject.net.flow.instructions.Instruction.Type.OUTPUT; 46 | import static org.onosproject.ngsdn.tutorial.AppConstants.CPU_CLONE_SESSION_ID; 47 | import static org.slf4j.LoggerFactory.getLogger; 48 | 49 | /** 50 | * Pipeliner implementation that maps all forwarding objectives to the ACL 51 | * table. All other types of objectives are not supported. 52 | */ 53 | public class PipelinerImpl extends AbstractHandlerBehaviour implements Pipeliner { 54 | 55 | // From the P4Info file 56 | private static final String ACL_TABLE = "IngressPipeImpl.acl_table"; 57 | private static final String CLONE_TO_CPU = "IngressPipeImpl.clone_to_cpu"; 58 | 59 | private final Logger log = getLogger(getClass()); 60 | 61 | private FlowRuleService flowRuleService; 62 | private GroupService groupService; 63 | private DeviceId deviceId; 64 | 65 | 66 | @Override 67 | public void init(DeviceId deviceId, PipelinerContext context) { 68 | this.deviceId = deviceId; 69 | this.flowRuleService = context.directory().get(FlowRuleService.class); 70 | this.groupService = context.directory().get(GroupService.class); 71 | } 72 | 73 | @Override 74 | public void filter(FilteringObjective obj) { 75 | obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED)); 76 | } 77 | 78 | @Override 79 | public void forward(ForwardingObjective obj) { 80 | if (obj.treatment() == null) { 81 | obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED)); 82 | } 83 | 84 | // Whether this objective specifies an OUTPUT:CONTROLLER instruction. 85 | final boolean hasCloneToCpuAction = obj.treatment() 86 | .allInstructions().stream() 87 | .filter(i -> i.type().equals(OUTPUT)) 88 | .map(i -> (Instructions.OutputInstruction) i) 89 | .anyMatch(i -> i.port().equals(PortNumber.CONTROLLER)); 90 | 91 | if (!hasCloneToCpuAction) { 92 | // We support only objectives for clone to CPU behaviours (e.g. for 93 | // host and link discovery) 94 | obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED)); 95 | } 96 | 97 | // Create an equivalent FlowRule with same selector and clone_to_cpu action. 98 | final PiAction cloneToCpuAction = PiAction.builder() 99 | .withId(PiActionId.of(CLONE_TO_CPU)) 100 | .build(); 101 | 102 | final FlowRule.Builder ruleBuilder = DefaultFlowRule.builder() 103 | .forTable(PiTableId.of(ACL_TABLE)) 104 | .forDevice(deviceId) 105 | .withSelector(obj.selector()) 106 | .fromApp(obj.appId()) 107 | .withPriority(obj.priority()) 108 | .withTreatment(DefaultTrafficTreatment.builder() 109 | .piTableAction(cloneToCpuAction).build()); 110 | 111 | if (obj.permanent()) { 112 | ruleBuilder.makePermanent(); 113 | } else { 114 | ruleBuilder.makeTemporary(obj.timeout()); 115 | } 116 | 117 | final GroupDescription cloneGroup = Utils.buildCloneGroup( 118 | obj.appId(), 119 | deviceId, 120 | CPU_CLONE_SESSION_ID, 121 | // Ports where to clone the packet. 122 | // Just controller in this case. 123 | Collections.singleton(PortNumber.CONTROLLER)); 124 | 125 | switch (obj.op()) { 126 | case ADD: 127 | flowRuleService.applyFlowRules(ruleBuilder.build()); 128 | groupService.addGroup(cloneGroup); 129 | break; 130 | case REMOVE: 131 | flowRuleService.removeFlowRules(ruleBuilder.build()); 132 | // Do not remove the clone group as other flow rules might be 133 | // pointing to it. 134 | break; 135 | default: 136 | log.warn("Unknown operation {}", obj.op()); 137 | } 138 | 139 | obj.context().ifPresent(c -> c.onSuccess(obj)); 140 | } 141 | 142 | @Override 143 | public void next(NextObjective obj) { 144 | obj.context().ifPresent(c -> c.onError(obj, ObjectiveError.UNSUPPORTED)); 145 | } 146 | 147 | @Override 148 | public List getNextMappings(NextGroup nextGroup) { 149 | // We do not use nextObjectives or groups. 150 | return Collections.emptyList(); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | mininet: 5 | image: opennetworking/ngsdn-tutorial:stratum_bmv2 6 | hostname: mininet 7 | container_name: mininet 8 | privileged: true 9 | tty: true 10 | stdin_open: true 11 | restart: always 12 | volumes: 13 | - ./tmp:/tmp 14 | - ./mininet:/mininet 15 | ports: 16 | - "50001:50001" 17 | - "50002:50002" 18 | - "50003:50003" 19 | - "50004:50004" 20 | # NGSDN_TOPO_PY is a Python-based Mininet script defining the topology. Its 21 | # value is passed to docker-compose as an environment variable, defined in 22 | # the Makefile. 23 | entrypoint: "/mininet/${NGSDN_TOPO_PY}" 24 | onos: 25 | image: onosproject/onos:2.2.2 26 | hostname: onos 27 | container_name: onos 28 | ports: 29 | - "8181:8181" # HTTP 30 | - "8101:8101" # SSH (CLI) 31 | volumes: 32 | - ./tmp/onos:/root/onos/apache-karaf-4.2.8/data/tmp 33 | environment: 34 | - ONOS_APPS=gui2,drivers.bmv2,lldpprovider,hostprovider 35 | links: 36 | - mininet 37 | -------------------------------------------------------------------------------- /img/device-leaf1-details-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/device-leaf1-details-panel.png -------------------------------------------------------------------------------- /img/onos-gui-pipeconf-leaf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/onos-gui-pipeconf-leaf1.png -------------------------------------------------------------------------------- /img/routing-ecmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/routing-ecmp.png -------------------------------------------------------------------------------- /img/srv6-ping-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/srv6-ping-1.png -------------------------------------------------------------------------------- /img/srv6-ping-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/srv6-ping-2.png -------------------------------------------------------------------------------- /img/topo-gtp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/topo-gtp.png -------------------------------------------------------------------------------- /img/topo-v4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/topo-v4.png -------------------------------------------------------------------------------- /img/topo-v6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/topo-v6.png -------------------------------------------------------------------------------- /img/trellis-features.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/img/trellis-features.png -------------------------------------------------------------------------------- /mininet/flowrule-gtp.json: -------------------------------------------------------------------------------- 1 | { 2 | "flows": [ 3 | { 4 | "deviceId": "device:leaf1", 5 | "tableId": "FabricIngress.spgw_ingress.dl_sess_lookup", 6 | "priority": 10, 7 | "timeout": 0, 8 | "isPermanent": true, 9 | "selector": { 10 | "criteria": [ 11 | { 12 | "type": "IPV4_DST", 13 | "ip": "/32" 14 | } 15 | ] 16 | }, 17 | "treatment": { 18 | "instructions": [ 19 | { 20 | "type": "PROTOCOL_INDEPENDENT", 21 | "subtype": "ACTION", 22 | "actionId": "FabricIngress.spgw_ingress.set_dl_sess_info", 23 | "actionParams": { 24 | "teid": "BEEF", 25 | "s1u_enb_addr": "0a006401", 26 | "s1u_sgw_addr": "0a0064fe" 27 | } 28 | } 29 | ] 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /mininet/host-cmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Attach to a Mininet host and run a command 4 | 5 | if [ -z $1 ]; then 6 | echo "usage: $0 host cmd [args...]" 7 | exit 1 8 | else 9 | host=$1 10 | fi 11 | 12 | pid=`ps ax | grep "mininet:$host$" | grep bash | grep -v mnexec | awk '{print $1};'` 13 | 14 | if echo $pid | grep -q ' '; then 15 | echo "Error: found multiple mininet:$host processes" 16 | exit 2 17 | fi 18 | 19 | if [ "$pid" == "" ]; then 20 | echo "Could not find Mininet host $host" 21 | exit 3 22 | fi 23 | 24 | if [ -z $2 ]; then 25 | cmd='bash' 26 | else 27 | shift 28 | cmd=$* 29 | fi 30 | 31 | cgroup=/sys/fs/cgroup/cpu/$host 32 | if [ -d "$cgroup" ]; then 33 | cg="-g $host" 34 | fi 35 | 36 | # Check whether host should be running in a chroot dir 37 | rootdir="/var/run/mn/$host/root" 38 | if [ -d $rootdir -a -x $rootdir/bin/bash ]; then 39 | cmd="'cd `pwd`; exec $cmd'" 40 | cmd="chroot $rootdir /bin/bash -c $cmd" 41 | fi 42 | 43 | mnexec $cg -a $pid hostname $host 44 | cmd="exec mnexec $cg -a $pid $cmd" 45 | eval $cmd -------------------------------------------------------------------------------- /mininet/netcfg-gtp.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": { 3 | "device:leaf1": { 4 | "basic": { 5 | "managementAddress": "grpc://mininet:50001?device_id=1", 6 | "driver": "stratum-bmv2", 7 | "pipeconf": "org.onosproject.pipelines.fabric", 8 | "locType": "grid", 9 | "gridX": 200, 10 | "gridY": 600 11 | }, 12 | "segmentrouting": { 13 | "name": "leaf1", 14 | "ipv4NodeSid": 101, 15 | "ipv4Loopback": "192.168.1.1", 16 | "routerMac": "00:AA:00:00:00:01", 17 | "isEdgeRouter": true, 18 | "adjacencySids": [] 19 | } 20 | }, 21 | "device:leaf2": { 22 | "basic": { 23 | "managementAddress": "grpc://mininet:50002?device_id=1", 24 | "driver": "stratum-bmv2", 25 | "pipeconf": "org.onosproject.pipelines.fabric", 26 | "locType": "grid", 27 | "gridX": 800, 28 | "gridY": 600 29 | }, 30 | "segmentrouting": { 31 | "name": "leaf2", 32 | "ipv4NodeSid": 102, 33 | "ipv4Loopback": "192.168.1.2", 34 | "routerMac": "00:AA:00:00:00:02", 35 | "isEdgeRouter": true, 36 | "adjacencySids": [] 37 | } 38 | }, 39 | "device:spine1": { 40 | "basic": { 41 | "managementAddress": "grpc://mininet:50003?device_id=1", 42 | "driver": "stratum-bmv2", 43 | "pipeconf": "org.onosproject.pipelines.fabric", 44 | "locType": "grid", 45 | "gridX": 400, 46 | "gridY": 400 47 | }, 48 | "segmentrouting": { 49 | "name": "spine1", 50 | "ipv4NodeSid": 201, 51 | "ipv4Loopback": "192.168.2.1", 52 | "routerMac": "00:BB:00:00:00:01", 53 | "isEdgeRouter": false, 54 | "adjacencySids": [] 55 | } 56 | }, 57 | "device:spine2": { 58 | "basic": { 59 | "managementAddress": "grpc://mininet:50004?device_id=1", 60 | "driver": "stratum-bmv2", 61 | "pipeconf": "org.onosproject.pipelines.fabric", 62 | "locType": "grid", 63 | "gridX": 600, 64 | "gridY": 400 65 | }, 66 | "segmentrouting": { 67 | "name": "spine2", 68 | "ipv4NodeSid": 202, 69 | "ipv4Loopback": "192.168.2.2", 70 | "routerMac": "00:BB:00:00:00:02", 71 | "isEdgeRouter": false, 72 | "adjacencySids": [] 73 | } 74 | } 75 | }, 76 | "ports": { 77 | "device:leaf1/3": { 78 | "interfaces": [ 79 | { 80 | "name": "leaf1-3", 81 | "ips": [ 82 | "10.0.100.254/24" 83 | ], 84 | "vlan-untagged": 100 85 | } 86 | ] 87 | }, 88 | "device:leaf2/3": { 89 | "interfaces": [ 90 | { 91 | "name": "leaf2-3", 92 | "ips": [ 93 | "10.0.200.254/24" 94 | ], 95 | "vlan-untagged": 200 96 | } 97 | ] 98 | } 99 | }, 100 | "hosts": { 101 | "00:00:00:00:00:10/None": { 102 | "basic": { 103 | "name": "enodeb", 104 | "gridX": 100, 105 | "gridY": 700, 106 | "locType": "grid", 107 | "ips": [ 108 | "10.0.100.1" 109 | ], 110 | "locations": [ 111 | "device:leaf1/3" 112 | ] 113 | } 114 | }, 115 | "00:00:00:00:00:20/None": { 116 | "basic": { 117 | "name": "pdn", 118 | "gridX": 850, 119 | "gridY": 700, 120 | "locType": "grid", 121 | "ips": [ 122 | "10.0.200.1" 123 | ], 124 | "locations": [ 125 | "device:leaf2/3" 126 | ] 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /mininet/netcfg-sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": { 3 | "device:leaf1": { 4 | "basic": { 5 | "managementAddress": "grpc://mininet:50001?device_id=1", 6 | "driver": "stratum-bmv2", 7 | "pipeconf": "org.onosproject.pipelines.fabric", 8 | "locType": "grid", 9 | "gridX": 200, 10 | "gridY": 600 11 | }, 12 | "segmentrouting": { 13 | "name": "leaf1", 14 | "ipv4NodeSid": 101, 15 | "ipv4Loopback": "192.168.1.1", 16 | "routerMac": "00:AA:00:00:00:01", 17 | "isEdgeRouter": true, 18 | "adjacencySids": [] 19 | } 20 | }, 21 | "device:leaf2": { 22 | "basic": { 23 | "managementAddress": "grpc://mininet:50002?device_id=1", 24 | "driver": "stratum-bmv2", 25 | "pipeconf": "org.onosproject.pipelines.fabric", 26 | "locType": "grid", 27 | "gridX": 800, 28 | "gridY": 600 29 | }, 30 | "segmentrouting": { 31 | "name": "leaf2", 32 | "ipv4NodeSid": 102, 33 | "ipv4Loopback": "192.168.1.2", 34 | "routerMac": "00:AA:00:00:00:02", 35 | "isEdgeRouter": true, 36 | "adjacencySids": [] 37 | } 38 | }, 39 | "device:spine1": { 40 | "basic": { 41 | "managementAddress": "grpc://mininet:50003?device_id=1", 42 | "driver": "stratum-bmv2", 43 | "pipeconf": "org.onosproject.pipelines.fabric", 44 | "locType": "grid", 45 | "gridX": 400, 46 | "gridY": 400 47 | }, 48 | "segmentrouting": { 49 | "name": "spine1", 50 | "ipv4NodeSid": 201, 51 | "ipv4Loopback": "192.168.2.1", 52 | "routerMac": "00:BB:00:00:00:01", 53 | "isEdgeRouter": false, 54 | "adjacencySids": [] 55 | } 56 | }, 57 | "device:spine2": { 58 | "basic": { 59 | "managementAddress": "grpc://mininet:50004?device_id=1", 60 | "driver": "stratum-bmv2", 61 | "pipeconf": "org.onosproject.pipelines.fabric", 62 | "locType": "grid", 63 | "gridX": 600, 64 | "gridY": 400 65 | }, 66 | "segmentrouting": { 67 | "name": "spine2", 68 | "ipv4NodeSid": 202, 69 | "ipv4Loopback": "192.168.2.2", 70 | "routerMac": "00:BB:00:00:00:02", 71 | "isEdgeRouter": false, 72 | "adjacencySids": [] 73 | } 74 | } 75 | }, 76 | "ports": { 77 | "device:leaf1/3": { 78 | "interfaces": [ 79 | { 80 | "name": "leaf1-3", 81 | "ips": [ 82 | "172.16.1.254/24" 83 | ], 84 | "vlan-untagged": 100 85 | } 86 | ] 87 | }, 88 | "device:leaf1/4": { 89 | "interfaces": [ 90 | { 91 | "name": "leaf1-4", 92 | "ips": [ 93 | "172.16.1.254/24" 94 | ], 95 | "vlan-untagged": 100 96 | } 97 | ] 98 | }, 99 | "device:leaf1/5": { 100 | "interfaces": [ 101 | { 102 | "name": "leaf1-5", 103 | "ips": [ 104 | "172.16.1.254/24" 105 | ], 106 | "vlan-tagged": [ 107 | 100 108 | ] 109 | } 110 | ] 111 | }, 112 | "device:leaf1/6": { 113 | "interfaces": [ 114 | { 115 | "name": "leaf1-6", 116 | "ips": [ 117 | "172.16.2.254/24" 118 | ], 119 | "vlan-tagged": [ 120 | 200 121 | ] 122 | } 123 | ] 124 | } 125 | }, 126 | "hosts": { 127 | "00:00:00:00:00:1A/None": { 128 | "basic": { 129 | "name": "h1a", 130 | "locType": "grid", 131 | "gridX": 100, 132 | "gridY": 700 133 | } 134 | }, 135 | "00:00:00:00:00:1B/None": { 136 | "basic": { 137 | "name": "h1b", 138 | "locType": "grid", 139 | "gridX": 100, 140 | "gridY": 800 141 | } 142 | }, 143 | "00:00:00:00:00:1C/100": { 144 | "basic": { 145 | "name": "h1c", 146 | "locType": "grid", 147 | "gridX": 250, 148 | "gridY": 800 149 | } 150 | }, 151 | "00:00:00:00:00:20/200": { 152 | "basic": { 153 | "name": "h2", 154 | "locType": "grid", 155 | "gridX": 400, 156 | "gridY": 700 157 | } 158 | }, 159 | "00:00:00:00:00:30/300": { 160 | "basic": { 161 | "name": "h3", 162 | "locType": "grid", 163 | "gridX": 750, 164 | "gridY": 700 165 | } 166 | }, 167 | "00:00:00:00:00:40/None": { 168 | "basic": { 169 | "name": "h4", 170 | "locType": "grid", 171 | "gridX": 850, 172 | "gridY": 700 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /mininet/netcfg.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": { 3 | "device:leaf1": { 4 | "basic": { 5 | "managementAddress": "grpc://mininet:50001?device_id=1", 6 | "driver": "stratum-bmv2", 7 | "pipeconf": "org.onosproject.ngsdn-tutorial", 8 | "locType": "grid", 9 | "gridX": 200, 10 | "gridY": 600 11 | }, 12 | "fabricDeviceConfig": { 13 | "myStationMac": "00:aa:00:00:00:01", 14 | "mySid": "3:101:2::", 15 | "isSpine": false 16 | } 17 | }, 18 | "device:leaf2": { 19 | "basic": { 20 | "managementAddress": "grpc://mininet:50002?device_id=1", 21 | "driver": "stratum-bmv2", 22 | "pipeconf": "org.onosproject.ngsdn-tutorial", 23 | "locType": "grid", 24 | "gridX": 800, 25 | "gridY": 600 26 | }, 27 | "fabricDeviceConfig": { 28 | "myStationMac": "00:aa:00:00:00:02", 29 | "mySid": "3:102:2::", 30 | "isSpine": false 31 | } 32 | }, 33 | "device:spine1": { 34 | "basic": { 35 | "managementAddress": "grpc://mininet:50003?device_id=1", 36 | "driver": "stratum-bmv2", 37 | "pipeconf": "org.onosproject.ngsdn-tutorial", 38 | "locType": "grid", 39 | "gridX": 400, 40 | "gridY": 400 41 | }, 42 | "fabricDeviceConfig": { 43 | "myStationMac": "00:bb:00:00:00:01", 44 | "mySid": "3:201:2::", 45 | "isSpine": true 46 | } 47 | }, 48 | "device:spine2": { 49 | "basic": { 50 | "managementAddress": "grpc://mininet:50004?device_id=1", 51 | "driver": "stratum-bmv2", 52 | "pipeconf": "org.onosproject.ngsdn-tutorial", 53 | "locType": "grid", 54 | "gridX": 600, 55 | "gridY": 400 56 | }, 57 | "fabricDeviceConfig": { 58 | "myStationMac": "00:bb:00:00:00:02", 59 | "mySid": "3:202:2::", 60 | "isSpine": true 61 | } 62 | } 63 | }, 64 | "ports": { 65 | "device:leaf1/3": { 66 | "interfaces": [ 67 | { 68 | "name": "leaf1-3", 69 | "ips": ["2001:1:1::ff/64"] 70 | } 71 | ] 72 | }, 73 | "device:leaf1/4": { 74 | "interfaces": [ 75 | { 76 | "name": "leaf1-4", 77 | "ips": ["2001:1:1::ff/64"] 78 | } 79 | ] 80 | }, 81 | "device:leaf1/5": { 82 | "interfaces": [ 83 | { 84 | "name": "leaf1-5", 85 | "ips": ["2001:1:1::ff/64"] 86 | } 87 | ] 88 | }, 89 | "device:leaf1/6": { 90 | "interfaces": [ 91 | { 92 | "name": "leaf1-6", 93 | "ips": ["2001:1:2::ff/64"] 94 | } 95 | ] 96 | }, 97 | "device:leaf2/3": { 98 | "interfaces": [ 99 | { 100 | "name": "leaf2-3", 101 | "ips": ["2001:2:3::ff/64"] 102 | } 103 | ] 104 | }, 105 | "device:leaf2/4": { 106 | "interfaces": [ 107 | { 108 | "name": "leaf2-4", 109 | "ips": ["2001:2:4::ff/64"] 110 | } 111 | ] 112 | } 113 | }, 114 | "hosts": { 115 | "00:00:00:00:00:1A/None": { 116 | "basic": { 117 | "name": "h1a", 118 | "locType": "grid", 119 | "gridX": 100, 120 | "gridY": 700 121 | } 122 | }, 123 | "00:00:00:00:00:1B/None": { 124 | "basic": { 125 | "name": "h1b", 126 | "locType": "grid", 127 | "gridX": 100, 128 | "gridY": 800 129 | } 130 | }, 131 | "00:00:00:00:00:1C/None": { 132 | "basic": { 133 | "name": "h1c", 134 | "locType": "grid", 135 | "gridX": 250, 136 | "gridY": 800 137 | } 138 | }, 139 | "00:00:00:00:00:20/None": { 140 | "basic": { 141 | "name": "h2", 142 | "locType": "grid", 143 | "gridX": 400, 144 | "gridY": 700 145 | } 146 | }, 147 | "00:00:00:00:00:30/None": { 148 | "basic": { 149 | "name": "h3", 150 | "locType": "grid", 151 | "gridX": 750, 152 | "gridY": 700 153 | } 154 | }, 155 | "00:00:00:00:00:40/None": { 156 | "basic": { 157 | "name": "h4", 158 | "locType": "grid", 159 | "gridX": 850, 160 | "gridY": 700 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /mininet/recv-gtp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Script used in Exercise 8 that sniffs packets and prints on screen whether 4 | # they are GTP encapsulated or not. 5 | 6 | import signal 7 | import sys 8 | 9 | from ptf.packet import IP 10 | from scapy.contrib import gtp 11 | from scapy.sendrecv import sniff 12 | 13 | pkt_count = 0 14 | 15 | 16 | def handle_pkt(pkt, ex): 17 | global pkt_count 18 | pkt_count = pkt_count + 1 19 | if gtp.GTP_U_Header in pkt: 20 | is_gtp_encap = True 21 | else: 22 | is_gtp_encap = False 23 | 24 | print "[%d] %d bytes: %s -> %s, is_gtp_encap=%s\n\t%s" \ 25 | % (pkt_count, len(pkt), pkt[IP].src, pkt[IP].dst, 26 | is_gtp_encap, pkt.summary()) 27 | 28 | if is_gtp_encap and ex: 29 | exit() 30 | 31 | 32 | print "Will print a line for each UDP packet received..." 33 | 34 | 35 | def handle_timeout(signum, frame): 36 | print "Timeout! Did not receive any GTP packet" 37 | exit(1) 38 | 39 | 40 | exitOnSuccess = False 41 | if len(sys.argv) > 1 and sys.argv[1] == "-e": 42 | # wait max 10 seconds or exit 43 | signal.signal(signal.SIGALRM, handle_timeout) 44 | signal.alarm(10) 45 | exitOnSuccess = True 46 | 47 | sniff(count=0, store=False, filter="udp", 48 | prn=lambda x: handle_pkt(x, exitOnSuccess)) 49 | -------------------------------------------------------------------------------- /mininet/send-udp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Script used in Exercise 8. 4 | # Send downlink packets to UE address. 5 | 6 | from scapy.layers.inet import IP, UDP 7 | from scapy.sendrecv import send 8 | 9 | UE_ADDR = '17.0.0.1' 10 | RATE = 5 # packets per second 11 | PAYLOAD = ' '.join(['P4 is great!'] * 50) 12 | 13 | print "Sending %d UDP packets per second to %s..." % (RATE, UE_ADDR) 14 | 15 | pkt = IP(dst=UE_ADDR) / UDP(sport=80, dport=400) / PAYLOAD 16 | send(pkt, inter=1.0 / RATE, loop=True, verbose=True) 17 | -------------------------------------------------------------------------------- /mininet/topo-gtp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2019-present Open Networking Foundation 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 | # http://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 | import argparse 18 | 19 | from mininet.cli import CLI 20 | from mininet.log import setLogLevel 21 | from mininet.net import Mininet 22 | from mininet.node import Host 23 | from mininet.topo import Topo 24 | from stratum import StratumBmv2Switch 25 | 26 | CPU_PORT = 255 27 | 28 | 29 | class IPv4Host(Host): 30 | """Host that can be configured with an IPv4 gateway (default route). 31 | """ 32 | 33 | def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, 34 | **_params): 35 | super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) 36 | self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) 37 | self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) 38 | self.cmd('sysctl -w net.ipv4.ip_forward=0') 39 | self.cmd('ip -4 link set up %s' % self.defaultIntf()) 40 | self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) 41 | if gw: 42 | self.cmd('ip -4 route add default via %s' % gw) 43 | # Disable offload 44 | for attr in ["rx", "tx", "sg"]: 45 | cmd = "/sbin/ethtool --offload %s %s off" % ( 46 | self.defaultIntf(), attr) 47 | self.cmd(cmd) 48 | 49 | def updateIP(): 50 | return ip.split('/')[0] 51 | 52 | self.defaultIntf().updateIP = updateIP 53 | 54 | 55 | class TutorialTopo(Topo): 56 | """2x2 fabric topology for GTP encap exercise with 2 IPv4 hosts emulating an 57 | enodeb (base station) and a gateway to a Packet Data Metwork (PDN) 58 | """ 59 | 60 | def __init__(self, *args, **kwargs): 61 | Topo.__init__(self, *args, **kwargs) 62 | 63 | # Leaves 64 | # gRPC port 50001 65 | leaf1 = self.addSwitch('leaf1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 66 | # gRPC port 50002 67 | leaf2 = self.addSwitch('leaf2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 68 | 69 | # Spines 70 | # gRPC port 50003 71 | spine1 = self.addSwitch('spine1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 72 | # gRPC port 50004 73 | spine2 = self.addSwitch('spine2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 74 | 75 | # Switch Links 76 | self.addLink(spine1, leaf1) 77 | self.addLink(spine1, leaf2) 78 | self.addLink(spine2, leaf1) 79 | self.addLink(spine2, leaf2) 80 | 81 | # IPv4 hosts attached to leaf 1 82 | enodeb = self.addHost('enodeb', cls=IPv4Host, mac='00:00:00:00:00:10', 83 | ip='10.0.100.1/24', gw='10.0.100.254') 84 | self.addLink(enodeb, leaf1) # port 3 85 | 86 | # IPv4 hosts attached to leaf 2 87 | pdn = self.addHost('pdn', cls=IPv4Host, mac='00:00:00:00:00:20', 88 | ip='10.0.200.1/24', gw='10.0.200.254') 89 | self.addLink(pdn, leaf2) # port 3 90 | 91 | 92 | def main(): 93 | net = Mininet(topo=TutorialTopo(), controller=None) 94 | net.start() 95 | CLI(net) 96 | net.stop() 97 | print '#' * 80 98 | print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' 99 | print 'No worries, it will restart automatically in a few seconds...' 100 | print 'To access again the Mininet CLI, use `make mn-cli`' 101 | print 'To detach from the CLI (without stopping), press Ctrl-D' 102 | print 'To permanently quit Mininet, use `make stop`' 103 | print '#' * 80 104 | 105 | 106 | if __name__ == "__main__": 107 | parser = argparse.ArgumentParser( 108 | description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') 109 | args = parser.parse_args() 110 | setLogLevel('info') 111 | 112 | main() 113 | -------------------------------------------------------------------------------- /mininet/topo-v4.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2019-present Open Networking Foundation 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 | # http://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 | import argparse 18 | 19 | from mininet.cli import CLI 20 | from mininet.log import setLogLevel 21 | from mininet.net import Mininet 22 | from mininet.node import Host 23 | from mininet.topo import Topo 24 | from stratum import StratumBmv2Switch 25 | 26 | CPU_PORT = 255 27 | 28 | 29 | class IPv4Host(Host): 30 | """Host that can be configured with an IPv4 gateway (default route). 31 | """ 32 | 33 | def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, 34 | **_params): 35 | super(IPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) 36 | self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) 37 | self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) 38 | self.cmd('ip -4 link set up %s' % self.defaultIntf()) 39 | self.cmd('ip -4 addr add %s dev %s' % (ip, self.defaultIntf())) 40 | if gw: 41 | self.cmd('ip -4 route add default via %s' % gw) 42 | # Disable offload 43 | for attr in ["rx", "tx", "sg"]: 44 | cmd = "/sbin/ethtool --offload %s %s off" % ( 45 | self.defaultIntf(), attr) 46 | self.cmd(cmd) 47 | 48 | def updateIP(): 49 | return ip.split('/')[0] 50 | 51 | self.defaultIntf().updateIP = updateIP 52 | 53 | 54 | class TaggedIPv4Host(Host): 55 | """VLAN-tagged host that can be configured with an IPv4 gateway 56 | (default route). 57 | """ 58 | vlanIntf = None 59 | 60 | def config(self, mac=None, ip=None, defaultRoute=None, lo='up', gw=None, 61 | vlan=None, **_params): 62 | super(TaggedIPv4Host, self).config(mac, ip, defaultRoute, lo, **_params) 63 | self.vlanIntf = "%s.%s" % (self.defaultIntf(), vlan) 64 | # Replace default interface with a tagged one 65 | self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) 66 | self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) 67 | self.cmd('ip -4 link add link %s name %s type vlan id %s' % ( 68 | self.defaultIntf(), self.vlanIntf, vlan)) 69 | self.cmd('ip -4 link set up %s' % self.vlanIntf) 70 | self.cmd('ip -4 addr add %s dev %s' % (ip, self.vlanIntf)) 71 | if gw: 72 | self.cmd('ip -4 route add default via %s' % gw) 73 | 74 | self.defaultIntf().name = self.vlanIntf 75 | self.nameToIntf[self.vlanIntf] = self.defaultIntf() 76 | 77 | # Disable offload 78 | for attr in ["rx", "tx", "sg"]: 79 | cmd = "/sbin/ethtool --offload %s %s off" % ( 80 | self.defaultIntf(), attr) 81 | self.cmd(cmd) 82 | 83 | def updateIP(): 84 | return ip.split('/')[0] 85 | 86 | self.defaultIntf().updateIP = updateIP 87 | 88 | def terminate(self): 89 | self.cmd('ip -4 link remove link %s' % self.vlanIntf) 90 | super(TaggedIPv4Host, self).terminate() 91 | 92 | 93 | class TutorialTopo(Topo): 94 | """2x2 fabric topology with IPv4 hosts""" 95 | 96 | def __init__(self, *args, **kwargs): 97 | Topo.__init__(self, *args, **kwargs) 98 | 99 | # Leaves 100 | # gRPC port 50001 101 | leaf1 = self.addSwitch('leaf1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 102 | # gRPC port 50002 103 | leaf2 = self.addSwitch('leaf2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 104 | 105 | # Spines 106 | # gRPC port 50003 107 | spine1 = self.addSwitch('spine1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 108 | # gRPC port 50004 109 | spine2 = self.addSwitch('spine2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 110 | 111 | # Switch Links 112 | self.addLink(spine1, leaf1) 113 | self.addLink(spine1, leaf2) 114 | self.addLink(spine2, leaf1) 115 | self.addLink(spine2, leaf2) 116 | 117 | # IPv4 hosts attached to leaf 1 118 | h1a = self.addHost('h1a', cls=IPv4Host, mac="00:00:00:00:00:1A", 119 | ip='172.16.1.1/24', gw='172.16.1.254') 120 | h1b = self.addHost('h1b', cls=IPv4Host, mac="00:00:00:00:00:1B", 121 | ip='172.16.1.2/24', gw='172.16.1.254') 122 | h1c = self.addHost('h1c', cls=TaggedIPv4Host, mac="00:00:00:00:00:1C", 123 | ip='172.16.1.3/24', gw='172.16.1.254', vlan=100) 124 | h2 = self.addHost('h2', cls=TaggedIPv4Host, mac="00:00:00:00:00:20", 125 | ip='172.16.2.1/24', gw='172.16.2.254', vlan=200) 126 | self.addLink(h1a, leaf1) # port 3 127 | self.addLink(h1b, leaf1) # port 4 128 | self.addLink(h1c, leaf1) # port 5 129 | self.addLink(h2, leaf1) # port 6 130 | 131 | # IPv4 hosts attached to leaf 2 132 | h3 = self.addHost('h3', cls=TaggedIPv4Host, mac="00:00:00:00:00:30", 133 | ip='172.16.3.1/24', gw='172.16.3.254', vlan=300) 134 | h4 = self.addHost('h4', cls=IPv4Host, mac="00:00:00:00:00:40", 135 | ip='172.16.4.1/24', gw='172.16.4.254') 136 | self.addLink(h3, leaf2) # port 3 137 | self.addLink(h4, leaf2) # port 4 138 | 139 | 140 | def main(): 141 | net = Mininet(topo=TutorialTopo(), controller=None) 142 | net.start() 143 | CLI(net) 144 | net.stop() 145 | print '#' * 80 146 | print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' 147 | print 'No worries, it will restart automatically in a few seconds...' 148 | print 'To access again the Mininet CLI, use `make mn-cli`' 149 | print 'To detach from the CLI (without stopping), press Ctrl-D' 150 | print 'To permanently quit Mininet, use `make stop`' 151 | print '#' * 80 152 | 153 | 154 | if __name__ == "__main__": 155 | parser = argparse.ArgumentParser( 156 | description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv4 hosts') 157 | args = parser.parse_args() 158 | setLogLevel('info') 159 | 160 | main() 161 | -------------------------------------------------------------------------------- /mininet/topo-v6.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # Copyright 2019-present Open Networking Foundation 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 | # http://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 | import argparse 18 | 19 | from mininet.cli import CLI 20 | from mininet.log import setLogLevel 21 | from mininet.net import Mininet 22 | from mininet.node import Host 23 | from mininet.topo import Topo 24 | from stratum import StratumBmv2Switch 25 | 26 | CPU_PORT = 255 27 | 28 | 29 | class IPv6Host(Host): 30 | """Host that can be configured with an IPv6 gateway (default route). 31 | """ 32 | 33 | def config(self, ipv6, ipv6_gw=None, **params): 34 | super(IPv6Host, self).config(**params) 35 | self.cmd('ip -4 addr flush dev %s' % self.defaultIntf()) 36 | self.cmd('ip -6 addr flush dev %s' % self.defaultIntf()) 37 | self.cmd('ip -6 addr add %s dev %s' % (ipv6, self.defaultIntf())) 38 | if ipv6_gw: 39 | self.cmd('ip -6 route add default via %s' % ipv6_gw) 40 | # Disable offload 41 | for attr in ["rx", "tx", "sg"]: 42 | cmd = "/sbin/ethtool --offload %s %s off" % (self.defaultIntf(), attr) 43 | self.cmd(cmd) 44 | 45 | def updateIP(): 46 | return ipv6.split('/')[0] 47 | 48 | self.defaultIntf().updateIP = updateIP 49 | 50 | def terminate(self): 51 | super(IPv6Host, self).terminate() 52 | 53 | 54 | class TutorialTopo(Topo): 55 | """2x2 fabric topology with IPv6 hosts""" 56 | 57 | def __init__(self, *args, **kwargs): 58 | Topo.__init__(self, *args, **kwargs) 59 | 60 | # Leaves 61 | # gRPC port 50001 62 | leaf1 = self.addSwitch('leaf1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 63 | # gRPC port 50002 64 | leaf2 = self.addSwitch('leaf2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 65 | 66 | # Spines 67 | # gRPC port 50003 68 | spine1 = self.addSwitch('spine1', cls=StratumBmv2Switch, cpuport=CPU_PORT) 69 | # gRPC port 50004 70 | spine2 = self.addSwitch('spine2', cls=StratumBmv2Switch, cpuport=CPU_PORT) 71 | 72 | # Switch Links 73 | self.addLink(spine1, leaf1) 74 | self.addLink(spine1, leaf2) 75 | self.addLink(spine2, leaf1) 76 | self.addLink(spine2, leaf2) 77 | 78 | # IPv6 hosts attached to leaf 1 79 | h1a = self.addHost('h1a', cls=IPv6Host, mac="00:00:00:00:00:1A", 80 | ipv6='2001:1:1::a/64', ipv6_gw='2001:1:1::ff') 81 | h1b = self.addHost('h1b', cls=IPv6Host, mac="00:00:00:00:00:1B", 82 | ipv6='2001:1:1::b/64', ipv6_gw='2001:1:1::ff') 83 | h1c = self.addHost('h1c', cls=IPv6Host, mac="00:00:00:00:00:1C", 84 | ipv6='2001:1:1::c/64', ipv6_gw='2001:1:1::ff') 85 | h2 = self.addHost('h2', cls=IPv6Host, mac="00:00:00:00:00:20", 86 | ipv6='2001:1:2::1/64', ipv6_gw='2001:1:2::ff') 87 | self.addLink(h1a, leaf1) # port 3 88 | self.addLink(h1b, leaf1) # port 4 89 | self.addLink(h1c, leaf1) # port 5 90 | self.addLink(h2, leaf1) # port 6 91 | 92 | # IPv6 hosts attached to leaf 2 93 | h3 = self.addHost('h3', cls=IPv6Host, mac="00:00:00:00:00:30", 94 | ipv6='2001:2:3::1/64', ipv6_gw='2001:2:3::ff') 95 | h4 = self.addHost('h4', cls=IPv6Host, mac="00:00:00:00:00:40", 96 | ipv6='2001:2:4::1/64', ipv6_gw='2001:2:4::ff') 97 | self.addLink(h3, leaf2) # port 3 98 | self.addLink(h4, leaf2) # port 4 99 | 100 | 101 | def main(): 102 | net = Mininet(topo=TutorialTopo(), controller=None) 103 | net.start() 104 | CLI(net) 105 | net.stop() 106 | print '#' * 80 107 | print 'ATTENTION: Mininet was stopped! Perhaps accidentally?' 108 | print 'No worries, it will restart automatically in a few seconds...' 109 | print 'To access again the Mininet CLI, use `make mn-cli`' 110 | print 'To detach from the CLI (without stopping), press Ctrl-D' 111 | print 'To permanently quit Mininet, use `make stop`' 112 | print '#' * 80 113 | 114 | 115 | if __name__ == "__main__": 116 | parser = argparse.ArgumentParser( 117 | description='Mininet topology script for 2x2 fabric with stratum_bmv2 and IPv6 hosts') 118 | args = parser.parse_args() 119 | setLogLevel('info') 120 | 121 | main() 122 | -------------------------------------------------------------------------------- /p4src/snippets.p4: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // SNIPPETS FOR EXERCISE 5 (IPV6 ROUTING) 3 | //------------------------------------------------------------------------------ 4 | 5 | // Action that transforms an NDP NS packet into an NDP NA one for the given 6 | // target MAC address. The action also sets the egress port to the ingress 7 | // one where the NDP NS packet was received. 8 | action ndp_ns_to_na(mac_addr_t target_mac) { 9 | hdr.ethernet.src_addr = target_mac; 10 | hdr.ethernet.dst_addr = IPV6_MCAST_01; 11 | ipv6_addr_t host_ipv6_tmp = hdr.ipv6.src_addr; 12 | hdr.ipv6.src_addr = hdr.ndp.target_ipv6_addr; 13 | hdr.ipv6.dst_addr = host_ipv6_tmp; 14 | hdr.ipv6.next_hdr = IP_PROTO_ICMPV6; 15 | hdr.icmpv6.type = ICMP6_TYPE_NA; 16 | hdr.ndp.flags = NDP_FLAG_ROUTER | NDP_FLAG_OVERRIDE; 17 | hdr.ndp.type = NDP_OPT_TARGET_LL_ADDR; 18 | hdr.ndp.length = 1; 19 | hdr.ndp.target_mac_addr = target_mac; 20 | standard_metadata.egress_spec = standard_metadata.ingress_port; 21 | } 22 | 23 | // ECMP action selector definition: 24 | action_selector(HashAlgorithm.crc16, 32w1024, 32w16) ecmp_selector; 25 | 26 | // Example indirect table that uses the ecmp_selector. "Selector" match fields 27 | // are used as input to the action selector hash function. 28 | // table table_with_action_selector { 29 | // key = { 30 | // hdr_field_1: lpm / exact / ternary; 31 | // hdr_field_2: selector; 32 | // hdr_field_3: selector; 33 | // ... 34 | // } 35 | // actions = { ... } 36 | // implementation = ecmp_selector; 37 | // ... 38 | // } 39 | 40 | //------------------------------------------------------------------------------ 41 | // SNIPPETS FOR EXERCISE 6 (SRV6) 42 | //------------------------------------------------------------------------------ 43 | 44 | action insert_srv6h_header(bit<8> num_segments) { 45 | hdr.srv6h.setValid(); 46 | hdr.srv6h.next_hdr = hdr.ipv6.next_hdr; 47 | hdr.srv6h.hdr_ext_len = num_segments * 2; 48 | hdr.srv6h.routing_type = 4; 49 | hdr.srv6h.segment_left = num_segments - 1; 50 | hdr.srv6h.last_entry = num_segments - 1; 51 | hdr.srv6h.flags = 0; 52 | hdr.srv6h.tag = 0; 53 | hdr.ipv6.next_hdr = IP_PROTO_SRV6; 54 | } 55 | 56 | action srv6_t_insert_2(ipv6_addr_t s1, ipv6_addr_t s2) { 57 | hdr.ipv6.dst_addr = s1; 58 | hdr.ipv6.payload_len = hdr.ipv6.payload_len + 40; 59 | insert_srv6h_header(2); 60 | hdr.srv6_list[0].setValid(); 61 | hdr.srv6_list[0].segment_id = s2; 62 | hdr.srv6_list[1].setValid(); 63 | hdr.srv6_list[1].segment_id = s1; 64 | } 65 | 66 | action srv6_t_insert_3(ipv6_addr_t s1, ipv6_addr_t s2, ipv6_addr_t s3) { 67 | hdr.ipv6.dst_addr = s1; 68 | hdr.ipv6.payload_len = hdr.ipv6.payload_len + 56; 69 | insert_srv6h_header(3); 70 | hdr.srv6_list[0].setValid(); 71 | hdr.srv6_list[0].segment_id = s3; 72 | hdr.srv6_list[1].setValid(); 73 | hdr.srv6_list[1].segment_id = s2; 74 | hdr.srv6_list[2].setValid(); 75 | hdr.srv6_list[2].segment_id = s1; 76 | } 77 | 78 | table srv6_transit { 79 | key = { 80 | // TODO: Add match fields for SRv6 transit rules; we'll start with the 81 | // destination IP address. 82 | } 83 | actions = { 84 | // Note: Single segment header doesn't make sense given PSP 85 | // i.e. we will pop the SRv6 header when segments_left reaches 0 86 | srv6_t_insert_2; 87 | srv6_t_insert_3; 88 | // Extra credit: set a metadata field, then push label stack in egress 89 | } 90 | @name("srv6_transit_table_counter") 91 | counters = direct_counter(CounterType.packets_and_bytes); 92 | } 93 | 94 | action srv6_pop() { 95 | hdr.ipv6.next_hdr = hdr.srv6h.next_hdr; 96 | // SRv6 header is 8 bytes 97 | // SRv6 list entry is 16 bytes each 98 | // (((bit<16>)hdr.srv6h.last_entry + 1) * 16) + 8; 99 | bit<16> srv6h_size = (((bit<16>)hdr.srv6h.last_entry + 1) << 4) + 8; 100 | hdr.ipv6.payload_len = hdr.ipv6.payload_len - srv6h_size; 101 | 102 | hdr.srv6h.setInvalid(); 103 | // Need to set MAX_HOPS headers invalid 104 | hdr.srv6_list[0].setInvalid(); 105 | hdr.srv6_list[1].setInvalid(); 106 | hdr.srv6_list[2].setInvalid(); 107 | } 108 | -------------------------------------------------------------------------------- /ptf/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opennetworkinglab/ngsdn-tutorial/62705c3cb64fa7c731b5255f5fc793f293ce0665/ptf/lib/__init__.py -------------------------------------------------------------------------------- /ptf/lib/chassis_config.pb.txt: -------------------------------------------------------------------------------- 1 | description: "Config for PTF tests using virtual interfaces" 2 | chassis { 3 | platform: PLT_P4_SOFT_SWITCH 4 | name: "bmv2-simple_switch" 5 | } 6 | nodes { 7 | id: 1 8 | slot: 1 9 | index: 1 10 | } 11 | singleton_ports { 12 | id: 1 13 | name: "veth0" 14 | slot: 1 15 | port: 1 16 | channel: 1 17 | speed_bps: 100000000000 18 | config_params { 19 | admin_state: ADMIN_STATE_ENABLED 20 | } 21 | node: 1 22 | } 23 | singleton_ports { 24 | id: 2 25 | name: "veth2" 26 | slot: 1 27 | port: 2 28 | channel: 1 29 | speed_bps: 100000000000 30 | config_params { 31 | admin_state: ADMIN_STATE_ENABLED 32 | } 33 | node: 1 34 | } 35 | singleton_ports { 36 | id: 3 37 | name: "veth4" 38 | slot: 1 39 | port: 3 40 | channel: 1 41 | speed_bps: 100000000000 42 | config_params { 43 | admin_state: ADMIN_STATE_ENABLED 44 | } 45 | node: 1 46 | } 47 | singleton_ports { 48 | id: 4 49 | name: "veth6" 50 | slot: 1 51 | port: 4 52 | channel: 1 53 | speed_bps: 100000000000 54 | config_params { 55 | admin_state: ADMIN_STATE_ENABLED 56 | } 57 | node: 1 58 | } 59 | singleton_ports { 60 | id: 5 61 | name: "veth8" 62 | slot: 1 63 | port: 5 64 | channel: 1 65 | speed_bps: 100000000000 66 | config_params { 67 | admin_state: ADMIN_STATE_ENABLED 68 | } 69 | node: 1 70 | } 71 | singleton_ports { 72 | id: 6 73 | name: "veth10" 74 | slot: 1 75 | port: 6 76 | channel: 1 77 | speed_bps: 100000000000 78 | config_params { 79 | admin_state: ADMIN_STATE_ENABLED 80 | } 81 | node: 1 82 | } 83 | singleton_ports { 84 | id: 7 85 | name: "veth12" 86 | slot: 1 87 | port: 7 88 | channel: 1 89 | speed_bps: 100000000000 90 | config_params { 91 | admin_state: ADMIN_STATE_ENABLED 92 | } 93 | node: 1 94 | } 95 | singleton_ports { 96 | id: 8 97 | name: "veth14" 98 | slot: 1 99 | port: 8 100 | channel: 1 101 | speed_bps: 100000000000 102 | config_params { 103 | admin_state: ADMIN_STATE_ENABLED 104 | } 105 | node: 1 106 | } -------------------------------------------------------------------------------- /ptf/lib/convert.py: -------------------------------------------------------------------------------- 1 | # Copyright 2017-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | import math 16 | import re 17 | import socket 18 | 19 | import ipaddress 20 | 21 | """ 22 | This package contains several helper functions for encoding to and decoding from 23 | byte strings: 24 | - integers 25 | - IPv4 address strings 26 | - IPv6 address strings 27 | - Ethernet address strings 28 | """ 29 | 30 | mac_pattern = re.compile(r'^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$') 31 | 32 | 33 | def matchesMac(mac_addr_string): 34 | return mac_pattern.match(mac_addr_string) is not None 35 | 36 | 37 | def encodeMac(mac_addr_string): 38 | return mac_addr_string.replace(':', '').decode('hex') 39 | 40 | 41 | def decodeMac(encoded_mac_addr): 42 | return ':'.join(s.encode('hex') for s in encoded_mac_addr) 43 | 44 | 45 | ip_pattern = re.compile(r'^(\d{1,3}\.){3}(\d{1,3})$') 46 | 47 | 48 | def matchesIPv4(ip_addr_string): 49 | return ip_pattern.match(ip_addr_string) is not None 50 | 51 | 52 | def encodeIPv4(ip_addr_string): 53 | return socket.inet_aton(ip_addr_string) 54 | 55 | 56 | def decodeIPv4(encoded_ip_addr): 57 | return socket.inet_ntoa(encoded_ip_addr) 58 | 59 | 60 | def matchesIPv6(ip_addr_string): 61 | try: 62 | addr = ipaddress.ip_address(unicode(ip_addr_string, "utf-8")) 63 | return isinstance(addr, ipaddress.IPv6Address) 64 | except ValueError: 65 | return False 66 | 67 | 68 | def encodeIPv6(ip_addr_string): 69 | return socket.inet_pton(socket.AF_INET6, ip_addr_string) 70 | 71 | 72 | def bitwidthToBytes(bitwidth): 73 | return int(math.ceil(bitwidth / 8.0)) 74 | 75 | 76 | def encodeNum(number, bitwidth): 77 | byte_len = bitwidthToBytes(bitwidth) 78 | num_str = '%x' % number 79 | if number >= 2 ** bitwidth: 80 | raise Exception( 81 | "Number, %d, does not fit in %d bits" % (number, bitwidth)) 82 | return ('0' * (byte_len * 2 - len(num_str)) + num_str).decode('hex') 83 | 84 | 85 | def decodeNum(encoded_number): 86 | return int(encoded_number.encode('hex'), 16) 87 | 88 | 89 | def encode(x, bitwidth): 90 | 'Tries to infer the type of `x` and encode it' 91 | byte_len = bitwidthToBytes(bitwidth) 92 | if (type(x) == list or type(x) == tuple) and len(x) == 1: 93 | x = x[0] 94 | encoded_bytes = None 95 | if type(x) == str: 96 | if matchesMac(x): 97 | encoded_bytes = encodeMac(x) 98 | elif matchesIPv4(x): 99 | encoded_bytes = encodeIPv4(x) 100 | elif matchesIPv6(x): 101 | encoded_bytes = encodeIPv6(x) 102 | else: 103 | # Assume that the string is already encoded 104 | encoded_bytes = x 105 | elif type(x) == int: 106 | encoded_bytes = encodeNum(x, bitwidth) 107 | else: 108 | raise Exception("Encoding objects of %r is not supported" % type(x)) 109 | assert (len(encoded_bytes) == byte_len) 110 | return encoded_bytes 111 | 112 | 113 | def test(): 114 | # TODO These tests should be moved out of main eventually 115 | mac = "aa:bb:cc:dd:ee:ff" 116 | enc_mac = encodeMac(mac) 117 | assert (enc_mac == '\xaa\xbb\xcc\xdd\xee\xff') 118 | dec_mac = decodeMac(enc_mac) 119 | assert (mac == dec_mac) 120 | 121 | ip = "10.0.0.1" 122 | enc_ip = encodeIPv4(ip) 123 | assert (enc_ip == '\x0a\x00\x00\x01') 124 | dec_ip = decodeIPv4(enc_ip) 125 | assert (ip == dec_ip) 126 | 127 | num = 1337 128 | byte_len = 5 129 | enc_num = encodeNum(num, byte_len * 8) 130 | assert (enc_num == '\x00\x00\x00\x05\x39') 131 | dec_num = decodeNum(enc_num) 132 | assert (num == dec_num) 133 | 134 | assert (matchesIPv4('10.0.0.1')) 135 | assert (not matchesIPv4('10.0.0.1.5')) 136 | assert (not matchesIPv4('1000.0.0.1')) 137 | assert (not matchesIPv4('10001')) 138 | 139 | assert (matchesIPv6('::1')) 140 | assert (encode('1:2:3:4:5:6:7:8', 128) == '\x00\x01\x00\x02\x00\x03\x00\x04\x00\x05\x00\x06\x00\x07\x00\x08') 141 | assert (matchesIPv6('2001:0000:85a3::8a2e:370:1111')) 142 | assert (not matchesIPv6('10.0.0.1')) 143 | 144 | assert (encode(mac, 6 * 8) == enc_mac) 145 | assert (encode(ip, 4 * 8) == enc_ip) 146 | assert (encode(num, 5 * 8) == enc_num) 147 | assert (encode((num,), 5 * 8) == enc_num) 148 | assert (encode([num], 5 * 8) == enc_num) 149 | 150 | num = 256 151 | try: 152 | encodeNum(num, 8) 153 | raise Exception("expected exception") 154 | except Exception as e: 155 | print e 156 | 157 | 158 | if __name__ == '__main__': 159 | test() 160 | -------------------------------------------------------------------------------- /ptf/lib/port_map.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ptf_port": 0, 4 | "p4_port": 1, 5 | "iface_name": "veth1" 6 | }, 7 | { 8 | "ptf_port": 1, 9 | "p4_port": 2, 10 | "iface_name": "veth3" 11 | }, 12 | { 13 | "ptf_port": 2, 14 | "p4_port": 3, 15 | "iface_name": "veth5" 16 | }, 17 | { 18 | "ptf_port": 3, 19 | "p4_port": 4, 20 | "iface_name": "veth7" 21 | }, 22 | { 23 | "ptf_port": 4, 24 | "p4_port": 5, 25 | "iface_name": "veth9" 26 | }, 27 | { 28 | "ptf_port": 5, 29 | "p4_port": 6, 30 | "iface_name": "veth11" 31 | }, 32 | { 33 | "ptf_port": 6, 34 | "p4_port": 7, 35 | "iface_name": "veth13" 36 | }, 37 | { 38 | "ptf_port": 7, 39 | "p4_port": 8, 40 | "iface_name": "veth15" 41 | } 42 | ] 43 | -------------------------------------------------------------------------------- /ptf/lib/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | # Copyright 2013-present Barefoot Networks, Inc. 4 | # Copyright 2018-present Open Networking Foundation 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | import Queue 19 | import argparse 20 | import json 21 | import logging 22 | import os 23 | import re 24 | import subprocess 25 | import sys 26 | import threading 27 | import time 28 | from collections import OrderedDict 29 | 30 | import google.protobuf.text_format 31 | import grpc 32 | from p4.v1 import p4runtime_pb2, p4runtime_pb2_grpc 33 | 34 | PTF_ROOT = os.path.dirname(os.path.realpath(__file__)) 35 | 36 | logging.basicConfig(level=logging.INFO) 37 | logger = logging.getLogger("PTF runner") 38 | 39 | 40 | def error(msg, *args, **kwargs): 41 | logger.error(msg, *args, **kwargs) 42 | 43 | 44 | def warn(msg, *args, **kwargs): 45 | logger.warn(msg, *args, **kwargs) 46 | 47 | 48 | def info(msg, *args, **kwargs): 49 | logger.info(msg, *args, **kwargs) 50 | 51 | 52 | def debug(msg, *args, **kwargs): 53 | logger.debug(msg, *args, **kwargs) 54 | 55 | 56 | def check_ifaces(ifaces): 57 | """ 58 | Checks that required interfaces exist. 59 | """ 60 | ifconfig_out = subprocess.check_output(['ifconfig']) 61 | iface_list = re.findall(r'^([a-zA-Z0-9]+)', ifconfig_out, re.S | re.M) 62 | present_ifaces = set(iface_list) 63 | ifaces = set(ifaces) 64 | return ifaces <= present_ifaces 65 | 66 | 67 | def build_bmv2_config(bmv2_json_path): 68 | """ 69 | Builds the device config for BMv2 70 | """ 71 | with open(bmv2_json_path) as f: 72 | return f.read() 73 | 74 | 75 | def update_config(p4info_path, bmv2_json_path, grpc_addr, device_id): 76 | """ 77 | Performs a SetForwardingPipelineConfig on the device 78 | """ 79 | channel = grpc.insecure_channel(grpc_addr) 80 | stub = p4runtime_pb2_grpc.P4RuntimeStub(channel) 81 | 82 | debug("Sending P4 config") 83 | 84 | # Send master arbitration via stream channel 85 | # This should go in library, to be re-used also by base_test.py. 86 | stream_out_q = Queue.Queue() 87 | stream_in_q = Queue.Queue() 88 | 89 | def stream_req_iterator(): 90 | while True: 91 | p = stream_out_q.get() 92 | if p is None: 93 | break 94 | yield p 95 | 96 | def stream_recv(stream): 97 | for p in stream: 98 | stream_in_q.put(p) 99 | 100 | def get_stream_packet(type_, timeout=1): 101 | start = time.time() 102 | try: 103 | while True: 104 | remaining = timeout - (time.time() - start) 105 | if remaining < 0: 106 | break 107 | msg = stream_in_q.get(timeout=remaining) 108 | if not msg.HasField(type_): 109 | continue 110 | return msg 111 | except: # timeout expired 112 | pass 113 | return None 114 | 115 | stream = stub.StreamChannel(stream_req_iterator()) 116 | stream_recv_thread = threading.Thread(target=stream_recv, args=(stream,)) 117 | stream_recv_thread.start() 118 | 119 | req = p4runtime_pb2.StreamMessageRequest() 120 | arbitration = req.arbitration 121 | arbitration.device_id = device_id 122 | election_id = arbitration.election_id 123 | election_id.high = 0 124 | election_id.low = 1 125 | stream_out_q.put(req) 126 | 127 | rep = get_stream_packet("arbitration", timeout=5) 128 | if rep is None: 129 | error("Failed to establish handshake") 130 | return False 131 | 132 | try: 133 | # Set pipeline config. 134 | request = p4runtime_pb2.SetForwardingPipelineConfigRequest() 135 | request.device_id = device_id 136 | election_id = request.election_id 137 | election_id.high = 0 138 | election_id.low = 1 139 | config = request.config 140 | with open(p4info_path, 'r') as p4info_f: 141 | google.protobuf.text_format.Merge(p4info_f.read(), config.p4info) 142 | config.p4_device_config = build_bmv2_config(bmv2_json_path) 143 | request.action = p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT 144 | try: 145 | stub.SetForwardingPipelineConfig(request) 146 | except Exception as e: 147 | error("Error during SetForwardingPipelineConfig") 148 | error(str(e)) 149 | return False 150 | return True 151 | finally: 152 | stream_out_q.put(None) 153 | stream_recv_thread.join() 154 | 155 | 156 | def run_test(p4info_path, grpc_addr, device_id, cpu_port, ptfdir, port_map_path, 157 | extra_args=()): 158 | """ 159 | Runs PTF tests included in provided directory. 160 | Device must be running and configfured with appropriate P4 program. 161 | """ 162 | # TODO: check schema? 163 | # "ptf_port" is ignored for now, we assume that ports are provided by 164 | # increasing values of ptf_port, in the range [0, NUM_IFACES[. 165 | port_map = OrderedDict() 166 | with open(port_map_path, 'r') as port_map_f: 167 | port_list = json.load(port_map_f) 168 | for entry in port_list: 169 | p4_port = entry["p4_port"] 170 | iface_name = entry["iface_name"] 171 | port_map[p4_port] = iface_name 172 | 173 | if not check_ifaces(port_map.values()): 174 | error("Some interfaces are missing") 175 | return False 176 | 177 | ifaces = [] 178 | # FIXME 179 | # find base_test.py 180 | pypath = os.path.dirname(os.path.abspath(__file__)) 181 | if 'PYTHONPATH' in os.environ: 182 | os.environ['PYTHONPATH'] += ":" + pypath 183 | else: 184 | os.environ['PYTHONPATH'] = pypath 185 | for iface_idx, iface_name in port_map.items(): 186 | ifaces.extend(['-i', '{}@{}'.format(iface_idx, iface_name)]) 187 | cmd = ['ptf'] 188 | cmd.extend(['--test-dir', ptfdir]) 189 | cmd.extend(ifaces) 190 | test_params = 'p4info=\'{}\''.format(p4info_path) 191 | test_params += ';grpcaddr=\'{}\''.format(grpc_addr) 192 | test_params += ';device_id=\'{}\''.format(device_id) 193 | test_params += ';cpu_port=\'{}\''.format(cpu_port) 194 | cmd.append('--test-params={}'.format(test_params)) 195 | cmd.extend(extra_args) 196 | debug("Executing PTF command: {}".format(' '.join(cmd))) 197 | 198 | try: 199 | # we want the ptf output to be sent to stdout 200 | p = subprocess.Popen(cmd) 201 | p.wait() 202 | except: 203 | error("Error when running PTF tests") 204 | return False 205 | return p.returncode == 0 206 | 207 | 208 | def check_ptf(): 209 | try: 210 | with open(os.devnull, 'w') as devnull: 211 | subprocess.check_call(['ptf', '--version'], 212 | stdout=devnull, stderr=devnull) 213 | return True 214 | except subprocess.CalledProcessError: 215 | return True 216 | except OSError: # PTF not found 217 | return False 218 | 219 | 220 | # noinspection PyTypeChecker 221 | def main(): 222 | parser = argparse.ArgumentParser( 223 | description="Compile the provided P4 program and run PTF tests on it") 224 | parser.add_argument('--p4info', 225 | help='Location of p4info proto in text format', 226 | type=str, action="store", required=True) 227 | parser.add_argument('--bmv2-json', 228 | help='Location BMv2 JSON output from p4c (if target is bmv2)', 229 | type=str, action="store", required=False) 230 | parser.add_argument('--grpc-addr', 231 | help='Address to use to connect to P4 Runtime server', 232 | type=str, default='localhost:50051') 233 | parser.add_argument('--device-id', 234 | help='Device id for device under test', 235 | type=int, default=1) 236 | parser.add_argument('--cpu-port', 237 | help='CPU port ID of device under test', 238 | type=int, required=True) 239 | parser.add_argument('--ptf-dir', 240 | help='Directory containing PTF tests', 241 | type=str, required=True) 242 | parser.add_argument('--port-map', 243 | help='Path to JSON port mapping', 244 | type=str, required=True) 245 | args, unknown_args = parser.parse_known_args() 246 | 247 | if not check_ptf(): 248 | error("Cannot find PTF executable") 249 | sys.exit(1) 250 | 251 | if not os.path.exists(args.p4info): 252 | error("P4Info file {} not found".format(args.p4info)) 253 | sys.exit(1) 254 | if not os.path.exists(args.bmv2_json): 255 | error("BMv2 json file {} not found".format(args.bmv2_json)) 256 | sys.exit(1) 257 | if not os.path.exists(args.port_map): 258 | print "Port map path '{}' does not exist".format(args.port_map) 259 | sys.exit(1) 260 | 261 | try: 262 | 263 | success = update_config(p4info_path=args.p4info, 264 | bmv2_json_path=args.bmv2_json, 265 | grpc_addr=args.grpc_addr, 266 | device_id=args.device_id) 267 | if not success: 268 | sys.exit(2) 269 | 270 | success = run_test(p4info_path=args.p4info, 271 | device_id=args.device_id, 272 | grpc_addr=args.grpc_addr, 273 | cpu_port=args.cpu_port, 274 | ptfdir=args.ptf_dir, 275 | port_map_path=args.port_map, 276 | extra_args=unknown_args) 277 | 278 | if not success: 279 | sys.exit(3) 280 | 281 | except Exception: 282 | raise 283 | 284 | 285 | if __name__ == '__main__': 286 | main() 287 | -------------------------------------------------------------------------------- /ptf/lib/start_bmv2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | 7 | CPU_PORT=255 8 | GRPC_PORT=28000 9 | 10 | # Create veths 11 | for idx in 0 1 2 3 4 5 6 7; do 12 | intf0="veth$(($idx*2))" 13 | intf1="veth$(($idx*2+1))" 14 | if ! ip link show $intf0 &> /dev/null; then 15 | ip link add name $intf0 type veth peer name $intf1 16 | ip link set dev $intf0 up 17 | ip link set dev $intf1 up 18 | 19 | # Set the MTU of these interfaces to be larger than default of 20 | # 1500 bytes, so that P4 behavioral-model testing can be done 21 | # on jumbo frames. 22 | ip link set $intf0 mtu 9500 23 | ip link set $intf1 mtu 9500 24 | 25 | # Disable IPv6 on the interfaces, so that the Linux kernel 26 | # will not automatically send IPv6 MDNS, Router Solicitation, 27 | # and Multicast Listener Report packets on the interface, 28 | # which can make P4 program debugging more confusing. 29 | # 30 | # Testing indicates that we can still send IPv6 packets across 31 | # such interfaces, both from scapy to simple_switch, and from 32 | # simple_switch out to scapy sniffing. 33 | # 34 | # https://superuser.com/questions/356286/how-can-i-switch-off-ipv6-nd-ra-transmissions-in-linux 35 | sysctl net.ipv6.conf.${intf0}.disable_ipv6=1 36 | sysctl net.ipv6.conf.${intf1}.disable_ipv6=1 37 | fi 38 | done 39 | 40 | # shellcheck disable=SC2086 41 | stratum_bmv2 \ 42 | --external_stratum_urls=0.0.0.0:${GRPC_PORT} \ 43 | --persistent_config_dir=/tmp \ 44 | --forwarding_pipeline_configs_file=/dev/null \ 45 | --chassis_config_file="${DIR}"/chassis_config.pb.txt \ 46 | --write_req_log_file=p4rt_write.log \ 47 | --initial_pipeline=/root/dummy.json \ 48 | --bmv2_log_level=trace \ 49 | --cpu_port 255 \ 50 | > stratum_bmv2.log 2>&1 51 | -------------------------------------------------------------------------------- /ptf/run_tests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | PTF_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | P4SRC_DIR=${PTF_DIR}/../p4src 7 | P4C_OUT=${P4SRC_DIR}/build 8 | PTF_DOCKER_IMG=${PTF_DOCKER_IMG:-undefined} 9 | 10 | runName=ptf-${RANDOM} 11 | 12 | function stop() { 13 | echo "Stopping container ${runName}..." 14 | docker stop -t0 "${runName}" > /dev/null 15 | } 16 | trap stop INT 17 | 18 | # Start container. Entrypoint starts stratum_bmv2. We put that in the background 19 | # and execute the PTF scripts separately. 20 | echo "*** Starting stratum_bmv2 in Docker (${runName})..." 21 | docker run --name "${runName}" -d --privileged --rm \ 22 | -v "${PTF_DIR}":/ptf -w /ptf \ 23 | -v "${P4C_OUT}":/p4c-out \ 24 | "${PTF_DOCKER_IMG}" \ 25 | ./lib/start_bmv2.sh > /dev/null 26 | 27 | sleep 2 28 | 29 | set +e 30 | 31 | printf "*** Starting tests...\n" 32 | docker exec "${runName}" ./lib/runner.py \ 33 | --bmv2-json /p4c-out/bmv2.json \ 34 | --p4info /p4c-out/p4info.txt \ 35 | --grpc-addr localhost:28000 \ 36 | --device-id 1 \ 37 | --ptf-dir ./tests \ 38 | --cpu-port 255 \ 39 | --port-map /ptf/lib/port_map.json "${@}" 40 | 41 | stop 42 | -------------------------------------------------------------------------------- /ptf/tests/bridging.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-present Barefoot Networks, Inc. 2 | # Copyright 2018-present Open Networking Foundation 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # ------------------------------------------------------------------------------ 18 | # BRIDGING TESTS 19 | # 20 | # To run all tests in this file: 21 | # make p4-test TEST=bridging 22 | # 23 | # To run a specific test case: 24 | # make p4-test TEST=bridging. 25 | # 26 | # For example: 27 | # make p4-test TEST=bridging.BridgingTest 28 | # ------------------------------------------------------------------------------ 29 | 30 | # ------------------------------------------------------------------------------ 31 | # Modify everywhere you see TODO 32 | # 33 | # When providing your solution, make sure to use the same names for P4Runtime 34 | # entities as specified in your P4Info file. 35 | # 36 | # Test cases are based on the P4 program design suggested in the exercises 37 | # README. Make sure to modify the test cases accordingly if you decide to 38 | # implement the pipeline differently. 39 | # ------------------------------------------------------------------------------ 40 | 41 | from ptf.testutils import group 42 | 43 | from base_test import * 44 | 45 | # From the P4 program. 46 | CPU_CLONE_SESSION_ID = 99 47 | 48 | 49 | @group("bridging") 50 | class ArpNdpRequestWithCloneTest(P4RuntimeTest): 51 | """Tests ability to broadcast ARP requests and NDP Neighbor Solicitation 52 | (NS) messages as well as cloning to CPU (controller) for host 53 | discovery. 54 | """ 55 | 56 | def runTest(self): 57 | # Test With both ARP and NDP NS packets... 58 | print_inline("ARP request ... ") 59 | arp_pkt = testutils.simple_arp_packet() 60 | self.testPacket(arp_pkt) 61 | 62 | print_inline("NDP NS ... ") 63 | ndp_pkt = genNdpNsPkt(src_mac=HOST1_MAC, src_ip=HOST1_IPV6, 64 | target_ip=HOST2_IPV6) 65 | self.testPacket(ndp_pkt) 66 | 67 | @autocleanup 68 | def testPacket(self, pkt): 69 | mcast_group_id = 10 70 | mcast_ports = [self.port1, self.port2, self.port3] 71 | 72 | # Add multicast group. 73 | self.insert_pre_multicast_group( 74 | group_id=mcast_group_id, 75 | ports=mcast_ports) 76 | 77 | # Match eth dst: FF:FF:FF:FF:FF:FF (MAC broadcast for ARP requests) 78 | # Modify names to match content of P4Info file (look for the fully 79 | # qualified name of tables, match fields, and actions. 80 | # ---- START SOLUTION ---- 81 | self.insert(self.helper.build_table_entry( 82 | table_name="IngressPipeImpl.l2_ternary_table", 83 | match_fields={ 84 | # Ternary match. 85 | "hdr.ethernet.dst_addr": ( 86 | "FF:FF:FF:FF:FF:FF", 87 | "FF:FF:FF:FF:FF:FF") 88 | }, 89 | action_name="IngressPipeImpl.set_multicast_group", 90 | action_params={ 91 | "gid": mcast_group_id 92 | }, 93 | priority=DEFAULT_PRIORITY 94 | )) 95 | # ---- END SOLUTION ---- 96 | 97 | # Match eth dst: 33:33:**:**:**:** (IPv6 multicast for NDP requests) 98 | # Modify names to match content of P4Info file (look for the fully 99 | # qualified name of tables, match fields, and actions. 100 | # ---- START SOLUTION ---- 101 | self.insert(self.helper.build_table_entry( 102 | table_name="IngressPipeImpl.l2_ternary_table", 103 | match_fields={ 104 | # Ternary match (value, mask) 105 | "hdr.ethernet.dst_addr": ( 106 | "33:33:00:00:00:00", 107 | "FF:FF:00:00:00:00") 108 | }, 109 | action_name="IngressPipeImpl.set_multicast_group", 110 | action_params={ 111 | "gid": mcast_group_id 112 | }, 113 | priority=DEFAULT_PRIORITY 114 | )) 115 | # ---- END SOLUTION ---- 116 | 117 | # Insert CPU clone session. 118 | self.insert_pre_clone_session( 119 | session_id=CPU_CLONE_SESSION_ID, 120 | ports=[self.cpu_port]) 121 | 122 | # ACL entry to clone ARPs 123 | self.insert(self.helper.build_table_entry( 124 | table_name="IngressPipeImpl.acl_table", 125 | match_fields={ 126 | # Ternary match. 127 | "hdr.ethernet.ether_type": (ARP_ETH_TYPE, 0xffff) 128 | }, 129 | action_name="IngressPipeImpl.clone_to_cpu", 130 | priority=DEFAULT_PRIORITY 131 | )) 132 | 133 | # ACL entry to clone NDP Neighbor Solicitation 134 | self.insert(self.helper.build_table_entry( 135 | table_name="IngressPipeImpl.acl_table", 136 | match_fields={ 137 | # Ternary match. 138 | "hdr.ethernet.ether_type": (IPV6_ETH_TYPE, 0xffff), 139 | "local_metadata.ip_proto": (ICMPV6_IP_PROTO, 0xff), 140 | "local_metadata.icmp_type": (NS_ICMPV6_TYPE, 0xff) 141 | }, 142 | action_name="IngressPipeImpl.clone_to_cpu", 143 | priority=DEFAULT_PRIORITY 144 | )) 145 | 146 | for inport in mcast_ports: 147 | 148 | # Send packet... 149 | testutils.send_packet(self, inport, str(pkt)) 150 | 151 | # Pkt should be received on CPU via PacketIn... 152 | # Expected P4Runtime PacketIn message. 153 | exp_packet_in_msg = self.helper.build_packet_in( 154 | payload=str(pkt), 155 | metadata={ 156 | "ingress_port": inport, 157 | "_pad": 0 158 | }) 159 | self.verify_packet_in(exp_packet_in_msg) 160 | 161 | # ...and on all ports except the ingress one. 162 | verify_ports = set(mcast_ports) 163 | verify_ports.discard(inport) 164 | for port in verify_ports: 165 | testutils.verify_packet(self, pkt, port) 166 | 167 | testutils.verify_no_other_packets(self) 168 | 169 | 170 | @group("bridging") 171 | class ArpNdpReplyWithCloneTest(P4RuntimeTest): 172 | """Tests ability to clone ARP replies and NDP Neighbor Advertisement 173 | (NA) messages as well as unicast forwarding to requesting host. 174 | """ 175 | 176 | def runTest(self): 177 | # Test With both ARP and NDP NS packets... 178 | print_inline("ARP reply ... ") 179 | # op=1 request, op=2 relpy 180 | arp_pkt = testutils.simple_arp_packet( 181 | eth_src=HOST1_MAC, eth_dst=HOST2_MAC, arp_op=2) 182 | self.testPacket(arp_pkt) 183 | 184 | print_inline("NDP NA ... ") 185 | ndp_pkt = genNdpNaPkt(target_ip=HOST1_IPV6, target_mac=HOST1_MAC) 186 | self.testPacket(ndp_pkt) 187 | 188 | @autocleanup 189 | def testPacket(self, pkt): 190 | 191 | # L2 unicast entry, match on pkt's eth dst address. 192 | # Modify names to match content of P4Info file (look for the fully 193 | # qualified name of tables, match fields, and actions. 194 | # ---- START SOLUTION ---- 195 | self.insert(self.helper.build_table_entry( 196 | table_name="IngressPipeImpl.l2_exact_table", 197 | match_fields={ 198 | # Exact match. 199 | "hdr.ethernet.dst_addr": pkt[Ether].dst 200 | }, 201 | action_name="IngressPipeImpl.set_egress_port", 202 | action_params={ 203 | "port_num": self.port2 204 | } 205 | )) 206 | # ---- END SOLUTION ---- 207 | 208 | # CPU clone session. 209 | self.insert_pre_clone_session( 210 | session_id=CPU_CLONE_SESSION_ID, 211 | ports=[self.cpu_port]) 212 | 213 | # ACL entry to clone ARPs 214 | self.insert(self.helper.build_table_entry( 215 | table_name="IngressPipeImpl.acl_table", 216 | match_fields={ 217 | # Ternary match. 218 | "hdr.ethernet.ether_type": (ARP_ETH_TYPE, 0xffff) 219 | }, 220 | action_name="IngressPipeImpl.clone_to_cpu", 221 | priority=DEFAULT_PRIORITY 222 | )) 223 | 224 | # ACL entry to clone NDP Neighbor Solicitation 225 | self.insert(self.helper.build_table_entry( 226 | table_name="IngressPipeImpl.acl_table", 227 | match_fields={ 228 | # Ternary match. 229 | "hdr.ethernet.ether_type": (IPV6_ETH_TYPE, 0xffff), 230 | "local_metadata.ip_proto": (ICMPV6_IP_PROTO, 0xff), 231 | "local_metadata.icmp_type": (NA_ICMPV6_TYPE, 0xff) 232 | }, 233 | action_name="IngressPipeImpl.clone_to_cpu", 234 | priority=DEFAULT_PRIORITY 235 | )) 236 | 237 | testutils.send_packet(self, self.port1, str(pkt)) 238 | 239 | # Pkt should be received on CPU via PacketIn... 240 | # Expected P4Runtime PacketIn message. 241 | exp_packet_in_msg = self.helper.build_packet_in( 242 | payload=str(pkt), 243 | metadata={ 244 | "ingress_port": self.port1, 245 | "_pad": 0 246 | }) 247 | self.verify_packet_in(exp_packet_in_msg) 248 | 249 | # ..and on port2 as indicated by the L2 unicast rule. 250 | testutils.verify_packet(self, pkt, self.port2) 251 | 252 | 253 | @group("bridging") 254 | class BridgingTest(P4RuntimeTest): 255 | """Tests basic L2 unicast forwarding""" 256 | 257 | def runTest(self): 258 | # Test with different type of packets. 259 | for pkt_type in ["tcp", "udp", "icmp", "tcpv6", "udpv6", "icmpv6"]: 260 | print_inline("%s ... " % pkt_type) 261 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)(pktlen=120) 262 | self.testPacket(pkt) 263 | 264 | @autocleanup 265 | def testPacket(self, pkt): 266 | 267 | # Insert L2 unicast entry, match on pkt's eth dst address. 268 | # Modify names to match content of P4Info file (look for the fully 269 | # qualified name of tables, match fields, and actions. 270 | # ---- START SOLUTION ---- 271 | self.insert(self.helper.build_table_entry( 272 | table_name="IngressPipeImpl.l2_exact_table", 273 | match_fields={ 274 | # Exact match. 275 | "hdr.ethernet.dst_addr": pkt[Ether].dst 276 | }, 277 | action_name="IngressPipeImpl.set_egress_port", 278 | action_params={ 279 | "port_num": self.port2 280 | } 281 | )) 282 | # ---- END SOLUTION ---- 283 | 284 | # Test bidirectional forwarding by swapping MAC addresses on the pkt 285 | pkt2 = pkt_mac_swap(pkt.copy()) 286 | 287 | # Insert L2 unicast entry for pkt2. 288 | # Modify names to match content of P4Info file (look for the fully 289 | # qualified name of tables, match fields, and actions. 290 | # ---- START SOLUTION ---- 291 | self.insert(self.helper.build_table_entry( 292 | table_name="IngressPipeImpl.l2_exact_table", 293 | match_fields={ 294 | # Exact match. 295 | "hdr.ethernet.dst_addr": pkt2[Ether].dst 296 | }, 297 | action_name="IngressPipeImpl.set_egress_port", 298 | action_params={ 299 | "port_num": self.port1 300 | } 301 | )) 302 | # ---- END SOLUTION ---- 303 | 304 | # Send and verify. 305 | testutils.send_packet(self, self.port1, str(pkt)) 306 | testutils.send_packet(self, self.port2, str(pkt2)) 307 | 308 | testutils.verify_each_packet_on_each_port( 309 | self, [pkt, pkt2], [self.port2, self.port1]) 310 | -------------------------------------------------------------------------------- /ptf/tests/packetio.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | # ------------------------------------------------------------------------------ 17 | # CONTROLLER PACKET-IN/OUT TESTS 18 | # 19 | # To run all tests in this file: 20 | # make p4-test TEST=packetio 21 | # 22 | # To run a specific test case: 23 | # make p4-test TEST=packetio. 24 | # 25 | # For example: 26 | # make p4-test TEST=packetio.PacketOutTest 27 | # ------------------------------------------------------------------------------ 28 | 29 | # ------------------------------------------------------------------------------ 30 | # Modify everywhere you see TODO 31 | # 32 | # When providing your solution, make sure to use the same names for P4Runtime 33 | # entities as specified in your P4Info file. 34 | # 35 | # Test cases are based on the P4 program design suggested in the exercises 36 | # README. Make sure to modify the test cases accordingly if you decide to 37 | # implement the pipeline differently. 38 | # ------------------------------------------------------------------------------ 39 | 40 | from ptf.testutils import group 41 | 42 | from base_test import * 43 | 44 | CPU_CLONE_SESSION_ID = 99 45 | 46 | 47 | @group("packetio") 48 | class PacketOutTest(P4RuntimeTest): 49 | """Tests controller packet-out capability by sending PacketOut messages and 50 | expecting a corresponding packet on the output port set in the PacketOut 51 | metadata. 52 | """ 53 | 54 | def runTest(self): 55 | for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6", 56 | "icmpv6"]: 57 | print_inline("%s ... " % pkt_type) 58 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 59 | self.testPacket(pkt) 60 | 61 | def testPacket(self, pkt): 62 | for outport in [self.port1, self.port2]: 63 | # Build PacketOut message. 64 | # TODO EXERCISE 4 65 | # Modify metadata names to match the content of your P4Info file 66 | # ---- START SOLUTION ---- 67 | packet_out_msg = self.helper.build_packet_out( 68 | payload=str(pkt), 69 | metadata={ 70 | "MODIFY ME": outport, 71 | "_pad": 0 72 | }) 73 | # ---- END SOLUTION ---- 74 | 75 | # Send message and expect packet on the given data plane port. 76 | self.send_packet_out(packet_out_msg) 77 | 78 | testutils.verify_packet(self, pkt, outport) 79 | 80 | # Make sure packet was forwarded only on the specified ports 81 | testutils.verify_no_other_packets(self) 82 | 83 | 84 | @group("packetio") 85 | class PacketInTest(P4RuntimeTest): 86 | """Tests controller packet-in capability my matching on the packet EtherType 87 | and cloning to the CPU port. 88 | """ 89 | 90 | def runTest(self): 91 | for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6", 92 | "icmpv6"]: 93 | print_inline("%s ... " % pkt_type) 94 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 95 | self.testPacket(pkt) 96 | 97 | @autocleanup 98 | def testPacket(self, pkt): 99 | 100 | # Insert clone to CPU session 101 | self.insert_pre_clone_session( 102 | session_id=CPU_CLONE_SESSION_ID, 103 | ports=[self.cpu_port]) 104 | 105 | # Insert ACL entry to match on the given eth_type and clone to CPU. 106 | eth_type = pkt[Ether].type 107 | # TODO EXERCISE 4 108 | # Modify names to match content of P4Info file (look for the fully 109 | # qualified name of the ACL table, EtherType match field, and 110 | # clone_to_cpu action. 111 | # ---- START SOLUTION ---- 112 | self.insert(self.helper.build_table_entry( 113 | table_name="MODIFY ME", 114 | match_fields={ 115 | # Ternary match. 116 | "MODIFY ME": (eth_type, 0xffff) 117 | }, 118 | action_name="MODIFY ME", 119 | priority=DEFAULT_PRIORITY 120 | )) 121 | # ---- END SOLUTION ---- 122 | 123 | for inport in [self.port1, self.port2, self.port3]: 124 | # TODO EXERCISE 4 125 | # Modify metadata names to match the content of your P4Info file 126 | # ---- START SOLUTION ---- 127 | # Expected P4Runtime PacketIn message. 128 | exp_packet_in_msg = self.helper.build_packet_in( 129 | payload=str(pkt), 130 | metadata={ 131 | "MODIFY ME": inport, 132 | "_pad": 0 133 | }) 134 | # ---- END SOLUTION ---- 135 | 136 | # Send packet to given switch ingress port and expect P4Runtime 137 | # PacketIn message. 138 | testutils.send_packet(self, inport, str(pkt)) 139 | self.verify_packet_in(exp_packet_in_msg) 140 | -------------------------------------------------------------------------------- /ptf/tests/routing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | # ------------------------------------------------------------------------------ 17 | # IPV6 ROUTING TESTS 18 | # 19 | # To run all tests: 20 | # make p4-test TEST=routing 21 | # 22 | # To run a specific test case: 23 | # make p4-test TEST=routing. 24 | # 25 | # For example: 26 | # make p4-test TEST=routing.IPv6RoutingTest 27 | # ------------------------------------------------------------------------------ 28 | 29 | # ------------------------------------------------------------------------------ 30 | # Modify everywhere you see TODO 31 | # 32 | # When providing your solution, make sure to use the same names for P4Runtime 33 | # entities as specified in your P4Info file. 34 | # 35 | # Test cases are based on the P4 program design suggested in the exercises 36 | # README. Make sure to modify the test cases accordingly if you decide to 37 | # implement the pipeline differently. 38 | # ------------------------------------------------------------------------------ 39 | 40 | from ptf.testutils import group 41 | 42 | from base_test import * 43 | 44 | 45 | @group("routing") 46 | class IPv6RoutingTest(P4RuntimeTest): 47 | """Tests basic IPv6 routing""" 48 | 49 | def runTest(self): 50 | # Test with different type of packets. 51 | for pkt_type in ["tcpv6", "udpv6", "icmpv6"]: 52 | print_inline("%s ... " % pkt_type) 53 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 54 | self.testPacket(pkt) 55 | 56 | @autocleanup 57 | def testPacket(self, pkt): 58 | next_hop_mac = SWITCH2_MAC 59 | 60 | # Add entry to "My Station" table. Consider the given pkt's eth dst addr 61 | # as myStationMac address. 62 | # *** TODO EXERCISE 5 63 | # Modify names to match content of P4Info file (look for the fully 64 | # qualified name of tables, match fields, and actions. 65 | # ---- START SOLUTION ---- 66 | self.insert(self.helper.build_table_entry( 67 | table_name="MODIFY ME", 68 | match_fields={ 69 | # Exact match. 70 | "MODIFY ME": pkt[Ether].dst 71 | }, 72 | action_name="NoAction" 73 | )) 74 | # ---- END SOLUTION ---- 75 | 76 | # Insert ECMP group with only one member (next_hop_mac) 77 | # *** TODO EXERCISE 5 78 | # Modify names to match content of P4Info file (look for the fully 79 | # qualified name of tables, match fields, and actions. 80 | # ---- START SOLUTION ---- 81 | self.insert(self.helper.build_act_prof_group( 82 | act_prof_name="MODIFY ME", 83 | group_id=1, 84 | actions=[ 85 | # List of tuples (action name, action param dict) 86 | ("MODIFY ME", {"MODIFY ME": next_hop_mac}), 87 | ] 88 | )) 89 | # ---- END SOLUTION ---- 90 | 91 | # Insert L3 routing entry to map pkt's IPv6 dst addr to group 92 | # *** TODO EXERCISE 5 93 | # Modify names to match content of P4Info file (look for the fully 94 | # qualified name of tables, match fields, and actions. 95 | # ---- START SOLUTION ---- 96 | self.insert(self.helper.build_table_entry( 97 | table_name="MODIFY ME", 98 | match_fields={ 99 | # LPM match (value, prefix) 100 | "MODIFY ME": (pkt[IPv6].dst, 128) 101 | }, 102 | group_id=1 103 | )) 104 | # ---- END SOLUTION ---- 105 | 106 | # Insert L3 entry to map next_hop_mac to output port 2. 107 | # *** TODO EXERCISE 5 108 | # Modify names to match content of P4Info file (look for the fully 109 | # qualified name of tables, match fields, and actions. 110 | # ---- START SOLUTION ---- 111 | self.insert(self.helper.build_table_entry( 112 | table_name="MODIFY ME", 113 | match_fields={ 114 | # Exact match 115 | "MODIFY ME": next_hop_mac 116 | }, 117 | action_name="MODIFY ME", 118 | action_params={ 119 | "MODIFY ME": self.port2 120 | } 121 | )) 122 | # ---- END SOLUTION ---- 123 | 124 | # Expected pkt should have routed MAC addresses and decremented hop 125 | # limit (TTL). 126 | exp_pkt = pkt.copy() 127 | pkt_route(exp_pkt, next_hop_mac) 128 | pkt_decrement_ttl(exp_pkt) 129 | 130 | testutils.send_packet(self, self.port1, str(pkt)) 131 | testutils.verify_packet(self, exp_pkt, self.port2) 132 | 133 | 134 | @group("routing") 135 | class NdpReplyGenTest(P4RuntimeTest): 136 | """Tests automatic generation of NDP Neighbor Advertisement for IPV6 137 | addresses associated to the switch interface. 138 | """ 139 | 140 | @autocleanup 141 | def runTest(self): 142 | switch_ip = SWITCH1_IPV6 143 | target_mac = SWITCH1_MAC 144 | 145 | # Insert entry to transform NDP NA packets for the given target address 146 | # (match), to NDP NA packets with the given target MAC address (action 147 | # *** TODO EXERCISE 5 148 | # Modify names to match content of P4Info file (look for the fully 149 | # qualified name of tables, match fields, and actions. 150 | # ---- START SOLUTION ---- 151 | self.insert(self.helper.build_table_entry( 152 | table_name="MODIFY ME", 153 | match_fields={ 154 | # Exact match. 155 | "MODIFY ME": switch_ip 156 | }, 157 | action_name="MODIFY ME", 158 | action_params={ 159 | "MODIFY ME": target_mac 160 | } 161 | )) 162 | # ---- END SOLUTION ---- 163 | 164 | # NDP Neighbor Solicitation packet 165 | pkt = genNdpNsPkt(target_ip=switch_ip) 166 | 167 | # NDP Neighbor Advertisement packet 168 | exp_pkt = genNdpNaPkt(target_ip=switch_ip, 169 | target_mac=target_mac, 170 | src_mac=target_mac, 171 | src_ip=switch_ip, 172 | dst_ip=pkt[IPv6].src) 173 | 174 | # Send NDP NS, expect NDP NA from the same port. 175 | testutils.send_packet(self, self.port1, str(pkt)) 176 | testutils.verify_packet(self, exp_pkt, self.port1) -------------------------------------------------------------------------------- /solution/app/src/main/java/org/onosproject/ngsdn/tutorial/pipeconf/InterpreterImpl.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-present Open Networking Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.onosproject.ngsdn.tutorial.pipeconf; 18 | 19 | import com.google.common.collect.ImmutableList; 20 | import com.google.common.collect.ImmutableMap; 21 | import org.onlab.packet.DeserializationException; 22 | import org.onlab.packet.Ethernet; 23 | import org.onlab.util.ImmutableByteSequence; 24 | import org.onosproject.net.ConnectPoint; 25 | import org.onosproject.net.DeviceId; 26 | import org.onosproject.net.Port; 27 | import org.onosproject.net.PortNumber; 28 | import org.onosproject.net.device.DeviceService; 29 | import org.onosproject.net.driver.AbstractHandlerBehaviour; 30 | import org.onosproject.net.flow.TrafficTreatment; 31 | import org.onosproject.net.flow.criteria.Criterion; 32 | import org.onosproject.net.packet.DefaultInboundPacket; 33 | import org.onosproject.net.packet.InboundPacket; 34 | import org.onosproject.net.packet.OutboundPacket; 35 | import org.onosproject.net.pi.model.PiMatchFieldId; 36 | import org.onosproject.net.pi.model.PiPacketMetadataId; 37 | import org.onosproject.net.pi.model.PiPipelineInterpreter; 38 | import org.onosproject.net.pi.model.PiTableId; 39 | import org.onosproject.net.pi.runtime.PiAction; 40 | import org.onosproject.net.pi.runtime.PiPacketMetadata; 41 | import org.onosproject.net.pi.runtime.PiPacketOperation; 42 | 43 | import java.nio.ByteBuffer; 44 | import java.util.Collection; 45 | import java.util.List; 46 | import java.util.Map; 47 | import java.util.Optional; 48 | 49 | import static java.lang.String.format; 50 | import static java.util.stream.Collectors.toList; 51 | import static org.onlab.util.ImmutableByteSequence.copyFrom; 52 | import static org.onosproject.net.PortNumber.CONTROLLER; 53 | import static org.onosproject.net.PortNumber.FLOOD; 54 | import static org.onosproject.net.flow.instructions.Instruction.Type.OUTPUT; 55 | import static org.onosproject.net.flow.instructions.Instructions.OutputInstruction; 56 | import static org.onosproject.net.pi.model.PiPacketOperationType.PACKET_OUT; 57 | import static org.onosproject.ngsdn.tutorial.AppConstants.CPU_PORT_ID; 58 | 59 | 60 | /** 61 | * Interpreter implementation. 62 | */ 63 | public class InterpreterImpl extends AbstractHandlerBehaviour 64 | implements PiPipelineInterpreter { 65 | 66 | 67 | // From v1model.p4 68 | private static final int V1MODEL_PORT_BITWIDTH = 9; 69 | 70 | // From P4Info. 71 | private static final Map CRITERION_MAP = 72 | new ImmutableMap.Builder() 73 | .put(Criterion.Type.IN_PORT, "standard_metadata.ingress_port") 74 | .put(Criterion.Type.ETH_DST, "hdr.ethernet.dst_addr") 75 | .put(Criterion.Type.ETH_SRC, "hdr.ethernet.src_addr") 76 | .put(Criterion.Type.ETH_TYPE, "hdr.ethernet.ether_type") 77 | .put(Criterion.Type.IPV6_DST, "hdr.ipv6.dst_addr") 78 | .put(Criterion.Type.IP_PROTO, "local_metadata.ip_proto") 79 | .put(Criterion.Type.ICMPV4_TYPE, "local_metadata.icmp_type") 80 | .put(Criterion.Type.ICMPV6_TYPE, "local_metadata.icmp_type") 81 | .build(); 82 | 83 | /** 84 | * Returns a collection of PI packet operations populated with metadata 85 | * specific for this pipeconf and equivalent to the given ONOS 86 | * OutboundPacket instance. 87 | * 88 | * @param packet ONOS OutboundPacket 89 | * @return collection of PI packet operations 90 | * @throws PiInterpreterException if the packet treatments cannot be 91 | * executed by this pipeline 92 | */ 93 | @Override 94 | public Collection mapOutboundPacket(OutboundPacket packet) 95 | throws PiInterpreterException { 96 | TrafficTreatment treatment = packet.treatment(); 97 | 98 | // Packet-out in main.p4 supports only setting the output port, 99 | // i.e. we only understand OUTPUT instructions. 100 | List outInstructions = treatment 101 | .allInstructions() 102 | .stream() 103 | .filter(i -> i.type().equals(OUTPUT)) 104 | .map(i -> (OutputInstruction) i) 105 | .collect(toList()); 106 | 107 | if (treatment.allInstructions().size() != outInstructions.size()) { 108 | // There are other instructions that are not of type OUTPUT. 109 | throw new PiInterpreterException("Treatment not supported: " + treatment); 110 | } 111 | 112 | ImmutableList.Builder builder = ImmutableList.builder(); 113 | for (OutputInstruction outInst : outInstructions) { 114 | if (outInst.port().isLogical() && !outInst.port().equals(FLOOD)) { 115 | throw new PiInterpreterException(format( 116 | "Packet-out on logical port '%s' not supported", 117 | outInst.port())); 118 | } else if (outInst.port().equals(FLOOD)) { 119 | // To emulate flooding, we create a packet-out operation for 120 | // each switch port. 121 | final DeviceService deviceService = handler().get(DeviceService.class); 122 | for (Port port : deviceService.getPorts(packet.sendThrough())) { 123 | builder.add(buildPacketOut(packet.data(), port.number().toLong())); 124 | } 125 | } else { 126 | // Create only one packet-out for the given OUTPUT instruction. 127 | builder.add(buildPacketOut(packet.data(), outInst.port().toLong())); 128 | } 129 | } 130 | return builder.build(); 131 | } 132 | 133 | /** 134 | * Builds a pipeconf-specific packet-out instance with the given payload and 135 | * egress port. 136 | * 137 | * @param pktData packet payload 138 | * @param portNumber egress port 139 | * @return packet-out 140 | * @throws PiInterpreterException if packet-out cannot be built 141 | */ 142 | private PiPacketOperation buildPacketOut(ByteBuffer pktData, long portNumber) 143 | throws PiInterpreterException { 144 | 145 | // Make sure port number can fit in v1model port metadata bitwidth. 146 | final ImmutableByteSequence portBytes; 147 | try { 148 | portBytes = copyFrom(portNumber).fit(V1MODEL_PORT_BITWIDTH); 149 | } catch (ImmutableByteSequence.ByteSequenceTrimException e) { 150 | throw new PiInterpreterException(format( 151 | "Port number %d too big, %s", portNumber, e.getMessage())); 152 | } 153 | 154 | // Create metadata instance for egress port. 155 | // *** TODO EXERCISE 4: modify metadata names to match P4 program 156 | // ---- START SOLUTION ---- 157 | final String outPortMetadataName = "egress_port"; 158 | // ---- END SOLUTION ---- 159 | final PiPacketMetadata outPortMetadata = PiPacketMetadata.builder() 160 | .withId(PiPacketMetadataId.of(outPortMetadataName)) 161 | .withValue(portBytes) 162 | .build(); 163 | 164 | // Build packet out. 165 | return PiPacketOperation.builder() 166 | .withType(PACKET_OUT) 167 | .withData(copyFrom(pktData)) 168 | .withMetadata(outPortMetadata) 169 | .build(); 170 | } 171 | 172 | /** 173 | * Returns an ONS InboundPacket equivalent to the given pipeconf-specific 174 | * packet-in operation. 175 | * 176 | * @param packetIn packet operation 177 | * @param deviceId ID of the device that originated the packet-in 178 | * @return inbound packet 179 | * @throws PiInterpreterException if the packet operation cannot be mapped 180 | * to an inbound packet 181 | */ 182 | @Override 183 | public InboundPacket mapInboundPacket(PiPacketOperation packetIn, DeviceId deviceId) 184 | throws PiInterpreterException { 185 | 186 | // Find the ingress_port metadata. 187 | // *** TODO EXERCISE 4: modify metadata names to match P4Info 188 | // ---- START SOLUTION ---- 189 | final String inportMetadataName = "ingress_port"; 190 | // ---- END SOLUTION ---- 191 | Optional inportMetadata = packetIn.metadatas() 192 | .stream() 193 | .filter(meta -> meta.id().id().equals(inportMetadataName)) 194 | .findFirst(); 195 | 196 | if (!inportMetadata.isPresent()) { 197 | throw new PiInterpreterException(format( 198 | "Missing metadata '%s' in packet-in received from '%s': %s", 199 | inportMetadataName, deviceId, packetIn)); 200 | } 201 | 202 | // Build ONOS InboundPacket instance with the given ingress port. 203 | 204 | // 1. Parse packet-in object into Ethernet packet instance. 205 | final byte[] payloadBytes = packetIn.data().asArray(); 206 | final ByteBuffer rawData = ByteBuffer.wrap(payloadBytes); 207 | final Ethernet ethPkt; 208 | try { 209 | ethPkt = Ethernet.deserializer().deserialize( 210 | payloadBytes, 0, packetIn.data().size()); 211 | } catch (DeserializationException dex) { 212 | throw new PiInterpreterException(dex.getMessage()); 213 | } 214 | 215 | // 2. Get ingress port 216 | final ImmutableByteSequence portBytes = inportMetadata.get().value(); 217 | final short portNum = portBytes.asReadOnlyBuffer().getShort(); 218 | final ConnectPoint receivedFrom = new ConnectPoint( 219 | deviceId, PortNumber.portNumber(portNum)); 220 | 221 | return new DefaultInboundPacket(receivedFrom, ethPkt, rawData); 222 | } 223 | 224 | @Override 225 | public Optional mapLogicalPortNumber(PortNumber port) { 226 | if (CONTROLLER.equals(port)) { 227 | return Optional.of(CPU_PORT_ID); 228 | } else { 229 | return Optional.empty(); 230 | } 231 | } 232 | 233 | @Override 234 | public Optional mapCriterionType(Criterion.Type type) { 235 | if (CRITERION_MAP.containsKey(type)) { 236 | return Optional.of(PiMatchFieldId.of(CRITERION_MAP.get(type))); 237 | } else { 238 | return Optional.empty(); 239 | } 240 | } 241 | 242 | @Override 243 | public PiAction mapTreatment(TrafficTreatment treatment, PiTableId piTableId) 244 | throws PiInterpreterException { 245 | throw new PiInterpreterException("Treatment mapping not supported"); 246 | } 247 | 248 | @Override 249 | public Optional mapFlowRuleTableId(int flowRuleTableId) { 250 | return Optional.empty(); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /solution/mininet/flowrule-gtp.json: -------------------------------------------------------------------------------- 1 | { 2 | "flows": [ 3 | { 4 | "deviceId": "device:leaf1", 5 | "tableId": "FabricIngress.spgw_ingress.dl_sess_lookup", 6 | "priority": 10, 7 | "timeout": 0, 8 | "isPermanent": true, 9 | "selector": { 10 | "criteria": [ 11 | { 12 | "type": "IPV4_DST", 13 | "ip": "17.0.0.1/32" 14 | } 15 | ] 16 | }, 17 | "treatment": { 18 | "instructions": [ 19 | { 20 | "type": "PROTOCOL_INDEPENDENT", 21 | "subtype": "ACTION", 22 | "actionId": "FabricIngress.spgw_ingress.set_dl_sess_info", 23 | "actionParams": { 24 | "teid": "BEEF", 25 | "s1u_enb_addr": "0a006401", 26 | "s1u_sgw_addr": "0a0064fe" 27 | } 28 | } 29 | ] 30 | } 31 | } 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /solution/mininet/netcfg-gtp.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": { 3 | "device:leaf1": { 4 | "basic": { 5 | "managementAddress": "grpc://mininet:50001?device_id=1", 6 | "driver": "stratum-bmv2", 7 | "pipeconf": "org.onosproject.pipelines.fabric-spgw", 8 | "locType": "grid", 9 | "gridX": 200, 10 | "gridY": 600 11 | }, 12 | "segmentrouting": { 13 | "name": "leaf1", 14 | "ipv4NodeSid": 101, 15 | "ipv4Loopback": "192.168.1.1", 16 | "routerMac": "00:AA:00:00:00:01", 17 | "isEdgeRouter": true, 18 | "adjacencySids": [] 19 | } 20 | }, 21 | "device:leaf2": { 22 | "basic": { 23 | "managementAddress": "grpc://mininet:50002?device_id=1", 24 | "driver": "stratum-bmv2", 25 | "pipeconf": "org.onosproject.pipelines.fabric", 26 | "locType": "grid", 27 | "gridX": 800, 28 | "gridY": 600 29 | }, 30 | "segmentrouting": { 31 | "name": "leaf2", 32 | "ipv4NodeSid": 102, 33 | "ipv4Loopback": "192.168.1.2", 34 | "routerMac": "00:AA:00:00:00:02", 35 | "isEdgeRouter": true, 36 | "adjacencySids": [] 37 | } 38 | }, 39 | "device:spine1": { 40 | "basic": { 41 | "managementAddress": "grpc://mininet:50003?device_id=1", 42 | "driver": "stratum-bmv2", 43 | "pipeconf": "org.onosproject.pipelines.fabric", 44 | "locType": "grid", 45 | "gridX": 400, 46 | "gridY": 400 47 | }, 48 | "segmentrouting": { 49 | "name": "spine1", 50 | "ipv4NodeSid": 201, 51 | "ipv4Loopback": "192.168.2.1", 52 | "routerMac": "00:BB:00:00:00:01", 53 | "isEdgeRouter": false, 54 | "adjacencySids": [] 55 | } 56 | }, 57 | "device:spine2": { 58 | "basic": { 59 | "managementAddress": "grpc://mininet:50004?device_id=1", 60 | "driver": "stratum-bmv2", 61 | "pipeconf": "org.onosproject.pipelines.fabric", 62 | "locType": "grid", 63 | "gridX": 600, 64 | "gridY": 400 65 | }, 66 | "segmentrouting": { 67 | "name": "spine2", 68 | "ipv4NodeSid": 202, 69 | "ipv4Loopback": "192.168.2.2", 70 | "routerMac": "00:BB:00:00:00:02", 71 | "isEdgeRouter": false, 72 | "adjacencySids": [] 73 | } 74 | } 75 | }, 76 | "ports": { 77 | "device:leaf1/3": { 78 | "interfaces": [ 79 | { 80 | "name": "leaf1-3", 81 | "ips": [ 82 | "10.0.100.254/24" 83 | ], 84 | "vlan-untagged": 100 85 | } 86 | ] 87 | }, 88 | "device:leaf2/3": { 89 | "interfaces": [ 90 | { 91 | "name": "leaf2-3", 92 | "ips": [ 93 | "10.0.200.254/24" 94 | ], 95 | "vlan-untagged": 200 96 | } 97 | ] 98 | } 99 | }, 100 | "hosts": { 101 | "00:00:00:00:00:10/None": { 102 | "basic": { 103 | "name": "enodeb", 104 | "gridX": 100, 105 | "gridY": 700, 106 | "locType": "grid", 107 | "ips": [ 108 | "10.0.100.1" 109 | ], 110 | "locations": [ 111 | "device:leaf1/3" 112 | ] 113 | } 114 | }, 115 | "00:00:00:00:00:20/None": { 116 | "basic": { 117 | "name": "pdn", 118 | "gridX": 850, 119 | "gridY": 700, 120 | "locType": "grid", 121 | "ips": [ 122 | "10.0.200.1" 123 | ], 124 | "locations": [ 125 | "device:leaf2/3" 126 | ] 127 | } 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /solution/mininet/netcfg-sr.json: -------------------------------------------------------------------------------- 1 | { 2 | "devices": { 3 | "device:leaf1": { 4 | "basic": { 5 | "managementAddress": "grpc://mininet:50001?device_id=1", 6 | "driver": "stratum-bmv2", 7 | "pipeconf": "org.onosproject.pipelines.fabric", 8 | "locType": "grid", 9 | "gridX": 200, 10 | "gridY": 600 11 | }, 12 | "segmentrouting": { 13 | "name": "leaf1", 14 | "ipv4NodeSid": 101, 15 | "ipv4Loopback": "192.168.1.1", 16 | "routerMac": "00:AA:00:00:00:01", 17 | "isEdgeRouter": true, 18 | "adjacencySids": [] 19 | } 20 | }, 21 | "device:leaf2": { 22 | "basic": { 23 | "managementAddress": "grpc://mininet:50002?device_id=1", 24 | "driver": "stratum-bmv2", 25 | "pipeconf": "org.onosproject.pipelines.fabric", 26 | "locType": "grid", 27 | "gridX": 800, 28 | "gridY": 600 29 | }, 30 | "segmentrouting": { 31 | "name": "leaf2", 32 | "ipv4NodeSid": 102, 33 | "ipv4Loopback": "192.168.1.2", 34 | "routerMac": "00:AA:00:00:00:02", 35 | "isEdgeRouter": true, 36 | "adjacencySids": [] 37 | } 38 | }, 39 | "device:spine1": { 40 | "basic": { 41 | "managementAddress": "grpc://mininet:50003?device_id=1", 42 | "driver": "stratum-bmv2", 43 | "pipeconf": "org.onosproject.pipelines.fabric", 44 | "locType": "grid", 45 | "gridX": 400, 46 | "gridY": 400 47 | }, 48 | "segmentrouting": { 49 | "name": "spine1", 50 | "ipv4NodeSid": 201, 51 | "ipv4Loopback": "192.168.2.1", 52 | "routerMac": "00:BB:00:00:00:01", 53 | "isEdgeRouter": false, 54 | "adjacencySids": [] 55 | } 56 | }, 57 | "device:spine2": { 58 | "basic": { 59 | "managementAddress": "grpc://mininet:50004?device_id=1", 60 | "driver": "stratum-bmv2", 61 | "pipeconf": "org.onosproject.pipelines.fabric", 62 | "locType": "grid", 63 | "gridX": 600, 64 | "gridY": 400 65 | }, 66 | "segmentrouting": { 67 | "name": "spine2", 68 | "ipv4NodeSid": 202, 69 | "ipv4Loopback": "192.168.2.2", 70 | "routerMac": "00:BB:00:00:00:02", 71 | "isEdgeRouter": false, 72 | "adjacencySids": [] 73 | } 74 | } 75 | }, 76 | "ports": { 77 | "device:leaf1/3": { 78 | "interfaces": [ 79 | { 80 | "name": "leaf1-3", 81 | "ips": [ 82 | "172.16.1.254/24" 83 | ], 84 | "vlan-untagged": 100 85 | } 86 | ] 87 | }, 88 | "device:leaf1/4": { 89 | "interfaces": [ 90 | { 91 | "name": "leaf1-4", 92 | "ips": [ 93 | "172.16.1.254/24" 94 | ], 95 | "vlan-untagged": 100 96 | } 97 | ] 98 | }, 99 | "device:leaf1/5": { 100 | "interfaces": [ 101 | { 102 | "name": "leaf1-5", 103 | "ips": [ 104 | "172.16.1.254/24" 105 | ], 106 | "vlan-tagged": [ 107 | 100 108 | ] 109 | } 110 | ] 111 | }, 112 | "device:leaf1/6": { 113 | "interfaces": [ 114 | { 115 | "name": "leaf1-6", 116 | "ips": [ 117 | "172.16.2.254/24" 118 | ], 119 | "vlan-tagged": [ 120 | 200 121 | ] 122 | } 123 | ] 124 | }, 125 | "device:leaf2/3": { 126 | "interfaces": [ 127 | { 128 | "name": "leaf2-3", 129 | "ips": [ 130 | "172.16.3.254/24" 131 | ], 132 | "vlan-tagged": [ 133 | 300 134 | ] 135 | } 136 | ] 137 | }, 138 | "device:leaf2/4": { 139 | "interfaces": [ 140 | { 141 | "name": "leaf2-4", 142 | "ips": [ 143 | "172.16.4.254/24" 144 | ], 145 | "vlan-untagged": 400 146 | } 147 | ] 148 | } 149 | }, 150 | "hosts": { 151 | "00:00:00:00:00:1A/None": { 152 | "basic": { 153 | "name": "h1a", 154 | "locType": "grid", 155 | "gridX": 100, 156 | "gridY": 700 157 | } 158 | }, 159 | "00:00:00:00:00:1B/None": { 160 | "basic": { 161 | "name": "h1b", 162 | "locType": "grid", 163 | "gridX": 100, 164 | "gridY": 800 165 | } 166 | }, 167 | "00:00:00:00:00:1C/100": { 168 | "basic": { 169 | "name": "h1c", 170 | "locType": "grid", 171 | "gridX": 250, 172 | "gridY": 800 173 | } 174 | }, 175 | "00:00:00:00:00:20/200": { 176 | "basic": { 177 | "name": "h2", 178 | "locType": "grid", 179 | "gridX": 400, 180 | "gridY": 700 181 | } 182 | }, 183 | "00:00:00:00:00:30/300": { 184 | "basic": { 185 | "name": "h3", 186 | "locType": "grid", 187 | "gridX": 750, 188 | "gridY": 700 189 | } 190 | }, 191 | "00:00:00:00:00:40/None": { 192 | "basic": { 193 | "name": "h4", 194 | "locType": "grid", 195 | "gridX": 850, 196 | "gridY": 700 197 | } 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /solution/ptf/tests/packetio.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | # ------------------------------------------------------------------------------ 17 | # CONTROLLER PACKET-IN/OUT TESTS 18 | # 19 | # To run all tests in this file: 20 | # make p4-test TEST=packetio 21 | # 22 | # To run a specific test case: 23 | # make p4-test TEST=packetio. 24 | # 25 | # For example: 26 | # make p4-test TEST=packetio.PacketOutTest 27 | # ------------------------------------------------------------------------------ 28 | 29 | # ------------------------------------------------------------------------------ 30 | # Modify everywhere you see TODO 31 | # 32 | # When providing your solution, make sure to use the same names for P4Runtime 33 | # entities as specified in your P4Info file. 34 | # 35 | # Test cases are based on the P4 program design suggested in the exercises 36 | # README. Make sure to modify the test cases accordingly if you decide to 37 | # implement the pipeline differently. 38 | # ------------------------------------------------------------------------------ 39 | 40 | from ptf.testutils import group 41 | 42 | from base_test import * 43 | 44 | CPU_CLONE_SESSION_ID = 99 45 | 46 | 47 | @group("packetio") 48 | class PacketOutTest(P4RuntimeTest): 49 | """Tests controller packet-out capability by sending PacketOut messages and 50 | expecting a corresponding packet on the output port set in the PacketOut 51 | metadata. 52 | """ 53 | 54 | def runTest(self): 55 | for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6", 56 | "icmpv6"]: 57 | print_inline("%s ... " % pkt_type) 58 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 59 | self.testPacket(pkt) 60 | 61 | def testPacket(self, pkt): 62 | for outport in [self.port1, self.port2]: 63 | # Build PacketOut message. 64 | # TODO EXERCISE 4 65 | # Modify metadata names to match the content of your P4Info file 66 | # ---- START SOLUTION ---- 67 | packet_out_msg = self.helper.build_packet_out( 68 | payload=str(pkt), 69 | metadata={ 70 | "egress_port": outport, 71 | "_pad": 0 72 | }) 73 | # ---- END SOLUTION ---- 74 | 75 | # Send message and expect packet on the given data plane port. 76 | self.send_packet_out(packet_out_msg) 77 | 78 | testutils.verify_packet(self, pkt, outport) 79 | 80 | # Make sure packet was forwarded only on the specified ports 81 | testutils.verify_no_other_packets(self) 82 | 83 | 84 | @group("packetio") 85 | class PacketInTest(P4RuntimeTest): 86 | """Tests controller packet-in capability my matching on the packet EtherType 87 | and cloning to the CPU port. 88 | """ 89 | 90 | def runTest(self): 91 | for pkt_type in ["tcp", "udp", "icmp", "arp", "tcpv6", "udpv6", 92 | "icmpv6"]: 93 | print_inline("%s ... " % pkt_type) 94 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 95 | self.testPacket(pkt) 96 | 97 | @autocleanup 98 | def testPacket(self, pkt): 99 | 100 | # Insert clone to CPU session 101 | self.insert_pre_clone_session( 102 | session_id=CPU_CLONE_SESSION_ID, 103 | ports=[self.cpu_port]) 104 | 105 | # Insert ACL entry to match on the given eth_type and clone to CPU. 106 | eth_type = pkt[Ether].type 107 | # TODO EXERCISE 4 108 | # Modify names to match content of P4Info file (look for the fully 109 | # qualified name of the ACL table, EtherType match field, and 110 | # clone_to_cpu action. 111 | # ---- START SOLUTION ---- 112 | self.insert(self.helper.build_table_entry( 113 | table_name="IngressPipeImpl.acl_table", 114 | match_fields={ 115 | # Ternary match. 116 | "hdr.ethernet.ether_type": (eth_type, 0xffff) 117 | }, 118 | action_name="IngressPipeImpl.clone_to_cpu", 119 | priority=DEFAULT_PRIORITY 120 | )) 121 | # ---- END SOLUTION ---- 122 | 123 | for inport in [self.port1, self.port2, self.port3]: 124 | # TODO EXERCISE 4 125 | # Modify metadata names to match the content of your P4Info file 126 | # ---- START SOLUTION ---- 127 | # Expected P4Runtime PacketIn message. 128 | exp_packet_in_msg = self.helper.build_packet_in( 129 | payload=str(pkt), 130 | metadata={ 131 | "ingress_port": inport, 132 | "_pad": 0 133 | }) 134 | # ---- END SOLUTION ---- 135 | 136 | # Send packet to given switch ingress port and expect P4Runtime 137 | # PacketIn message. 138 | testutils.send_packet(self, inport, str(pkt)) 139 | self.verify_packet_in(exp_packet_in_msg) 140 | -------------------------------------------------------------------------------- /solution/ptf/tests/routing.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | # 15 | 16 | # ------------------------------------------------------------------------------ 17 | # IPV6 ROUTING TESTS 18 | # 19 | # To run all tests: 20 | # make p4-test TEST=routing 21 | # 22 | # To run a specific test case: 23 | # make p4-test TEST=routing. 24 | # 25 | # For example: 26 | # make p4-test TEST=routing.IPv6RoutingTest 27 | # ------------------------------------------------------------------------------ 28 | 29 | # ------------------------------------------------------------------------------ 30 | # Modify everywhere you see TODO 31 | # 32 | # When providing your solution, make sure to use the same names for P4Runtime 33 | # entities as specified in your P4Info file. 34 | # 35 | # Test cases are based on the P4 program design suggested in the exercises 36 | # README. Make sure to modify the test cases accordingly if you decide to 37 | # implement the pipeline differently. 38 | # ------------------------------------------------------------------------------ 39 | 40 | from ptf.testutils import group 41 | 42 | from base_test import * 43 | 44 | 45 | @group("routing") 46 | class IPv6RoutingTest(P4RuntimeTest): 47 | """Tests basic IPv6 routing""" 48 | 49 | def runTest(self): 50 | # Test with different type of packets. 51 | for pkt_type in ["tcpv6", "udpv6", "icmpv6"]: 52 | print_inline("%s ... " % pkt_type) 53 | pkt = getattr(testutils, "simple_%s_packet" % pkt_type)() 54 | self.testPacket(pkt) 55 | 56 | @autocleanup 57 | def testPacket(self, pkt): 58 | next_hop_mac = SWITCH2_MAC 59 | 60 | # Add entry to "My Station" table. Consider the given pkt's eth dst addr 61 | # as myStationMac address. 62 | # *** TODO EXERCISE 5 63 | # Modify names to match content of P4Info file (look for the fully 64 | # qualified name of tables, match fields, and actions. 65 | # ---- START SOLUTION ---- 66 | self.insert(self.helper.build_table_entry( 67 | table_name="IngressPipeImpl.my_station_table", 68 | match_fields={ 69 | # Exact match. 70 | "hdr.ethernet.dst_addr": pkt[Ether].dst 71 | }, 72 | action_name="NoAction" 73 | )) 74 | # ---- END SOLUTION ---- 75 | 76 | # Insert ECMP group with only one member (next_hop_mac) 77 | # *** TODO EXERCISE 5 78 | # Modify names to match content of P4Info file (look for the fully 79 | # qualified name of tables, match fields, and actions. 80 | # ---- START SOLUTION ---- 81 | self.insert(self.helper.build_act_prof_group( 82 | act_prof_name="IngressPipeImpl.ecmp_selector", 83 | group_id=1, 84 | actions=[ 85 | # List of tuples (action name, action param dict) 86 | ("IngressPipeImpl.set_next_hop", {"dmac": next_hop_mac}), 87 | ] 88 | )) 89 | # ---- END SOLUTION ---- 90 | 91 | # Insert L3 entry to app pkt's IPv6 dst addr to group 92 | # *** TODO EXERCISE 5 93 | # Modify names to match content of P4Info file (look for the fully 94 | # qualified name of tables, match fields, and actions. 95 | # ---- START SOLUTION ---- 96 | self.insert(self.helper.build_table_entry( 97 | table_name="IngressPipeImpl.routing_v6_table", 98 | match_fields={ 99 | # LPM match (value, prefix) 100 | "hdr.ipv6.dst_addr": (pkt[IPv6].dst, 128) 101 | }, 102 | group_id=1 103 | )) 104 | # ---- END SOLUTION ---- 105 | 106 | # Insert L3 entry to map next_hop_mac to output port 2. 107 | # *** TODO EXERCISE 5 108 | # Modify names to match content of P4Info file (look for the fully 109 | # qualified name of tables, match fields, and actions. 110 | # ---- START SOLUTION ---- 111 | self.insert(self.helper.build_table_entry( 112 | table_name="IngressPipeImpl.l2_exact_table", 113 | match_fields={ 114 | # Exact match 115 | "hdr.ethernet.dst_addr": next_hop_mac 116 | }, 117 | action_name="IngressPipeImpl.set_egress_port", 118 | action_params={ 119 | "port_num": self.port2 120 | } 121 | )) 122 | # ---- END SOLUTION ---- 123 | 124 | # Expected pkt should have routed MAC addresses and decremented hop 125 | # limit (TTL). 126 | exp_pkt = pkt.copy() 127 | pkt_route(exp_pkt, next_hop_mac) 128 | pkt_decrement_ttl(exp_pkt) 129 | 130 | testutils.send_packet(self, self.port1, str(pkt)) 131 | testutils.verify_packet(self, exp_pkt, self.port2) 132 | 133 | 134 | @group("routing") 135 | class NdpReplyGenTest(P4RuntimeTest): 136 | """Tests automatic generation of NDP Neighbor Advertisement for IPV6 137 | addresses associated to the switch interface. 138 | """ 139 | 140 | @autocleanup 141 | def runTest(self): 142 | switch_ip = SWITCH1_IPV6 143 | target_mac = SWITCH1_MAC 144 | 145 | # Insert entry to transform NDP NA packets for the given target address 146 | # (match), to NDP NA packets with the given target MAC address (action 147 | # *** TODO EXERCISE 5 148 | # Modify names to match content of P4Info file (look for the fully 149 | # qualified name of tables, match fields, and actions. 150 | # ---- START SOLUTION ---- 151 | self.insert(self.helper.build_table_entry( 152 | table_name="IngressPipeImpl.ndp_reply_table", 153 | match_fields={ 154 | # Exact match. 155 | "hdr.ndp.target_ipv6_addr": switch_ip 156 | }, 157 | action_name="IngressPipeImpl.ndp_ns_to_na", 158 | action_params={ 159 | "target_mac": target_mac 160 | } 161 | )) 162 | # ---- END SOLUTION ---- 163 | 164 | # NDP Neighbor Solicitation packet 165 | pkt = genNdpNsPkt(target_ip=switch_ip) 166 | 167 | # NDP Neighbor Advertisement packet 168 | exp_pkt = genNdpNaPkt(target_ip=switch_ip, 169 | target_mac=target_mac, 170 | src_mac=target_mac, 171 | src_ip=switch_ip, 172 | dst_ip=pkt[IPv6].src) 173 | 174 | # Send NDP NS, expect NDP NA from the same port. 175 | testutils.send_packet(self, self.port1, str(pkt)) 176 | testutils.verify_packet(self, exp_pkt, self.port1) -------------------------------------------------------------------------------- /util/docker/Makefile: -------------------------------------------------------------------------------- 1 | include Makefile.vars 2 | 3 | build: build-stratum_bmv2 build-mvn 4 | push: push-stratum_bmv2 push-mvn 5 | 6 | build-stratum_bmv2: 7 | cd stratum_bmv2 && docker build -t ${STRATUM_BMV2_IMG} . 8 | 9 | build-mvn: 10 | cd ../../app && docker build --squash -f ../util/docker/mvn/Dockerfile \ 11 | -t ${MVN_IMG} . 12 | 13 | push-stratum_bmv2: 14 | # Remember to update Makefile.vars with the new image sha 15 | docker push ${STRATUM_BMV2_IMG} 16 | 17 | push-mvn: 18 | # Remember to update Makefile.vars with the new image sha 19 | docker push ${MVN_IMG} 20 | -------------------------------------------------------------------------------- /util/docker/Makefile.vars: -------------------------------------------------------------------------------- 1 | ONOS_IMG := onosproject/onos:2.2.2 2 | P4RT_SH_IMG := p4lang/p4runtime-sh:latest 3 | P4C_IMG := opennetworking/p4c:stable 4 | STRATUM_BMV2_IMG := opennetworking/ngsdn-tutorial:stratum_bmv2 5 | MVN_IMG := opennetworking/ngsdn-tutorial:mvn 6 | GNMI_CLI_IMG := bocon/gnmi-cli:latest 7 | YANG_IMG := bocon/yang-tools:latest 8 | SSHPASS_IMG := ictu/sshpass 9 | 10 | ONOS_SHA := sha256:438815ab20300cd7a31702b7dea635152c4c4b5b2fed9b14970bd2939a139d2a 11 | P4RT_SH_SHA := sha256:6ae50afb5bde620acb9473ce6cd7b990ff6cc63fe4113cf5584c8e38fe42176c 12 | P4C_SHA := sha256:8f9d27a6edf446c3801db621359fec5de993ebdebc6844d8b1292e369be5dfea 13 | STRATUM_BMV2_SHA := sha256:f31faa5e83abbb2d9cf39d28b3578f6e113225641337ec7d16d867b0667524ef 14 | MVN_SHA := sha256:d85eb93ac909a90f49b16b33cb872620f9b4f640e7a6451859aec704b21f9243 15 | GNMI_CLI_SHA := sha256:6f1590c35e71c07406539d0e1e288e87e1e520ef58de25293441c3b9c81dffc0 16 | YANG_SHA := sha256:feb2dc322af113fc52f17b5735454abfbe017972c867e522ba53ea44e8386fd2 17 | SSHPASS_SHA := sha256:6e3d0d7564b259ef9612843d220cc390e52aab28b0ff9adaec800c72a051f41c 18 | -------------------------------------------------------------------------------- /util/docker/mvn/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Docker image to build the ONOS app. 16 | # Provides pre-poulated maven repo cache to allow offline builds. 17 | 18 | FROM maven:3.6.1-jdk-11-slim 19 | 20 | COPY . /mvn-src 21 | WORKDIR /mvn-src 22 | 23 | RUN mvn clean package && rm -rf ./* 24 | -------------------------------------------------------------------------------- /util/docker/stratum_bmv2/Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2019-present Open Networking Foundation 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Docker image that extends opennetworking/mn-stratum with other dependencies 16 | # required by this tutorial. opennetworking/mn-stratum is the official image 17 | # from the Stratum project which contains stratum_bmv2 and the Mininet 18 | # libraries. We extend that with PTF, scapy, etc. 19 | 20 | ARG MN_STRATUM_SHA="sha256:1bba2e2c06460c73b0133ae22829937786217e5f20f8f80fcc3063dcf6707ebe" 21 | 22 | FROM bitnami/minideb:stretch as builder 23 | 24 | ENV BUILD_DEPS \ 25 | python-pip \ 26 | python-setuptools \ 27 | git 28 | RUN install_packages $BUILD_DEPS 29 | 30 | RUN mkdir -p /ouput 31 | 32 | ENV PIP_DEPS \ 33 | scapy==2.4.3 \ 34 | git+https://github.com/p4lang/ptf.git \ 35 | googleapis-common-protos==1.6.0 \ 36 | ipaddress 37 | RUN pip install --no-cache-dir --root /output $PIP_DEPS 38 | 39 | FROM opennetworking/mn-stratum:latest@$MN_STRATUM_SHA as runtime 40 | 41 | ENV RUNTIME_DEPS \ 42 | make 43 | RUN install_packages $RUNTIME_DEPS 44 | 45 | COPY --from=builder /output / 46 | 47 | ENV DOCKER_RUN true 48 | 49 | ENTRYPOINT [] 50 | -------------------------------------------------------------------------------- /util/gnmi-cli: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm -it --network host bocon/gnmi-cli:latest $@ 3 | -------------------------------------------------------------------------------- /util/mn-cmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $1 ]; then 4 | echo "usage: $0 host cmd [args...]" 5 | exit 1 6 | fi 7 | 8 | docker exec -it mininet /mininet/host-cmd $@ 9 | -------------------------------------------------------------------------------- /util/mn-pcap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 4 | 5 | if [ -z $1 ]; then 6 | echo "usage: $0 host" 7 | exit 1 8 | fi 9 | 10 | iface=$1-eth0 11 | file=${iface}.pcap 12 | 13 | set -e 14 | 15 | echo "*** Starting tcpdump on ${iface}... Ctrl-c to stop capture" 16 | echo "*** Pcap file will be written in ngsdn-tutorial/tmp/${file}" 17 | docker exec -it mininet /mininet/host-cmd $1 tcpdump -i $1-eth0 -w /tmp/"${file}" 18 | 19 | if [ -x "$(command -v wireshark)" ]; then 20 | echo "*** Opening wireshark... Ctrl-c to quit" 21 | wireshark "${DIR}/../tmp/${file}" 22 | fi 23 | -------------------------------------------------------------------------------- /util/oc-pb-decoder: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | docker run --rm -i bocon/yang-tools:latest oc-pb-decoder 3 | -------------------------------------------------------------------------------- /util/onos-cmd: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z $1 ]; then 4 | echo "usage: $0 cmd [args...]" 5 | exit 1 6 | fi 7 | 8 | # Use sshpass to skip the password prompt 9 | docker run -it --rm --network host ictu/sshpass \ 10 | -procks ssh -o "UserKnownHostsFile=/dev/null" \ 11 | -o "StrictHostKeyChecking=no" -o LogLevel=ERROR -p 8101 onos@localhost "$@" 12 | -------------------------------------------------------------------------------- /util/p4rt-sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2019 Barefoot Networks, 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 | # http://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 | 18 | """ 19 | P4Runtime shell docker wrapper 20 | From: https://github.com/p4lang/p4runtime-shell/blob/master/p4runtime-sh-docker 21 | """ 22 | 23 | import argparse 24 | from collections import namedtuple 25 | import logging 26 | import os.path 27 | import sys 28 | import tempfile 29 | import shutil 30 | import subprocess 31 | 32 | DOCKER_IMAGE = 'p4lang/p4runtime-sh' 33 | TMP_DIR = os.path.dirname(os.path.abspath(__file__)) + '/.pipe_cfg' 34 | 35 | 36 | def main(): 37 | FwdPipeConfig = namedtuple('FwdPipeConfig', ['p4info', 'bin']) 38 | 39 | def pipe_config(arg): 40 | try: 41 | paths = FwdPipeConfig(*[x for x in arg.split(',')]) 42 | if len(paths) != 2: 43 | raise argparse.ArgumentError 44 | return paths 45 | except Exception: 46 | raise argparse.ArgumentError( 47 | "Invalid pipeline config, expected ,") 48 | 49 | parser = argparse.ArgumentParser(description='P4Runtime shell docker wrapper', add_help=False) 50 | parser.add_argument('--grpc-addr', 51 | help='P4Runtime gRPC server address', 52 | metavar=':', 53 | type=str, action='store', default="localhost:50001") 54 | parser.add_argument('--config', 55 | help='If you want the shell to push a pipeline config to the server first', 56 | metavar=',', 57 | type=pipe_config, action='store', default=None) 58 | parser.add_argument('-v', '--verbose', help='Increase output verbosity', 59 | action='store_true') 60 | args, unknown_args = parser.parse_known_args() 61 | 62 | docker_args = [] 63 | new_args = [] 64 | 65 | if args.verbose: 66 | logging.basicConfig(level=logging.DEBUG) 67 | new_args.append('--verbose') 68 | 69 | if args.grpc_addr is not None: 70 | print("*** Connecting to P4Runtime server at {} ...".format(args.grpc_addr)) 71 | new_args.extend(["--grpc-addr", args.grpc_addr]) 72 | 73 | if args.config is not None: 74 | if not os.path.isfile(args.config.p4info): 75 | logging.critical("'{}' is not a valid file".format(args.config.p4info)) 76 | sys.exit(1) 77 | if not os.path.isfile(args.config.bin): 78 | logging.critical("'{}' is not a valid file".format(args.config.bin)) 79 | sys.exit(1) 80 | 81 | mount_path = "/fwd_pipe_config" 82 | fname_p4info = "p4info.pb.txt" 83 | fname_bin = "config.bin" 84 | 85 | os.mkdir(TMP_DIR) 86 | logging.debug( 87 | "Created temporary directory '{}', it will be mounted in the docker as '{}'".format( 88 | TMP_DIR, mount_path)) 89 | shutil.copy(args.config.p4info, os.path.join(TMP_DIR, fname_p4info)) 90 | shutil.copy(args.config.bin, os.path.join(TMP_DIR, fname_bin)) 91 | 92 | docker_args.extend(["-v", "{}:{}".format(TMP_DIR, mount_path)]) 93 | new_args.extend(["--config", "{},{}".format( 94 | os.path.join(mount_path, fname_p4info), os.path.join(mount_path, fname_bin))]) 95 | 96 | cmd = ["docker", "run", "-ti", "--network", "host"] 97 | cmd.extend(docker_args) 98 | cmd.append(DOCKER_IMAGE) 99 | cmd.extend(new_args) 100 | cmd.extend(unknown_args) 101 | logging.debug("Running cmd: {}".format(" ".join(cmd))) 102 | 103 | subprocess.run(cmd) 104 | 105 | if args.config is not None: 106 | logging.debug("Cleaning up...") 107 | try: 108 | shutil.rmtree(TMP_DIR) 109 | except Exception: 110 | logging.error("Error when removing temporary directory '{}'".format(TMP_DIR)) 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /util/vm/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .vagrant 3 | *.ova 4 | -------------------------------------------------------------------------------- /util/vm/README.md: -------------------------------------------------------------------------------- 1 | # Scripts to build the tutorial VM 2 | 3 | ## Requirements 4 | 5 | - [Vagrant](https://www.vagrantup.com/) (tested v2.2.5) 6 | - [VirtualBox](https://www.virtualbox.org/wiki/Downloads) (tested with v5.2.32) 7 | 8 | ## Steps to build 9 | 10 | If you want to provision and use the VM locally on your machine: 11 | 12 | cd util/vm 13 | vagrant up 14 | 15 | Otherwise, if you want to export the VM in `.ova` format for distribution to 16 | tutorial attendees: 17 | 18 | cd util/vm 19 | ./build-vm.sh 20 | 21 | This script will: 22 | 23 | 1. provision the VM using Vagrant; 24 | 2. reduce VM disk size; 25 | 3. generate a file named `ngsdn-tutorial.ova`. 26 | 27 | Use credentials `sdn`/`rocks` to log in the Ubuntu system. 28 | 29 | **Note on IntelliJ IDEA plugins:** plugins need to be installed manually. We 30 | recommend installing the following ones: 31 | 32 | * https://plugins.jetbrains.com/plugin/10620-p4-plugin 33 | * https://plugins.jetbrains.com/plugin/7322-python-community-edition -------------------------------------------------------------------------------- /util/vm/Vagrantfile: -------------------------------------------------------------------------------- 1 | REQUIRED_PLUGINS = %w( vagrant-vbguest vagrant-reload vagrant-disksize ) 2 | 3 | Vagrant.configure(2) do |config| 4 | 5 | # Install plugins if missing... 6 | _retry = false 7 | REQUIRED_PLUGINS.each do |plugin| 8 | unless Vagrant.has_plugin? plugin 9 | system "vagrant plugin install #{plugin}" 10 | _retry = true 11 | end 12 | end 13 | 14 | if (_retry) 15 | exec "vagrant " + ARGV.join(' ') 16 | end 17 | 18 | # Common config. 19 | config.vm.box = "lasp/ubuntu16.04-desktop" 20 | config.vbguest.auto_update = true 21 | config.disksize.size = '50GB' 22 | config.vm.synced_folder ".", "/vagrant", disabled: false, type: "virtualbox" 23 | config.vm.network "private_network", :type => 'dhcp', :adapter => 2 24 | 25 | config.vm.define "default" do |d| 26 | d.vm.hostname = "tutorial-vm" 27 | d.vm.provider "virtualbox" do |vb| 28 | vb.name = "ONF NG-SDN Tutorial " + Time.now.strftime("(%Y-%m-%d)") 29 | vb.gui = true 30 | vb.cpus = 8 31 | vb.memory = 8192 32 | vb.customize ['modifyvm', :id, '--clipboard', 'bidirectional'] 33 | vb.customize ["modifyvm", :id, "--accelerate3d", "on"] 34 | vb.customize ["modifyvm", :id, "--graphicscontroller", "vboxvga"] 35 | vb.customize ["modifyvm", :id, "--vram", "128"] 36 | end 37 | d.vm.provision "shell", path: "root-bootstrap.sh" 38 | d.vm.provision "shell", inline: "su sdn '/vagrant/user-bootstrap.sh'" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /util/vm/build-vm.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | function wait_vm_shutdown { 6 | set +x 7 | while vboxmanage showvminfo $1 | grep -c "running (since"; do 8 | echo "Waiting for VM to shutdown..." 9 | sleep 1 10 | done 11 | sleep 2 12 | set -x 13 | } 14 | 15 | # Provision 16 | vagrant up 17 | 18 | # Cleanup 19 | VB_UUID=$(cat .vagrant/machines/default/virtualbox/id) 20 | vagrant ssh -c 'bash /vagrant/cleanup.sh' 21 | sleep 5 22 | vboxmanage controlvm "${VB_UUID}" acpipowerbutton 23 | wait_vm_shutdown "${VB_UUID}" 24 | # Remove vagrant shared folder 25 | vboxmanage sharedfolder remove "${VB_UUID}" -name "vagrant" 26 | 27 | # Export 28 | rm -f ngsdn-tutorial.ova 29 | vboxmanage export "${VB_UUID}" -o ngsdn-tutorial.ova 30 | -------------------------------------------------------------------------------- /util/vm/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | sudo apt-get clean 5 | sudo apt-get -y autoremove 6 | 7 | sudo rm -rf /tmp/* 8 | 9 | history -c 10 | rm -f ~/.bash_history 11 | 12 | # Zerofill virtual hd to save space when exporting 13 | time sudo dd if=/dev/zero of=/tmp/zero bs=1M || true 14 | sync ; sleep 1 ; sync ; sudo rm -f /tmp/zero 15 | 16 | # Delete vagrant user 17 | sudo userdel -r -f vagrant 18 | -------------------------------------------------------------------------------- /util/vm/root-bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -xe 4 | 5 | # Create user sdn 6 | useradd -m -d /home/sdn -s /bin/bash sdn 7 | usermod -aG sudo sdn 8 | usermod -aG vboxsf sdn 9 | echo "sdn:rocks" | chpasswd 10 | echo "sdn ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/99_sdn 11 | chmod 440 /etc/sudoers.d/99_sdn 12 | update-locale LC_ALL="en_US.UTF-8" 13 | 14 | apt-get update 15 | 16 | apt-get install -y --no-install-recommends apt-transport-https ca-certificates 17 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add - 18 | add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" 19 | apt-get update 20 | 21 | # Required packages 22 | DEBIAN_FRONTEND=noninteractive apt-get -y --no-install-recommends install \ 23 | avahi-daemon \ 24 | git \ 25 | bash-completion \ 26 | htop \ 27 | python \ 28 | zip unzip \ 29 | make \ 30 | wget \ 31 | curl \ 32 | vim nano emacs \ 33 | docker-ce 34 | 35 | # Enable Docker at startup 36 | systemctl start docker 37 | systemctl enable docker 38 | # Add sdn user to docker group 39 | usermod -a -G docker sdn 40 | 41 | # Install pip 42 | curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py 43 | python get-pip.py --force-reinstall 44 | rm -f get-pip.py 45 | 46 | # Bash autocompletion 47 | echo "source /etc/profile.d/bash_completion.sh" >> ~/.bashrc 48 | 49 | # Fix SSH server config 50 | tee -a /etc/ssh/sshd_config <