├── .gitignore ├── LICENSE ├── README.md ├── TCPsend.py ├── UDPsend.py ├── controller.py ├── p4_mininet.py ├── run_mininet.py ├── simple.p4 └── utils ├── __init__.py ├── bmv2.py ├── convert.py ├── error_utils.py ├── helper.py ├── simple_controller.py └── switch.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | 7 | # logs 8 | **log.txt 9 | 10 | # p4info files and compiled p4 files (json) 11 | **p4info.txt 12 | **p4i 13 | **json 14 | 15 | # C extensions 16 | *.so 17 | 18 | # Distribution / packaging 19 | .Python 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | pip-wheel-metadata/ 33 | share/python-wheels/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | MANIFEST 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .nox/ 53 | .coverage 54 | .coverage.* 55 | .cache 56 | nosetests.xml 57 | coverage.xml 58 | *.cover 59 | *.py,cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | db.sqlite3-journal 72 | 73 | # Flask stuff: 74 | instance/ 75 | .webassets-cache 76 | 77 | # Scrapy stuff: 78 | .scrapy 79 | 80 | # Sphinx documentation 81 | docs/_build/ 82 | 83 | # PyBuilder 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # pyenv 94 | .python-version 95 | 96 | # pipenv 97 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 98 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 99 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 100 | # install all needed dependencies. 101 | #Pipfile.lock 102 | 103 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 104 | __pypackages__/ 105 | 106 | # Celery stuff 107 | celerybeat-schedule 108 | celerybeat.pid 109 | 110 | # SageMath parsed files 111 | *.sage.py 112 | 113 | # Environments 114 | .env 115 | .venv 116 | env/ 117 | venv/ 118 | ENV/ 119 | env.bak/ 120 | venv.bak/ 121 | 122 | # Spyder project settings 123 | .spyderproject 124 | .spyproject 125 | 126 | # Rope project settings 127 | .ropeproject 128 | 129 | # mkdocs documentation 130 | /site 131 | 132 | # mypy 133 | .mypy_cache/ 134 | .dmypy.json 135 | dmypy.json 136 | 137 | # Pyre type checker 138 | .pyre/ 139 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple-P4runtime-Controller 2 | 3 | ## Mininet with simple_switch_grpc 4 | The scripts starts a very simple Mininet topo with just one switch, n hosts and 1 server host. Additionally, it compiles the supplied P4 file to generate both the JSON as well as P4Info file (used by the controller) 5 | ```bash 6 | usage: run_mininet.py [-h] [--num-hosts NUM_HOSTS] [--p4-file P4_FILE] 7 | 8 | Mininet demo 9 | 10 | optional arguments: 11 | -h, --help show this help message and exit 12 | --num-hosts NUM_HOSTS 13 | Number of hosts to connect to switch 14 | --p4-file P4_FILE Path to P4 file 15 | ``` 16 | ## P4Runtime controller 17 | Dummy P4Runtime controller with packet-in/packet-out functionality. 18 | 19 | ```bash 20 | usage: controller.py [-h] [--p4info P4INFO] [--bmv2-json BMV2_JSON] 21 | 22 | P4Runtime Controller 23 | 24 | optional arguments: 25 | -h, --help show this help message and exit 26 | --p4info P4INFO p4info proto in text format from p4c 27 | --bmv2-json BMV2_JSON 28 | BMv2 JSON file from p4c 29 | 30 | ``` 31 | At the start of the connection, the controller installs three rules, forwarding the packets to the CPU port. Furthermore, it listens to PacketIn messages and outputs them to port 3. 32 | 33 | ### Packet-In messages 34 | At the beginning of each packet_in message, a special header containing the ingress port is appended—these values translate to the controller's metadata values. 35 | 36 | ```bash 37 | @controller_header("packet_in") 38 | header packet_in_t { 39 | bit<16> ingress_port; 40 | } 41 | ``` 42 | 43 | ### Packet-Out messages 44 | At the beginning of each packet Out message, a special header containing the egress port is appended. This value is set at the controller (as metadata in the buildPacketOut function) 45 | 46 | ```bash 47 | @controller_header("packet_in") 48 | header packet_in_t { 49 | bit<16> ingress_port; 50 | } 51 | ``` 52 | ## Run a test scenario 53 | 54 | ### Start Mininet (with simple_switch_grpc) 55 | ```bash 56 | python3 run_mininet.py --p4-file simple.p4 57 | 58 | ``` 59 | ### Start the controller in a different terminal 60 | ```bash 61 | python3 controller.py --p4info firmeware.p4info.txt --bmv2-json simple.json 62 | ``` 63 | ### Generate traffic (in Mininet) 64 | ```bash 65 | h1 python UDPsend.py 66 | ``` 67 | 68 | ### Observe PacketOut packets on the server 69 | Mininet will create a topology with 2 hosts and 1 server connected to a switch. The controller will for each received packetIn, generate a PacketOut and specify the output port as the 3rd port (hardcoded in the packetOut). 70 | 71 | ``` 72 | $ sudo tcpdump -i s0-eth3 73 | ``` 74 | 75 | Expected output: 76 | ``` 77 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 78 | listening on s0-eth3, link-type EN10MB (Ethernet), capture size 262144 bytes 79 | 23:03:25.949165 IP 10.10.10.1.7777 > 10.10.3.3.80: UDP, length 24 80 | 23:03:25.988292 IP 10.10.10.1.7777 > 10.10.3.3.80: UDP, length 24 81 | 23:03:26.033244 IP 10.10.10.1.7777 > 10.10.3.3.80: UDP, length 24 82 | 23:03:26.068418 IP 10.10.10.1.7777 > 10.10.3.3.80: UDP, length 24 83 | 23:03:26.109795 IP 10.10.10.1.7777 > 10.10.3.3.80: UDP, length 24 84 | ``` 85 | -------------------------------------------------------------------------------- /TCPsend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from scapy.all import * 4 | import argparse 5 | 6 | 7 | def main(): 8 | """ 9 | """ 10 | #packet = IP(dst="10.7.100.10")/TCP()/"from scapy packet" 11 | #send(packet) 12 | 13 | 14 | def packet_with_seq_n(): 15 | packet = Ether(src="00:04:00:00:00:00", dst="00:00:01:01:01:01")/IP(dst="10.10.3.3", src="10.10.10.1")/TCP(sport=7777, dport=7777, flags="S")/"111111112222222233333333" 16 | sendp(packet, iface="eth0") 17 | 18 | if __name__ == "__main__": 19 | main() 20 | parser = argparse.ArgumentParser(description='Simple script that sends TCP packets to an interface using scapy package') 21 | args = parser.parse_args() 22 | for i in range(0, 1): 23 | packet_with_seq_n() 24 | 25 | -------------------------------------------------------------------------------- /UDPsend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from scapy.all import * 4 | import argparse 5 | 6 | 7 | def main(): 8 | """ 9 | """ 10 | #packet = IP(dst="10.7.100.10")/TCP()/"from scapy packet" 11 | #send(packet) 12 | 13 | 14 | def packet_with_seq_n(port): 15 | packet = Ether(src="3c:a8:2a:13:8f:bb", dst="00:04:00:00:00:01")/IP(dst="10.10.3.3", src="10.10.10.1")/UDP(sport=7777, dport=port)/"111111112222222233333333" 16 | sendp(packet, iface="eth0") 17 | 18 | if __name__ == "__main__": 19 | main() 20 | parser = argparse.ArgumentParser(description='Simple script that sends TCP packets to an interface using scapy package') 21 | parser.add_argument('--dst-port', help='Destination port', type=int, action="store", default=80) 22 | args = parser.parse_args() 23 | for i in range(0, 5): 24 | packet_with_seq_n(int(args.dst_port)) 25 | 26 | -------------------------------------------------------------------------------- /controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # Copyright 2019 Belma Turkovic 3 | # TU Delft Embedded and Networked Systems Group. 4 | # NOTICE: THIS FILE IS BASED ON https://github.com/p4lang/tutorials/tree/master/exercises/p4runtime, BUT WAS MODIFIED UNDER COMPLIANCE 5 | # WITH THE APACHE 2.0 LICENCE FROM THE ORIGINAL WORK. 6 | import argparse 7 | import grpc 8 | import os 9 | import sys 10 | from time import sleep 11 | 12 | # Import P4Runtime lib from parent utils dir 13 | # Probably there's a better way of doing this. 14 | sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "utils/")) 15 | import bmv2 16 | from switch import ShutdownAllSwitchConnections 17 | from utils.convert import encodeNum 18 | import helper 19 | 20 | 21 | def writeIpv4Rules(p4info_helper, sw_id, dst_ip_addr, port): 22 | table_entry = p4info_helper.buildTableEntry( 23 | table_name="MyIngress.ipv4_lpm", 24 | match_fields={"hdr.ipv4.dstAddr": (dst_ip_addr, 32)}, 25 | action_name="MyIngress.ipv4_forward", 26 | action_params={"port": port}, 27 | ) 28 | sw_id.WriteTableEntry(table_entry) 29 | print("Installed ingress forwarding rule on %s" % sw_id.name) 30 | 31 | 32 | def readTableRules(p4info_helper, sw): 33 | """ 34 | Reads the table entries from all tables on the switch. 35 | 36 | :param p4info_helper: the P4Info helper 37 | :param sw: the switch connection 38 | """ 39 | print("\n----- Reading tables rules for %s -----" % sw.name) 40 | for response in sw.ReadTableEntries(): 41 | for entity in response.entities: 42 | entry = entity.table_entry 43 | table_name = p4info_helper.get_tables_name(entry.table_id) 44 | print("%s: " % table_name, end="") 45 | for m in entry.match: 46 | print( 47 | p4info_helper.get_match_field_name(table_name, m.field_id), end="" 48 | ) 49 | print("%r" % (p4info_helper.get_match_field_value(m),), end="") 50 | action = entry.action.action 51 | action_name = p4info_helper.get_actions_name(action.action_id) 52 | print("-> action:%s with parameters:" % action_name, end="") 53 | for p in action.params: 54 | print( 55 | " %s" 56 | % p4info_helper.get_action_param_name(action_name, p.param_id), 57 | end="", 58 | ) 59 | print(" %r" % p.value, end="") 60 | print("") 61 | 62 | 63 | def printGrpcError(e): 64 | print("gRPC Error:", e.details(), end="") 65 | status_code = e.code() 66 | print("(%s)" % status_code.name, end="") 67 | traceback = sys.exc_info()[2] 68 | print("[%s:%d]" % (traceback.tb_frame.f_code.co_filename, traceback.tb_lineno)) 69 | 70 | 71 | def main(p4info_file_path, bmv2_file_path): 72 | # Instantiate a P4Runtime helper from the p4info file 73 | p4info_helper = helper.P4InfoHelper(p4info_file_path) 74 | 75 | try: 76 | # Create a switch connection object for s1 and s2; 77 | # this is backed by a P4Runtime gRPC connection. 78 | # Also, dump all P4Runtime messages sent to switch to given txt files. 79 | s1 = bmv2.Bmv2SwitchConnection( 80 | name="s0", 81 | address="0.0.0.0:50051", 82 | device_id=1, 83 | proto_dump_file="p4runtime.log", 84 | ) 85 | 86 | # Send master arbitration update message to establish this controller as 87 | # master (required by P4Runtime before performing any other write operation) 88 | 89 | if s1.MasterArbitrationUpdate() == None: 90 | print("Failed to establish the connection") 91 | 92 | # Install the P4 program on the switches 93 | s1.SetForwardingPipelineConfig( 94 | p4info=p4info_helper.p4info, bmv2_json_file_path=bmv2_file_path 95 | ) 96 | print("Installed P4 Program using SetForwardingPipelineConfig on s1") 97 | 98 | # Forward all packet to the controller (CPU_PORT 255) 99 | writeIpv4Rules(p4info_helper, sw_id=s1, dst_ip_addr="10.10.10.1", port=255) 100 | writeIpv4Rules(p4info_helper, sw_id=s1, dst_ip_addr="10.10.10.2", port=255) 101 | writeIpv4Rules(p4info_helper, sw_id=s1, dst_ip_addr="10.10.3.3", port=255) 102 | # read all table rules 103 | readTableRules(p4info_helper, s1) 104 | while True: 105 | packetin = s1.PacketIn() # Packet in! 106 | if packetin is not None: 107 | print("PACKET IN received") 108 | print(packetin) 109 | packet = packetin.packet.payload 110 | packetout = p4info_helper.buildPacketOut( 111 | payload=packet, # send the packet in you received back to output port 3! 112 | metadata={ 113 | 1: encodeNum(3, 16), 114 | }, # egress_spec (check @controller_header("packet_out") in the p4 code) 115 | ) 116 | print("send PACKET OUT") 117 | print(s1.PacketOut(packetout)) 118 | 119 | except KeyboardInterrupt: 120 | print(" Shutting down.") 121 | except grpc.RpcError as e: 122 | printGrpcError(e) 123 | 124 | ShutdownAllSwitchConnections() 125 | 126 | 127 | if __name__ == "__main__": 128 | parser = argparse.ArgumentParser(description="P4Runtime Controller") 129 | parser.add_argument( 130 | "--p4info", 131 | help="p4info proto in text format from p4c", 132 | type=str, 133 | action="store", 134 | required=False, 135 | default="./firmeware.p4info.txt", 136 | ) 137 | parser.add_argument( 138 | "--bmv2-json", 139 | help="BMv2 JSON file from p4c", 140 | type=str, 141 | action="store", 142 | required=False, 143 | default="./simple.json", 144 | ) 145 | args = parser.parse_args() 146 | 147 | if not os.path.exists(args.p4info): 148 | parser.print_help() 149 | print("\np4info file %s not found!" % args.p4info) 150 | parser.exit(1) 151 | if not os.path.exists(args.bmv2_json): 152 | parser.print_help() 153 | print("\nBMv2 JSON file %s not found!" % args.bmv2_json) 154 | parser.exit(2) 155 | main(args.p4info, args.bmv2_json) 156 | -------------------------------------------------------------------------------- /p4_mininet.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Belma Turkovic 2 | # TU Delft Embedded and Networked Systems Group. 3 | # NOTICE: THIS FILE IS BASED ON https://github.com/p4lang/behavioral-model/blob/master/mininet/p4_mininet.py, BUT WAS MODIFIED UNDER COMPLIANCE 4 | # WITH THE APACHE 2.0 LICENCE FROM THE ORIGINAL WORK. 5 | 6 | # Copyright 2013-present Barefoot Networks, Inc. 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | 21 | from mininet.net import Mininet 22 | from mininet.node import Switch, Host 23 | from mininet.log import setLogLevel, info 24 | 25 | 26 | class P4Host(Host): 27 | def config(self, **params): 28 | r = super(Host, self).config(**params) 29 | 30 | self.defaultIntf().rename("eth0") 31 | 32 | for off in ["rx", "tx", "sg"]: 33 | cmd = "/sbin/ethtool --offload eth0 %s off" % off 34 | self.cmd(cmd) 35 | 36 | # disable IPv6 37 | self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") 38 | self.cmd("sysctl -w net.ipv6.conf.default.disable_ipv6=1") 39 | self.cmd("sysctl -w net.ipv6.conf.lo.disable_ipv6=1") 40 | 41 | return r 42 | 43 | def describe(self): 44 | print("**********") 45 | print(self.name) 46 | print( 47 | "default interface: %s\t%s\t%s" 48 | % ( 49 | self.defaultIntf().name, 50 | self.defaultIntf().IP(), 51 | self.defaultIntf().MAC(), 52 | ) 53 | ) 54 | print("**********") 55 | 56 | 57 | class P4Switch(Switch): 58 | """P4 virtual switch""" 59 | 60 | device_id = 0 61 | 62 | def __init__( 63 | self, 64 | name, 65 | sw_path=None, 66 | json_path=None, 67 | grpc_port=None, 68 | thrift_port=None, 69 | pcap_dump=False, 70 | verbose=False, 71 | device_id=None, 72 | enable_debugger=False, 73 | cpu_port=None, 74 | **kwargs 75 | ): 76 | Switch.__init__(self, name, **kwargs) 77 | assert sw_path 78 | self.sw_path = sw_path 79 | self.json_path = json_path 80 | self.verbose = verbose 81 | self.pcap_dump = pcap_dump 82 | self.enable_debugger = enable_debugger 83 | self.cpu_port = cpu_port 84 | if device_id is not None: 85 | self.device_id = device_id 86 | P4Switch.device_id = max(P4Switch.device_id, device_id) 87 | else: 88 | self.device_id = P4Switch.device_id 89 | P4Switch.device_id += 1 90 | self.nanomsg = "ipc:///tmp/bm-%d-log.ipc" % self.device_id 91 | 92 | @classmethod 93 | def setup(cls): 94 | pass 95 | 96 | def start(self, controllers): 97 | "Start up a new P4 switch" 98 | print("Starting P4 switch", self.name) 99 | args = [self.sw_path] 100 | for port, intf in self.intfs.items(): 101 | if not intf.IP(): 102 | args.extend(["-i", str(port) + "@" + intf.name]) 103 | if self.pcap_dump: 104 | args.append("--pcap") 105 | args.extend(["--device-id", str(self.device_id)]) 106 | P4Switch.device_id += 1 107 | notificationAddr = ( 108 | "ipc:///tmp/bmv2-" + str(self.device_id) + "-notifications.ipc" 109 | ) 110 | args.extend(["--notifications-addr", str(notificationAddr)]) 111 | if self.json_path: 112 | args.append(self.json_path) 113 | else: 114 | args.append("--no-p4") 115 | if self.enable_debugger: 116 | args.append("--debugger") 117 | args.append("-- --enable-swap") 118 | logfile = "p4s.%s.log" % self.name 119 | print(" ".join(args)) 120 | 121 | self.cmd(" ".join(args) + " >" + logfile + " 2>&1 &") 122 | 123 | print("switch has been started") 124 | 125 | def stop(self): 126 | "Terminate IVS switch." 127 | self.output.flush() 128 | self.cmd("kill %" + self.sw_path) 129 | self.cmd("wait") 130 | self.deleteIntfs() 131 | 132 | def attach(self, intf): 133 | "Connect a data port" 134 | assert 0 135 | 136 | def detach(self, intf): 137 | "Disconnect a data port" 138 | assert 0 139 | 140 | 141 | class P4GrpcSwitch(Switch): 142 | """P4 virtual switch""" 143 | 144 | device_id = 0 145 | 146 | def __init__( 147 | self, 148 | name, 149 | sw_path=None, 150 | json_path=None, 151 | thrift_port=None, 152 | grpc_port=None, 153 | pcap_dump=False, 154 | verbose=False, 155 | device_id=None, 156 | enable_debugger=False, 157 | cpu_port=None, 158 | **kwargs 159 | ): 160 | Switch.__init__(self, name, **kwargs) 161 | assert sw_path 162 | self.sw_path = sw_path 163 | self.json_path = json_path 164 | self.verbose = verbose 165 | self.thrift_port = thrift_port 166 | self.grpc_port = grpc_port 167 | self.enable_debugger = enable_debugger 168 | self.cpu_port = cpu_port 169 | if device_id is not None: 170 | self.device_id = device_id 171 | P4Switch.device_id = max(P4Switch.device_id, device_id) 172 | else: 173 | self.device_id = P4Switch.device_id 174 | P4Switch.device_id += 1 175 | 176 | @classmethod 177 | def setup(cls): 178 | pass 179 | 180 | def start(self, controllers): 181 | "Start up a new P4 switch" 182 | print("Starting P4 switch", self.name) 183 | args = [self.sw_path] 184 | for port, intf in self.intfs.items(): 185 | if not intf.IP(): 186 | args.extend(["-i", str(port) + "@" + intf.name]) 187 | if self.thrift_port: 188 | args.extend(["--thrift-port", str(self.thrift_port)]) 189 | 190 | args.extend(["--device-id", str(self.device_id)]) 191 | P4Switch.device_id += 1 192 | if self.json_path: 193 | args.append(self.json_path) 194 | else: 195 | args.append("--no-p4") 196 | 197 | args.append("--log-flush --log-level trace --log-file %s.log" % self.name) 198 | if self.grpc_port: 199 | args.append( 200 | "-- --grpc-server-addr 0.0.0.0:" 201 | + str(self.grpc_port) 202 | + " --cpu-port " 203 | + self.cpu_port 204 | ) 205 | print(" ".join(args)) 206 | self.cmd(" ".join(args) + " > %s.log 2>&1 &" % self.name) 207 | print("switch has been started") 208 | 209 | def stop(self): 210 | "Terminate IVS switch." 211 | self.cmd("kill %" + self.sw_path) 212 | self.cmd("wait") 213 | self.deleteIntfs() 214 | 215 | def attach(self, intf): 216 | "Connect a data port" 217 | assert 0 218 | 219 | def detach(self, intf): 220 | "Disconnect a data port" 221 | assert 0 222 | -------------------------------------------------------------------------------- /run_mininet.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Belma Turkovic 2 | # TU Delft Embedded and Networked Systems Group. 3 | from mininet.net import Mininet 4 | from mininet.topo import Topo 5 | from mininet.log import setLogLevel, info 6 | from mininet.cli import CLI 7 | from mininet.link import TCLink 8 | from p4_mininet import P4Switch, P4Host, P4GrpcSwitch 9 | 10 | # from p4runtime_switch import P4RuntimeSwitch 11 | import random 12 | import argparse 13 | from time import sleep 14 | import subprocess 15 | import sys 16 | import os 17 | import psutil 18 | 19 | parser = argparse.ArgumentParser(description="Mininet demo") 20 | # parser.add_argument('--thrift-port', help='Thrift server port for table updates', type=int, action="store", default=9090) 21 | parser.add_argument( 22 | "--num-hosts", 23 | help="Number of hosts to connect to switch", 24 | type=int, 25 | action="store", 26 | default=2, 27 | ) 28 | parser.add_argument( 29 | "--p4-file", help="Path to P4 file", type=str, action="store", required=False 30 | ) 31 | 32 | args = parser.parse_args() 33 | 34 | 35 | def get_all_virtual_interfaces(): 36 | try: 37 | return ( 38 | subprocess.check_output( 39 | ["ip addr | grep s.-eth. | cut -d':' -f2 | cut -d'@' -f1"], shell=True 40 | ) 41 | .decode(sys.stdout.encoding) 42 | .splitlines() 43 | ) 44 | except subprocess.CalledProcessError as e: 45 | print("Cannot retrieve interfaces.") 46 | print(e) 47 | return "" 48 | 49 | 50 | class SingleSwitchTopo(Topo): 51 | "Single switch connected to n (< 256) hosts." 52 | 53 | def __init__(self, sw_path, json_path, n, **opts): 54 | # Initialize topology and default options 55 | Topo.__init__(self, **opts) 56 | switch = self.addSwitch( 57 | "s0", 58 | sw_path=sw_path, 59 | json_path=json_path, 60 | grpc_port=50051, 61 | device_id=1, 62 | cpu_port="255", 63 | ) 64 | 65 | for h in range(0, n): 66 | host = self.addHost( 67 | "h%d" % (h + 1), 68 | ip="10.10.10.%d/16" % (h + 1), 69 | mac="00:04:00:00:00:%02x" % h, 70 | ) 71 | self.addLink(host, switch) 72 | server = self.addHost("server", ip="10.10.3.3/16", mac="00:00:01:01:01:01") 73 | self.addLink(server, switch) 74 | 75 | 76 | def main(): 77 | num_hosts = int(args.num_hosts) 78 | result = os.system( 79 | "p4c --target bmv2 --arch v1model --p4runtime-files firmeware.p4info.txt " 80 | + args.p4_file 81 | ) 82 | p4_file = args.p4_file.split("/")[-1] 83 | json_file = p4_file.split(".")[0] + ".json" 84 | 85 | topo = SingleSwitchTopo("simple_switch_grpc", json_file, num_hosts) 86 | net = Mininet( 87 | topo=topo, host=P4Host, switch=P4GrpcSwitch, link=TCLink, controller=None 88 | ) 89 | net.start() 90 | 91 | interfaces = get_all_virtual_interfaces() 92 | for i in interfaces: 93 | if i != "": 94 | os.system("ip link set {} mtu 1600 > /dev/null".format(i)) 95 | os.system("ethtool --offload {} rx off tx off > /dev/null".format(i)) 96 | net.staticArp() 97 | 98 | if result != 0: 99 | print("Error while compiling!") 100 | exit() 101 | 102 | switch_running = "simple_switch_grpc" in (p.name() for p in psutil.process_iter()) 103 | if switch_running == False: 104 | print("The switch didnt start correctly! Check the path to your P4 file!!") 105 | exit() 106 | 107 | print("Starting mininet!") 108 | 109 | CLI(net) 110 | 111 | 112 | if __name__ == "__main__": 113 | setLogLevel("info") 114 | main() 115 | -------------------------------------------------------------------------------- /simple.p4: -------------------------------------------------------------------------------- 1 | /* -*- P4_16 -*- */ 2 | /* Copyright 2019 Belma Turkovic*/ 3 | /* TU Delft Embedded and Networked Systems Group.*/ 4 | 5 | #include 6 | #include 7 | 8 | const bit<16> TYPE_IPV4 = 0x800; 9 | const bit<8> TYPE_TCP = 6; 10 | #define CPU_PORT 255 11 | /************************************************************************* 12 | *********************** H E A D E R S *********************************** 13 | *************************************************************************/ 14 | 15 | typedef bit<9> egressSpec_t; 16 | typedef bit<48> macAddr_t; 17 | typedef bit<32> ip4Addr_t; 18 | 19 | header ethernet_t { 20 | macAddr_t dstAddr; 21 | macAddr_t srcAddr; 22 | bit<16> etherType; 23 | } 24 | 25 | header ipv4_t { 26 | bit<4> version; 27 | bit<4> ihl; 28 | bit<8> diffserv; 29 | bit<16> totalLen; 30 | bit<16> identification; 31 | bit<3> flags; 32 | bit<13> fragOffset; 33 | bit<8> ttl; 34 | bit<8> protocol; 35 | bit<16> hdrChecksum; 36 | ip4Addr_t srcAddr; 37 | ip4Addr_t dstAddr; 38 | } 39 | 40 | header tcp_t{ 41 | bit<16> srcPort; 42 | bit<16> dstPort; 43 | bit<32> seqNo; 44 | bit<32> ackNo; 45 | bit<4> dataOffset; 46 | bit<4> res; 47 | bit<1> cwr; 48 | bit<1> ece; 49 | bit<1> urg; 50 | bit<1> ack; 51 | bit<1> psh; 52 | bit<1> rst; 53 | bit<1> syn; 54 | bit<1> fin; 55 | bit<16> window; 56 | bit<16> checksum; 57 | bit<16> urgentPtr; 58 | } 59 | 60 | struct metadata { 61 | } 62 | 63 | @controller_header("packet_in") 64 | header packet_in_t { 65 | bit<16> ingress_port; 66 | } 67 | 68 | @controller_header("packet_out") 69 | header packet_out_t { 70 | bit<16> egress_spec; 71 | } 72 | 73 | struct headers { 74 | packet_in_t packetin; 75 | packet_out_t packetout; 76 | ethernet_t ethernet; 77 | ipv4_t ipv4; 78 | //tcp_t tcp; 79 | } 80 | 81 | 82 | /************************************************************************* 83 | *********************** P A R S E R *********************************** 84 | *************************************************************************/ 85 | 86 | parser MyParser(packet_in packet, 87 | out headers hdr, 88 | inout metadata meta, 89 | inout standard_metadata_t standard_metadata) { 90 | 91 | state start { 92 | transition select(standard_metadata.ingress_port){ 93 | CPU_PORT: parse_packet_out; 94 | default: parse_ethernet; 95 | } 96 | } 97 | state parse_packet_out { 98 | packet.extract(hdr.packetout); 99 | transition parse_ethernet; 100 | } 101 | 102 | state parse_ethernet { 103 | packet.extract(hdr.ethernet); 104 | transition select(hdr.ethernet.etherType) { 105 | TYPE_IPV4: parse_ipv4; 106 | default: accept; 107 | } 108 | } 109 | 110 | state parse_ipv4 { 111 | packet.extract(hdr.ipv4); 112 | transition accept; 113 | } 114 | 115 | } 116 | 117 | /************************************************************************* 118 | ************ C H E C K S U M V E R I F I C A T I O N ************* 119 | *************************************************************************/ 120 | 121 | control MyVerifyChecksum(inout headers hdr, inout metadata meta) { 122 | apply { } 123 | } 124 | 125 | 126 | /************************************************************************* 127 | ************** I N G R E S S P R O C E S S I N G ******************* 128 | *************************************************************************/ 129 | 130 | control MyIngress(inout headers hdr, 131 | inout metadata meta, 132 | inout standard_metadata_t standard_metadata) { 133 | 134 | action drop() { 135 | mark_to_drop(standard_metadata); 136 | } 137 | 138 | action ipv4_forward(egressSpec_t port) { 139 | standard_metadata.egress_spec = port; 140 | } 141 | 142 | table ipv4_lpm { 143 | key = { 144 | hdr.ipv4.dstAddr: lpm; 145 | } 146 | actions = { 147 | ipv4_forward; 148 | drop; 149 | NoAction; 150 | } 151 | size = 1024; 152 | default_action = drop(); 153 | } 154 | 155 | apply { 156 | if (hdr.ipv4.isValid() && standard_metadata.ingress_port!=5) { 157 | ipv4_lpm.apply(); 158 | } 159 | 160 | if (standard_metadata.ingress_port == CPU_PORT) { 161 | standard_metadata.egress_spec = (bit<9>)hdr.packetout.egress_spec; 162 | hdr.packetout.setInvalid(); 163 | } 164 | if (standard_metadata.egress_spec == CPU_PORT) { // packet in 165 | hdr.packetin.setValid(); 166 | hdr.packetin.ingress_port = (bit<16>)standard_metadata.ingress_port; 167 | } 168 | } 169 | } 170 | 171 | /************************************************************************* 172 | **************** E G R E S S P R O C E S S I N G ******************* 173 | *************************************************************************/ 174 | 175 | control MyEgress(inout headers hdr, 176 | inout metadata meta, 177 | inout standard_metadata_t standard_metadata) { 178 | apply { } 179 | } 180 | 181 | /************************************************************************* 182 | ************* C H E C K S U M C O M P U T A T I O N ************** 183 | *************************************************************************/ 184 | 185 | control MyComputeChecksum(inout headers hdr, inout metadata meta) { 186 | apply { 187 | //update_checksum( 188 | //hdr.ipv4.isValid(), 189 | //{ hdr.ipv4.version, 190 | // hdr.ipv4.ihl, 191 | // hdr.ipv4.diffserv, 192 | // hdr.ipv4.totalLen, 193 | // hdr.ipv4.identification, 194 | // hdr.ipv4.flags, 195 | // hdr.ipv4.fragOffset, 196 | // hdr.ipv4.ttl, 197 | // hdr.ipv4.protocol, 198 | // hdr.ipv4.srcAddr, 199 | // hdr.ipv4.dstAddr }, 200 | //hdr.ipv4.hdrChecksum, 201 | //HashAlgorithm.csum16); 202 | } 203 | } 204 | 205 | /************************************************************************* 206 | *********************** D E P A R S E R ******************************* 207 | *************************************************************************/ 208 | 209 | control MyDeparser(packet_out packet, in headers hdr) { 210 | apply { 211 | packet.emit(hdr); 212 | //packet.emit(hdr.ipv4); 213 | //packet.emit(hdr.tcp); 214 | } 215 | } 216 | 217 | /************************************************************************* 218 | *********************** S W I T C H ******************************* 219 | *************************************************************************/ 220 | 221 | V1Switch( 222 | MyParser(), 223 | MyVerifyChecksum(), 224 | MyIngress(), 225 | MyEgress(), 226 | MyComputeChecksum(), 227 | MyDeparser() 228 | ) main; 229 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/demian91/Simple-P4runtime-Controller/292d64c2da4e6c70aa9e91fbccffac5fe4007598/utils/__init__.py -------------------------------------------------------------------------------- /utils/bmv2.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 | from switch import SwitchConnection 16 | from p4.tmp import p4config_pb2 17 | 18 | 19 | def buildDeviceConfig(bmv2_json_file_path=None): 20 | "Builds the device config for BMv2" 21 | device_config = p4config_pb2.P4DeviceConfig() 22 | device_config.reassign = True 23 | with open(bmv2_json_file_path) as f: 24 | device_config.device_data = f.read().encode("utf-8") 25 | return device_config 26 | 27 | 28 | class Bmv2SwitchConnection(SwitchConnection): 29 | def buildDeviceConfig(self, **kwargs): 30 | return buildDeviceConfig(**kwargs) 31 | -------------------------------------------------------------------------------- /utils/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 | """ 20 | This package contains several helper functions for encoding to and decoding from byte strings: 21 | - integers 22 | - IPv4 address strings 23 | - Ethernet address strings 24 | """ 25 | 26 | mac_pattern = re.compile("^([\da-fA-F]{2}:){5}([\da-fA-F]{2})$") 27 | 28 | 29 | def matchesMac(mac_addr_string): 30 | return mac_pattern.match(mac_addr_string) is not None 31 | 32 | 33 | def encodeMac(mac_addr_string): 34 | return bytes.fromhex(mac_addr_string.replace(":", "")) 35 | 36 | 37 | def decodeMac(encoded_mac_addr): 38 | return ":".join(s.hex() for s in encoded_mac_addr) 39 | 40 | 41 | ip_pattern = re.compile("^(\d{1,3}\.){3}(\d{1,3})$") 42 | 43 | 44 | def matchesIPv4(ip_addr_string): 45 | return ip_pattern.match(ip_addr_string) is not None 46 | 47 | 48 | def encodeIPv4(ip_addr_string): 49 | return socket.inet_aton(ip_addr_string) 50 | 51 | 52 | def decodeIPv4(encoded_ip_addr): 53 | return socket.inet_ntoa(encoded_ip_addr) 54 | 55 | 56 | def bitwidthToBytes(bitwidth): 57 | return int(math.ceil(bitwidth / 8.0)) 58 | 59 | 60 | def encodeNum(number, bitwidth): 61 | byte_len = bitwidthToBytes(bitwidth) 62 | num_str = "%x" % number 63 | if number >= 2**bitwidth: 64 | raise Exception("Number, %d, does not fit in %d bits" % (number, bitwidth)) 65 | return bytes.fromhex("0" * (byte_len * 2 - len(num_str)) + num_str) 66 | 67 | 68 | def decodeNum(encoded_number): 69 | return int(encoded_number.hex(), 16) 70 | 71 | 72 | def encode(x, bitwidth): 73 | "Tries to infer the type of `x` and encode it" 74 | byte_len = bitwidthToBytes(bitwidth) 75 | if (type(x) == list or type(x) == tuple) and len(x) == 1: 76 | x = x[0] 77 | encoded_bytes = None 78 | if type(x) == str: 79 | if matchesMac(x): 80 | encoded_bytes = encodeMac(x) 81 | elif matchesIPv4(x): 82 | encoded_bytes = encodeIPv4(x) 83 | else: 84 | # Assume that the string is already encoded 85 | encoded_bytes = x 86 | elif type(x) == int: 87 | encoded_bytes = encodeNum(x, bitwidth) 88 | else: 89 | raise Exception("Encoding objects of %r is not supported" % type(x)) 90 | assert len(encoded_bytes) == byte_len 91 | return encoded_bytes 92 | 93 | 94 | if __name__ == "__main__": 95 | # TODO These tests should be moved out of main eventually 96 | mac = "aa:bb:cc:dd:ee:ff" 97 | enc_mac = encodeMac(mac) 98 | assert enc_mac == "\xaa\xbb\xcc\xdd\xee\xff" 99 | dec_mac = decodeMac(enc_mac) 100 | assert mac == dec_mac 101 | 102 | ip = "10.0.0.1" 103 | enc_ip = encodeIPv4(ip) 104 | assert enc_ip == "\x0a\x00\x00\x01" 105 | dec_ip = decodeIPv4(enc_ip) 106 | assert ip == dec_ip 107 | 108 | num = 1337 109 | byte_len = 5 110 | enc_num = encodeNum(num, byte_len * 8) 111 | assert enc_num == "\x00\x00\x00\x05\x39" 112 | dec_num = decodeNum(enc_num) 113 | assert num == dec_num 114 | 115 | assert matchesIPv4("10.0.0.1") 116 | assert not matchesIPv4("10.0.0.1.5") 117 | assert not matchesIPv4("1000.0.0.1") 118 | assert not matchesIPv4("10001") 119 | 120 | assert encode(mac, 6 * 8) == enc_mac 121 | assert encode(ip, 4 * 8) == enc_ip 122 | assert encode(num, 5 * 8) == enc_num 123 | assert encode((num,), 5 * 8) == enc_num 124 | assert encode([num], 5 * 8) == enc_num 125 | 126 | num = 256 127 | byte_len = 2 128 | try: 129 | enc_num = encodeNum(num, 8) 130 | raise Exception("expected exception") 131 | except Exception as e: 132 | print(e) 133 | -------------------------------------------------------------------------------- /utils/error_utils.py: -------------------------------------------------------------------------------- 1 | # Copyright 2013-present Barefoot Networks, Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # 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 | import sys 17 | 18 | from google.rpc import status_pb2, code_pb2 19 | import grpc 20 | from p4.v1 import p4runtime_pb2 21 | from p4.v1 import p4runtime_pb2_grpc 22 | 23 | # Used to indicate that the gRPC error Status object returned by the server has 24 | # an incorrect format. 25 | class P4RuntimeErrorFormatException(Exception): 26 | def __init__(self, message): 27 | super(P4RuntimeErrorFormatException, self).__init__(message) 28 | 29 | 30 | # Parse the binary details of the gRPC error. This is required to print some 31 | # helpful debugging information in tha case of batched Write / Read 32 | # requests. Returns None if there are no useful binary details and throws 33 | # P4RuntimeErrorFormatException if the error is not formatted 34 | # properly. Otherwise, returns a list of tuples with the first element being the 35 | # index of the operation in the batch that failed and the second element being 36 | # the p4.Error Protobuf message. 37 | def parseGrpcErrorBinaryDetails(grpc_error): 38 | if grpc_error.code() != grpc.StatusCode.UNKNOWN: 39 | return None 40 | 41 | error = None 42 | # The gRPC Python package does not have a convenient way to access the 43 | # binary details for the error: they are treated as trailing metadata. 44 | for meta in grpc_error.trailing_metadata(): 45 | if meta[0] == "grpc-status-details-bin": 46 | error = status_pb2.Status() 47 | error.ParseFromString(meta[1]) 48 | break 49 | if error is None: # no binary details field 50 | return None 51 | if len(error.details) == 0: 52 | # binary details field has empty Any details repeated field 53 | return None 54 | 55 | indexed_p4_errors = [] 56 | for idx, one_error_any in enumerate(error.details): 57 | p4_error = p4runtime_pb2.Error() 58 | if not one_error_any.Unpack(p4_error): 59 | raise P4RuntimeErrorFormatException( 60 | "Cannot convert Any message to p4.Error") 61 | if p4_error.canonical_code == code_pb2.OK: 62 | continue 63 | indexed_p4_errors += [(idx, p4_error)] 64 | 65 | return indexed_p4_errors 66 | 67 | 68 | # P4Runtime uses a 3-level message in case of an error during the processing of 69 | # a write batch. This means that some care is required when printing the 70 | # exception if we do not want to end-up with a non-helpful message in case of 71 | # failure as only the first level will be printed. In this function, we extract 72 | # the nested error message when present (one for each operation included in the 73 | # batch) in order to print error code + user-facing message. See P4Runtime 74 | # documentation for more details on error-reporting. 75 | def printGrpcError(grpc_error): 76 | print "gRPC Error", grpc_error.details(), 77 | status_code = grpc_error.code() 78 | print "({})".format(status_code.name), 79 | traceback = sys.exc_info()[2] 80 | print "[{}:{}]".format( 81 | traceback.tb_frame.f_code.co_filename, traceback.tb_lineno) 82 | if status_code != grpc.StatusCode.UNKNOWN: 83 | return 84 | p4_errors = parseGrpcErrorBinaryDetails(grpc_error) 85 | if p4_errors is None: 86 | return 87 | print "Errors in batch:" 88 | for idx, p4_error in p4_errors: 89 | code_name = code_pb2._CODE.values_by_number[ 90 | p4_error.canonical_code].name 91 | print "\t* At index {}: {}, '{}'\n".format( 92 | idx, code_name, p4_error.message) 93 | -------------------------------------------------------------------------------- /utils/helper.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Belma Turkovic 2 | # TU Delft Embedded and Networked Systems Group. 3 | # NOTICE: THIS FILE IS BASED ON https://github.com/p4lang/tutorials/tree/master/exercises/p4runtime, BUT WAS MODIFIED UNDER COMPLIANCE 4 | # WITH THE APACHE 2.0 LICENCE FROM THE ORIGINAL WORK. THE FOLLOWING IS THE COPYRIGHT OF THE ORIGINAL DOCUMENT: 5 | # 6 | # Copyright 2017-present Open Networking Foundation 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | import re 21 | 22 | import google.protobuf.text_format 23 | from p4.v1 import p4runtime_pb2 24 | from p4.config.v1 import p4info_pb2 25 | 26 | from convert import encode 27 | 28 | 29 | class P4InfoHelper(object): 30 | def __init__(self, p4_info_filepath): 31 | p4info = p4info_pb2.P4Info() 32 | # Load the p4info file into a skeleton P4Info object 33 | with open(p4_info_filepath) as p4info_f: 34 | google.protobuf.text_format.Merge(p4info_f.read(), p4info) 35 | self.p4info = p4info 36 | 37 | def get(self, entity_type, name=None, id=None): 38 | if name is not None and id is not None: 39 | raise AssertionError("name or id must be None") 40 | 41 | for o in getattr(self.p4info, entity_type): 42 | pre = o.preamble 43 | if name: 44 | if pre.name == name or pre.alias == name: 45 | return o 46 | else: 47 | if pre.id == id: 48 | return o 49 | 50 | if name: 51 | raise AttributeError("Could not find %r of type %s" % (name, entity_type)) 52 | else: 53 | raise AttributeError("Could not find id %r of type %s" % (id, entity_type)) 54 | 55 | def get_id(self, entity_type, name): 56 | return self.get(entity_type, name=name).preamble.id 57 | 58 | def get_name(self, entity_type, id): 59 | return self.get(entity_type, id=id).preamble.name 60 | 61 | def get_alias(self, entity_type, id): 62 | return self.get(entity_type, id=id).preamble.alias 63 | 64 | def __getattr__(self, attr): 65 | # Synthesize convenience functions for name to id lookups for top-level entities 66 | # e.g. get_tables_id(name_string) or get_actions_id(name_string) 67 | m = re.search("^get_(\w+)_id$", attr) 68 | if m: 69 | primitive = m.group(1) 70 | return lambda name: self.get_id(primitive, name) 71 | 72 | # Synthesize convenience functions for id to name lookups 73 | # e.g. get_tables_name(id) or get_actions_name(id) 74 | m = re.search("^get_(\w+)_name$", attr) 75 | if m: 76 | primitive = m.group(1) 77 | return lambda id: self.get_name(primitive, id) 78 | 79 | raise AttributeError("%r object has no attribute %r" % (self.__class__, attr)) 80 | 81 | def get_match_field(self, table_name, name=None, id=None): 82 | for t in self.p4info.tables: 83 | pre = t.preamble 84 | if pre.name == table_name: 85 | for mf in t.match_fields: 86 | if name is not None: 87 | if mf.name == name: 88 | return mf 89 | elif id is not None: 90 | if mf.id == id: 91 | return mf 92 | raise AttributeError( 93 | "%r has no attribute %r" % (table_name, name if name is not None else id) 94 | ) 95 | 96 | def get_match_field_id(self, table_name, match_field_name): 97 | return self.get_match_field(table_name, name=match_field_name).id 98 | 99 | def get_match_field_name(self, table_name, match_field_id): 100 | return self.get_match_field(table_name, id=match_field_id).name 101 | 102 | def get_match_field_pb(self, table_name, match_field_name, value): 103 | p4info_match = self.get_match_field(table_name, match_field_name) 104 | bitwidth = p4info_match.bitwidth 105 | p4runtime_match = p4runtime_pb2.FieldMatch() 106 | p4runtime_match.field_id = p4info_match.id 107 | match_type = p4info_match.match_type 108 | # change vaild => to unspecified 109 | if match_type == p4info_pb2.MatchField.UNSPECIFIED: 110 | valid = p4runtime_match.valid 111 | valid.value = bool(value) 112 | elif match_type == p4info_pb2.MatchField.EXACT: 113 | exact = p4runtime_match.exact 114 | exact.value = encode(value, bitwidth) 115 | elif match_type == p4info_pb2.MatchField.LPM: 116 | lpm = p4runtime_match.lpm 117 | lpm.value = encode(value[0], bitwidth) 118 | lpm.prefix_len = value[1] 119 | elif match_type == p4info_pb2.MatchField.TERNARY: 120 | lpm = p4runtime_match.ternary 121 | lpm.value = encode(value[0], bitwidth) 122 | lpm.mask = encode(value[1], bitwidth) 123 | elif match_type == p4info_pb2.MatchField.RANGE: 124 | lpm = p4runtime_match.range 125 | lpm.low = encode(value[0], bitwidth) 126 | lpm.high = encode(value[1], bitwidth) 127 | else: 128 | raise Exception("Unsupported match type with type %r" % match_type) 129 | return p4runtime_match 130 | 131 | def get_match_field_value(self, match_field): 132 | match_type = match_field.WhichOneof("field_match_type") 133 | if match_type == "valid": 134 | return match_field.valid.value 135 | elif match_type == "exact": 136 | return match_field.exact.value 137 | elif match_type == "lpm": 138 | return (match_field.lpm.value, match_field.lpm.prefix_len) 139 | elif match_type == "ternary": 140 | return (match_field.ternary.value, match_field.ternary.mask) 141 | elif match_type == "range": 142 | return (match_field.range.low, match_field.range.high) 143 | else: 144 | raise Exception("Unsupported match type with type %r" % match_type) 145 | 146 | def get_action_param(self, action_name, name=None, id=None): 147 | for a in self.p4info.actions: 148 | pre = a.preamble 149 | if pre.name == action_name: 150 | for p in a.params: 151 | if name is not None: 152 | if p.name == name: 153 | return p 154 | elif id is not None: 155 | if p.id == id: 156 | return p 157 | raise AttributeError( 158 | "action %r has no param %r, (has: %r)" 159 | % (action_name, name if name is not None else id, a.params) 160 | ) 161 | 162 | def get_action_param_id(self, action_name, param_name): 163 | return self.get_action_param(action_name, name=param_name).id 164 | 165 | def get_action_param_name(self, action_name, param_id): 166 | return self.get_action_param(action_name, id=param_id).name 167 | 168 | def get_action_param_pb(self, action_name, param_name, value): 169 | p4info_param = self.get_action_param(action_name, param_name) 170 | p4runtime_param = p4runtime_pb2.Action.Param() 171 | p4runtime_param.param_id = p4info_param.id 172 | p4runtime_param.value = encode(value, p4info_param.bitwidth) 173 | return p4runtime_param 174 | 175 | # get replicas 176 | def get_replicas_pb(self, egress_port, instance): 177 | p4runtime_replicas = p4runtime_pb2.Replica() 178 | p4runtime_replicas.egress_port = egress_port 179 | p4runtime_replicas.instance = instance 180 | return p4runtime_replicas 181 | 182 | # get metadata 183 | def get_metadata_pb(self, metadata_id, value): 184 | p4runtime_metadata = p4runtime_pb2.PacketMetadata() 185 | p4runtime_metadata.metadata_id = metadata_id 186 | p4runtime_metadata.value = value 187 | return p4runtime_metadata 188 | 189 | # get mc_group_entry 190 | def buildMCEntry(self, mc_group_id, replicas=None): 191 | mc_group_entry = p4runtime_pb2.MulticastGroupEntry() 192 | mc_group_entry.multicast_group_id = mc_group_id 193 | if replicas: 194 | mc_group_entry.replicas.extend( 195 | [ 196 | self.get_replicas_pb(egress_port, instance) 197 | for egress_port, instance in replicas.iteritems() 198 | ] 199 | ) 200 | return mc_group_entry 201 | 202 | # get packetout 203 | def buildPacketOut(self, payload, metadata=None): 204 | packet_out = p4runtime_pb2.PacketOut() 205 | packet_out.payload = payload 206 | if metadata: 207 | packet_out.metadata.extend( 208 | [ 209 | self.get_metadata_pb(metadata_id, value) 210 | for metadata_id, value in metadata.items() 211 | ] 212 | ) 213 | return packet_out 214 | 215 | def buildTableEntry( 216 | self, 217 | table_name, 218 | match_fields=None, 219 | default_action=False, 220 | action_name=None, 221 | action_params=None, 222 | priority=None, 223 | ): 224 | table_entry = p4runtime_pb2.TableEntry() 225 | table_entry.table_id = self.get_tables_id(table_name) 226 | 227 | if priority is not None: 228 | table_entry.priority = priority 229 | 230 | if match_fields: 231 | table_entry.match.extend( 232 | [ 233 | self.get_match_field_pb(table_name, match_field_name, value) 234 | for match_field_name, value in match_fields.items() 235 | ] 236 | ) 237 | 238 | if default_action: 239 | table_entry.is_default_action = True 240 | 241 | if action_name: 242 | action = table_entry.action.action 243 | action.action_id = self.get_actions_id(action_name) 244 | if action_params: 245 | action.params.extend( 246 | [ 247 | self.get_action_param_pb(action_name, field_name, value) 248 | for field_name, value in action_params.items() 249 | ] 250 | ) 251 | return table_entry 252 | -------------------------------------------------------------------------------- /utils/simple_controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # 3 | # Copyright 2017-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 | import json 19 | import os 20 | import sys 21 | 22 | import bmv2 23 | import helper 24 | 25 | 26 | def error(msg): 27 | print >> sys.stderr, ' - ERROR! ' + msg 28 | 29 | def info(msg): 30 | print >> sys.stdout, ' - ' + msg 31 | 32 | 33 | class ConfException(Exception): 34 | pass 35 | 36 | 37 | def main(): 38 | parser = argparse.ArgumentParser(description='P4Runtime Simple Controller') 39 | 40 | parser.add_argument('-a', '--p4runtime-server-addr', 41 | help='address and port of the switch\'s P4Runtime server (e.g. 192.168.0.1:50051)', 42 | type=str, action="store", required=True) 43 | parser.add_argument('-d', '--device-id', 44 | help='Internal device ID to use in P4Runtime messages', 45 | type=int, action="store", required=True) 46 | parser.add_argument('-p', '--proto-dump-file', 47 | help='path to file where to dump protobuf messages sent to the switch', 48 | type=str, action="store", required=True) 49 | parser.add_argument("-c", '--runtime-conf-file', 50 | help="path to input runtime configuration file (JSON)", 51 | type=str, action="store", required=True) 52 | 53 | args = parser.parse_args() 54 | 55 | if not os.path.exists(args.runtime_conf_file): 56 | parser.error("File %s does not exist!" % args.runtime_conf_file) 57 | workdir = os.path.dirname(os.path.abspath(args.runtime_conf_file)) 58 | with open(args.runtime_conf_file, 'r') as sw_conf_file: 59 | program_switch(addr=args.p4runtime_server_addr, 60 | device_id=args.device_id, 61 | sw_conf_file=sw_conf_file, 62 | workdir=workdir, 63 | proto_dump_fpath=args.proto_dump_file) 64 | 65 | 66 | def check_switch_conf(sw_conf, workdir): 67 | required_keys = ["p4info"] 68 | files_to_check = ["p4info"] 69 | target_choices = ["bmv2"] 70 | 71 | if "target" not in sw_conf: 72 | raise ConfException("missing key 'target'") 73 | target = sw_conf['target'] 74 | if target not in target_choices: 75 | raise ConfException("unknown target '%s'" % target) 76 | 77 | if target == 'bmv2': 78 | required_keys.append("bmv2_json") 79 | files_to_check.append("bmv2_json") 80 | 81 | for conf_key in required_keys: 82 | if conf_key not in sw_conf or len(sw_conf[conf_key]) == 0: 83 | raise ConfException("missing key '%s' or empty value" % conf_key) 84 | 85 | for conf_key in files_to_check: 86 | real_path = os.path.join(workdir, sw_conf[conf_key]) 87 | if not os.path.exists(real_path): 88 | raise ConfException("file does not exist %s" % real_path) 89 | 90 | 91 | def program_switch(addr, device_id, sw_conf_file, workdir, proto_dump_fpath): 92 | sw_conf = json_load_byteified(sw_conf_file) 93 | try: 94 | check_switch_conf(sw_conf=sw_conf, workdir=workdir) 95 | except ConfException as e: 96 | error("While parsing input runtime configuration: %s" % str(e)) 97 | return 98 | 99 | info('Using P4Info file %s...' % sw_conf['p4info']) 100 | p4info_fpath = os.path.join(workdir, sw_conf['p4info']) 101 | p4info_helper = helper.P4InfoHelper(p4info_fpath) 102 | 103 | target = sw_conf['target'] 104 | 105 | info("Connecting to P4Runtime server on %s (%s)..." % (addr, target)) 106 | 107 | if target == "bmv2": 108 | sw = bmv2.Bmv2SwitchConnection(address=addr, device_id=device_id, 109 | proto_dump_file=proto_dump_fpath) 110 | else: 111 | raise Exception("Don't know how to connect to target %s" % target) 112 | 113 | try: 114 | sw.MasterArbitrationUpdate() 115 | 116 | if target == "bmv2": 117 | info("Setting pipeline config (%s)..." % sw_conf['bmv2_json']) 118 | bmv2_json_fpath = os.path.join(workdir, sw_conf['bmv2_json']) 119 | sw.SetForwardingPipelineConfig(p4info=p4info_helper.p4info, 120 | bmv2_json_file_path=bmv2_json_fpath) 121 | else: 122 | raise Exception("Should not be here") 123 | 124 | if 'table_entries' in sw_conf: 125 | table_entries = sw_conf['table_entries'] 126 | info("Inserting %d table entries..." % len(table_entries)) 127 | for entry in table_entries: 128 | info(tableEntryToString(entry)) 129 | insertTableEntry(sw, entry, p4info_helper) 130 | 131 | if 'multicast_group_entries' in sw_conf: 132 | group_entries = sw_conf['multicast_group_entries'] 133 | info("Inserting %d group entries..." % len(group_entries)) 134 | for entry in group_entries: 135 | info(groupEntryToString(entry)) 136 | insertMulticastGroupEntry(sw, entry, p4info_helper) 137 | 138 | if 'clone_session_entries' in sw_conf: 139 | clone_entries = sw_conf['clone_session_entries'] 140 | info("Inserting %d clone entries..." % len(clone_entries)) 141 | for entry in clone_entries: 142 | info(cloneEntryToString(entry)) 143 | insertCloneGroupEntry(sw, entry, p4info_helper) 144 | 145 | finally: 146 | sw.shutdown() 147 | 148 | 149 | def insertTableEntry(sw, flow, p4info_helper): 150 | table_name = flow['table'] 151 | match_fields = flow.get('match') # None if not found 152 | action_name = flow['action_name'] 153 | default_action = flow.get('default_action') # None if not found 154 | action_params = flow['action_params'] 155 | priority = flow.get('priority') # None if not found 156 | 157 | table_entry = p4info_helper.buildTableEntry( 158 | table_name=table_name, 159 | match_fields=match_fields, 160 | default_action=default_action, 161 | action_name=action_name, 162 | action_params=action_params, 163 | priority=priority) 164 | 165 | sw.WriteTableEntry(table_entry) 166 | 167 | 168 | # object hook for josn library, use str instead of unicode object 169 | # https://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-from-json 170 | def json_load_byteified(file_handle): 171 | return _byteify(json.load(file_handle, object_hook=_byteify), 172 | ignore_dicts=True) 173 | 174 | 175 | def _byteify(data, ignore_dicts=False): 176 | # if this is a unicode string, return its string representation 177 | if isinstance(data, unicode): 178 | return data.encode('utf-8') 179 | # if this is a list of values, return list of byteified values 180 | if isinstance(data, list): 181 | return [_byteify(item, ignore_dicts=True) for item in data] 182 | # if this is a dictionary, return dictionary of byteified keys and values 183 | # but only if we haven't already byteified it 184 | if isinstance(data, dict) and not ignore_dicts: 185 | return { 186 | _byteify(key, ignore_dicts=True): _byteify(value, ignore_dicts=True) 187 | for key, value in data.iteritems() 188 | } 189 | # if it's anything else, return it in its original form 190 | return data 191 | 192 | 193 | def tableEntryToString(flow): 194 | if 'match' in flow: 195 | match_str = ['%s=%s' % (match_name, str(flow['match'][match_name])) for match_name in 196 | flow['match']] 197 | match_str = ', '.join(match_str) 198 | elif 'default_action' in flow and flow['default_action']: 199 | match_str = '(default action)' 200 | else: 201 | match_str = '(any)' 202 | params = ['%s=%s' % (param_name, str(flow['action_params'][param_name])) for param_name in 203 | flow['action_params']] 204 | params = ', '.join(params) 205 | return "%s: %s => %s(%s)" % ( 206 | flow['table'], match_str, flow['action_name'], params) 207 | 208 | 209 | def groupEntryToString(rule): 210 | group_id = rule["multicast_group_id"] 211 | replicas = ['%d' % replica["egress_port"] for replica in rule['replicas']] 212 | ports_str = ', '.join(replicas) 213 | return 'Group {0} => ({1})'.format(group_id, ports_str) 214 | 215 | def cloneEntryToString(rule): 216 | clone_id = rule["clone_session_id"] 217 | if "packet_length_bytes" in rule: 218 | packet_length_bytes = str(rule["packet_length_bytes"])+"B" 219 | else: 220 | packet_length_bytes = "NO_TRUNCATION" 221 | replicas = ['%d' % replica["egress_port"] for replica in rule['replicas']] 222 | ports_str = ', '.join(replicas) 223 | return 'Clone Session {0} => ({1}) ({2})'.format(clone_id, ports_str, packet_length_bytes) 224 | 225 | def insertMulticastGroupEntry(sw, rule, p4info_helper): 226 | mc_entry = p4info_helper.buildMulticastGroupEntry(rule["multicast_group_id"], rule['replicas']) 227 | sw.WritePREEntry(mc_entry) 228 | 229 | def insertCloneGroupEntry(sw, rule, p4info_helper): 230 | clone_entry = p4info_helper.buildCloneSessionEntry(rule['clone_session_id'], rule['replicas'], 231 | rule.get('packet_length_bytes', 0)) 232 | sw.WritePREEntry(clone_entry) 233 | 234 | 235 | if __name__ == '__main__': 236 | main() 237 | -------------------------------------------------------------------------------- /utils/switch.py: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Belma Turkovic 2 | # TU Delft Embedded and Networked Systems Group. 3 | # NOTICE: THIS FILE IS BASED ON https://github.com/p4lang/tutorials/tree/master/exercises/p4runtime, BUT WAS MODIFIED UNDER COMPLIANCE 4 | # WITH THE APACHE 2.0 LICENCE FROM THE ORIGINAL WORK. THE FOLLOWING IS THE COPYRIGHT OF THE ORIGINAL DOCUMENT: 5 | # 6 | # Copyright 2017-present Open Networking Foundation 7 | # 8 | # Licensed under the Apache License, Version 2.0 (the "License"); 9 | # you may not use this file except in compliance with the License. 10 | # You may obtain a copy of the License at 11 | # 12 | # http://www.apache.org/licenses/LICENSE-2.0 13 | # 14 | # Unless required by applicable law or agreed to in writing, software 15 | # distributed under the License is distributed on an "AS IS" BASIS, 16 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | # See the License for the specific language governing permissions and 18 | # limitations under the License. 19 | # 20 | from queue import Queue 21 | from abc import abstractmethod 22 | from datetime import datetime 23 | import threading 24 | import grpc 25 | from p4.v1 import p4runtime_pb2 26 | from p4.v1 import p4runtime_pb2_grpc 27 | from p4.tmp import p4config_pb2 28 | import time 29 | 30 | MSG_LOG_MAX_LEN = 1024 31 | 32 | # List of all active connections 33 | connections = [] 34 | 35 | 36 | def ShutdownAllSwitchConnections(): 37 | for c in connections: 38 | c.shutdown() 39 | 40 | 41 | class SwitchConnection(object): 42 | def __init__( 43 | self, name=None, address="127.0.0.1:50051", device_id=0, proto_dump_file=None 44 | ): 45 | self.name = name 46 | self.address = address 47 | self.device_id = device_id 48 | self.p4info = None 49 | self.channel = grpc.insecure_channel(self.address) 50 | if proto_dump_file is not None: 51 | interceptor = GrpcRequestLogger(proto_dump_file) 52 | self.channel = grpc.intercept_channel(self.channel, interceptor) 53 | self.client_stub = p4runtime_pb2_grpc.P4RuntimeStub(self.channel) 54 | # create requests queue 55 | self.requests_stream = IterableQueue() 56 | # get response via requests queue 57 | self.stream_msg_resp = self.client_stub.StreamChannel( 58 | iter(self.requests_stream) 59 | ) 60 | self.proto_dump_file = proto_dump_file 61 | connections.append(self) 62 | 63 | @abstractmethod 64 | def buildDeviceConfig(self, **kwargs): 65 | return p4config_pb2.P4DeviceConfig() 66 | 67 | def shutdown(self): 68 | self.requests_stream.close() 69 | self.stream_msg_resp.cancel() 70 | 71 | def MasterArbitrationUpdate(self, dry_run=False, **kwargs): 72 | request = p4runtime_pb2.StreamMessageRequest() 73 | request.arbitration.device_id = self.device_id 74 | request.arbitration.election_id.high = 0 75 | request.arbitration.election_id.low = 1 76 | 77 | if dry_run: 78 | print("P4Runtime MasterArbitrationUpdate: ", request) 79 | else: 80 | self.requests_stream.put(request) 81 | for item in self.stream_msg_resp: 82 | return item # just one 83 | 84 | def SetForwardingPipelineConfig(self, p4info, dry_run=False, **kwargs): 85 | device_config = self.buildDeviceConfig(**kwargs) 86 | request = p4runtime_pb2.SetForwardingPipelineConfigRequest() 87 | request.election_id.low = 1 88 | request.device_id = self.device_id 89 | config = request.config 90 | 91 | config.p4info.CopyFrom(p4info) 92 | config.p4_device_config = device_config.SerializeToString() 93 | 94 | request.action = ( 95 | p4runtime_pb2.SetForwardingPipelineConfigRequest.VERIFY_AND_COMMIT 96 | ) 97 | if dry_run: 98 | print("P4Runtime SetForwardingPipelineConfig:", request) 99 | else: 100 | self.client_stub.SetForwardingPipelineConfig(request) 101 | 102 | def WriteTableEntry(self, table_entry, dry_run=False): 103 | request = p4runtime_pb2.WriteRequest() 104 | request.device_id = self.device_id 105 | request.election_id.low = 1 106 | update = request.updates.add() 107 | if table_entry.is_default_action: 108 | update.type = p4runtime_pb2.Update.MODIFY 109 | else: 110 | update.type = p4runtime_pb2.Update.INSERT 111 | update.entity.table_entry.CopyFrom(table_entry) 112 | if dry_run: 113 | print("P4Runtime Write:", request) 114 | else: 115 | self.client_stub.Write(request) 116 | 117 | def ModifyTableEntry(self, table_entry, dry_run=False): 118 | request = p4runtime_pb2.WriteRequest() 119 | request.device_id = self.device_id 120 | request.election_id.low = 1 121 | update = request.updates.add() 122 | update.type = p4runtime_pb2.Update.MODIFY 123 | update.entity.table_entry.CopyFrom(table_entry) 124 | if dry_run: 125 | print("P4Runtime Write:", request) 126 | else: 127 | self.client_stub.Write(request) 128 | 129 | def DeleteTableEntry(self, table_entry, dry_run=False): 130 | request = p4runtime_pb2.WriteRequest() 131 | request.device_id = self.device_id 132 | request.election_id.low = 1 133 | update = request.updates.add() 134 | update.type = p4runtime_pb2.Update.DELETE 135 | update.entity.table_entry.CopyFrom(table_entry) 136 | if dry_run: 137 | print("P4Runtime Write:", request) 138 | else: 139 | self.client_stub.Write(request) 140 | 141 | def ReadTableEntries(self, table_id=None, dry_run=False): 142 | request = p4runtime_pb2.ReadRequest() 143 | request.device_id = self.device_id 144 | entity = request.entities.add() 145 | table_entry = entity.table_entry 146 | if table_id is not None: 147 | table_entry.table_id = table_id 148 | else: 149 | table_entry.table_id = 0 150 | if dry_run: 151 | print("P4Runtime Read:", request) 152 | else: 153 | for response in self.client_stub.Read(request): 154 | yield response 155 | 156 | def ReadCounters(self, counter_id=None, index=None, dry_run=False): 157 | request = p4runtime_pb2.ReadRequest() 158 | request.device_id = self.device_id 159 | entity = request.entities.add() 160 | counter_entry = entity.counter_entry 161 | if counter_id is not None: 162 | counter_entry.counter_id = counter_id 163 | else: 164 | counter_entry.counter_id = 0 165 | if index is not None: 166 | counter_entry.index.index = index 167 | if dry_run: 168 | print("P4Runtime Read:", request) 169 | else: 170 | for response in self.client_stub.Read(request): 171 | yield response 172 | 173 | def PacketIn(self, dry_run=False, **kwargs): 174 | for item in self.stream_msg_resp: 175 | if dry_run: 176 | print("P4 Runtime PacketIn: ", request) 177 | else: 178 | return item 179 | 180 | def PacketOut(self, packet, dry_run=False, **kwargs): 181 | request = p4runtime_pb2.StreamMessageRequest() 182 | request.packet.CopyFrom(packet) 183 | if dry_run: 184 | print("P4 Runtime: ", request) 185 | else: 186 | self.requests_stream.put(request) 187 | # for item in self.stream_msg_resp: 188 | return request 189 | 190 | def WritePREEntry(self, pre_entry, dry_run=False): 191 | request = p4runtime_pb2.WriteRequest() 192 | request.device_id = self.device_id 193 | request.election_id.low = 1 194 | update = request.updates.add() 195 | update.type = p4runtime_pb2.Update.INSERT 196 | update.entity.packet_replication_engine_entry.CopyFrom(pre_entry) 197 | if dry_run: 198 | print("P4Runtime Write:", request) 199 | else: 200 | self.client_stub.Write(request) 201 | 202 | 203 | class GrpcRequestLogger( 204 | grpc.UnaryUnaryClientInterceptor, grpc.UnaryStreamClientInterceptor 205 | ): 206 | """Implementation of a gRPC interceptor that logs request to a file""" 207 | 208 | def __init__(self, log_file): 209 | self.log_file = log_file 210 | with open(self.log_file, "w") as f: 211 | # Clear content if it exists. 212 | f.write("") 213 | 214 | def log_message(self, method_name, body): 215 | with open(self.log_file, "a") as f: 216 | ts = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] 217 | msg = str(body) 218 | f.write("\n[%s] %s\n---\n" % (ts, method_name)) 219 | if len(msg) < MSG_LOG_MAX_LEN: 220 | f.write(str(body)) 221 | else: 222 | f.write("Message too long (%d bytes)! Skipping log...\n" % len(msg)) 223 | f.write("---\n") 224 | 225 | def intercept_unary_unary(self, continuation, client_call_details, request): 226 | self.log_message(client_call_details.method, request) 227 | return continuation(client_call_details, request) 228 | 229 | def intercept_unary_stream(self, continuation, client_call_details, request): 230 | self.log_message(client_call_details.method, request) 231 | return continuation(client_call_details, request) 232 | 233 | 234 | class IterableQueue(Queue): 235 | _sentinel = object() 236 | 237 | def __iter__(self): 238 | return iter(self.get, self._sentinel) 239 | 240 | def close(self): 241 | self.put(self._sentinel) 242 | --------------------------------------------------------------------------------