├── shared ├── .gitkeep ├── lesson02 │ ├── reset_vtep_db │ ├── get_locator_uuid.json │ ├── server1_topology.py │ ├── server2_topology.py │ ├── common.py │ ├── server1_flows.txt │ ├── attach_blue2.json │ ├── attach_red2.json │ └── ovs-vtep ├── lesson01 │ ├── _red2_.txt │ ├── CREDITS │ ├── server1_topology.py │ ├── server2_topology.py │ ├── common.py │ ├── server1_flows.txt │ ├── server2_flows.txt │ └── commands.txt ├── lesson03 │ ├── reset_todo_db.sh │ ├── chat.ovsschema │ ├── reset_chat_db.sh │ ├── stamps │ │ ├── templates │ │ │ ├── view.html │ │ │ └── index.html │ │ ├── reset_stamps_db.sh │ │ ├── stamps.ovsschema │ │ ├── main.py │ │ └── seed_stamps.py │ ├── sender.py │ ├── todo.ovsschema │ ├── receiver.py │ └── ovsdb_client.py └── lesson04 │ └── start_guests ├── puppet └── manifests │ ├── site.pp │ ├── nodes │ ├── internet.pp │ ├── server1.pp │ └── server2.pp │ ├── nodes.pp │ └── nodes.pp.build_from_source ├── .gitignore ├── LICENSE ├── README.txt └── Vagrantfile /shared/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /puppet/manifests/site.pp: -------------------------------------------------------------------------------- 1 | import 'nodes.pp' 2 | -------------------------------------------------------------------------------- /puppet/manifests/nodes/internet.pp: -------------------------------------------------------------------------------- 1 | node internet inherits basenode {} 2 | -------------------------------------------------------------------------------- /puppet/manifests/nodes/server1.pp: -------------------------------------------------------------------------------- 1 | node server1 inherits servernode {} 2 | -------------------------------------------------------------------------------- /puppet/manifests/nodes/server2.pp: -------------------------------------------------------------------------------- 1 | node server2 inherits servernode {} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | .DS_Store 3 | shared/ovs.zip 4 | shared/ovs/*.* 5 | *.pyc 6 | -------------------------------------------------------------------------------- /shared/lesson02/reset_vtep_db: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ovs-appctl -t ovs-vswitchd exit 4 | ovs-appctl -t ovsdb-server exit 5 | rm /etc/openvswitch/vtep.db 6 | /etc/init.d/openvswitch-switch start 7 | -------------------------------------------------------------------------------- /shared/lesson01/_red2_.txt: -------------------------------------------------------------------------------- 1 | 9147 2 | tcpdump: verbose output suppressed, use -v or -vv for full protocol decode 3 | listening on red2-eth0, link-type EN10MB (Ethernet), capture size 65535 bytes 4 | -------------------------------------------------------------------------------- /shared/lesson01/CREDITS: -------------------------------------------------------------------------------- 1 | This lesson is adapted from David Mahler's introductory video 2 | titled "VXLAN overlay networks with Open vSwitch" which is 3 | available at https://www.youtube.com/watch?v=tnSkHhsLqpM 4 | -------------------------------------------------------------------------------- /shared/lesson02/get_locator_uuid.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "transact", 3 | "params": [ 4 | "hardware_vtep", 5 | { 6 | "op": "select", 7 | "table": "Physical_Locator", 8 | "columns": ["_uuid"], 9 | "where": [ 10 | ["dst_ip", "==", "192.168.1.10"] 11 | ] 12 | } 13 | ], 14 | "id": 11 15 | } 16 | -------------------------------------------------------------------------------- /shared/lesson03/reset_todo_db.sh: -------------------------------------------------------------------------------- 1 | ovs-appctl -t ovsdb-server exit 2 | 3 | if [ ! -d "/etc/lesson03" ]; then 4 | mkdir /etc/lesson03 5 | else 6 | rm -f /etc/lesson03/todo.db 7 | fi 8 | 9 | ovsdb-tool create /etc/lesson03/todo.db /vagrant/shared/lesson03/todo.ovsschema 10 | ovsdb-server --pidfile --detach --log-file --remote punix:/var/run/openvswitch/db.sock /etc/lesson03/todo.db 11 | ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640 12 | -------------------------------------------------------------------------------- /shared/lesson03/chat.ovsschema: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "version": "1.0.0", 4 | "cksum": "1234567 891011", 5 | "tables": { 6 | "Message": { 7 | "columns": { 8 | "sender": { 9 | "type": "string" 10 | }, 11 | "message": { 12 | "type": "string" 13 | } 14 | }, 15 | "isRoot": true 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /shared/lesson03/reset_chat_db.sh: -------------------------------------------------------------------------------- 1 | ovs-appctl -t ovsdb-server exit 2 | 3 | if [ ! -d "/etc/lesson03" ]; then 4 | mkdir /etc/lesson03 5 | else 6 | rm -f /etc/lesson03/chat.db 7 | fi 8 | 9 | ovsdb-tool create /etc/lesson03/chat.db /vagrant/shared/lesson03/chat.ovsschema 10 | ovsdb-server --pidfile --detach --log-file --remote punix:/var/run/openvswitch/db.sock /etc/lesson03/chat.db 11 | ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640 12 | ovs-appctl -t ovsdb-server vlog/set dbg 13 | -------------------------------------------------------------------------------- /shared/lesson01/server1_topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from common import createTopology 4 | 5 | 6 | createTopology( 7 | 's1', 8 | [ 9 | [ 10 | 'red1', 11 | { 12 | 'ip': '10.0.0.1/8', 13 | 'mac': '00:00:00:00:aa:01' 14 | } 15 | ], 16 | [ 17 | 'blue1', 18 | { 19 | 'ip': '10.0.0.1/8', 20 | 'mac': '00:00:00:00:aa:01' 21 | } 22 | ] 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /shared/lesson01/server2_topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from common import createTopology 4 | 5 | 6 | createTopology( 7 | 's2', 8 | [ 9 | [ 10 | 'red2', 11 | { 12 | 'ip': '10.0.0.2/8', 13 | 'mac': '00:00:00:00:aa:02' 14 | } 15 | ], 16 | [ 17 | 'blue2', 18 | { 19 | 'ip': '10.0.0.2/8', 20 | 'mac': '00:00:00:00:aa:02' 21 | } 22 | ] 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /shared/lesson02/server1_topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from common import createTopology 4 | 5 | 6 | createTopology( 7 | 's1', 8 | [ 9 | [ 10 | 'red1', 11 | { 12 | 'ip': '10.0.0.1/8', 13 | 'mac': '00:00:00:00:aa:01' 14 | } 15 | ], 16 | [ 17 | 'blue1', 18 | { 19 | 'ip': '10.0.0.1/8', 20 | 'mac': '00:00:00:00:aa:01' 21 | } 22 | ] 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /shared/lesson02/server2_topology.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from common import createTopology 4 | 5 | 6 | createTopology( 7 | 's2', 8 | [ 9 | [ 10 | 'red2', 11 | { 12 | 'ip': '10.0.0.2/8', 13 | 'mac': '00:00:00:00:aa:02' 14 | } 15 | ], 16 | [ 17 | 'blue2', 18 | { 19 | 'ip': '10.0.0.2/8', 20 | 'mac': '00:00:00:00:aa:02' 21 | } 22 | ] 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/templates/view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open vStamps 5 | 6 | 7 |

OVSDB: Open vStamps Database

8 |
9 |
{{ stamp['name'] }}
10 | 11 |
{{ stamp['url'] }}
12 |
13 | Index 14 |
15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/reset_stamps_db.sh: -------------------------------------------------------------------------------- 1 | ovs-appctl -t ovsdb-server exit 2 | 3 | if [ ! -d "/etc/lesson03" ]; then 4 | mkdir /etc/lesson03 5 | else 6 | rm -f /etc/lesson03/stamps.db 7 | fi 8 | 9 | ovsdb-tool create /etc/lesson03/stamps.db /vagrant/shared/lesson03/stamps/stamps.ovsschema 10 | ovsdb-server --pidfile --detach --log-file --remote punix:/var/run/openvswitch/db.sock /etc/lesson03/stamps.db 11 | ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640 12 | ovs-appctl -t ovsdb-server vlog/set dbg 13 | python seed_stamps.py 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 Mark S. Maglana 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 | -------------------------------------------------------------------------------- /shared/lesson01/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from mininet.cli import CLI 4 | from mininet.net import Mininet 5 | from mininet.log import setLogLevel 6 | from mininet.topo import Topo 7 | from mininet.util import dumpNodeConnections 8 | 9 | 10 | def createTopology(switch, hosts): 11 | setLogLevel('info') 12 | topo = Topo() 13 | switch = topo.addSwitch(switch) 14 | 15 | for (hostname, opts) in hosts: 16 | host = topo.addHost(hostname, **opts) 17 | topo.addLink(host, switch) 18 | 19 | network = Mininet(topo, controller=None) 20 | network.start() 21 | print "*** Dumping host connections" 22 | dumpNodeConnections(network.hosts) 23 | CLI(network) 24 | network.stop() 25 | -------------------------------------------------------------------------------- /shared/lesson02/common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from mininet.cli import CLI 4 | from mininet.net import Mininet 5 | from mininet.log import setLogLevel 6 | from mininet.topo import Topo 7 | from mininet.util import dumpNodeConnections 8 | 9 | 10 | def createTopology(switch, hosts): 11 | setLogLevel('info') 12 | topo = Topo() 13 | switch = topo.addSwitch(switch) 14 | 15 | for (hostname, opts) in hosts: 16 | host = topo.addHost(hostname, **opts) 17 | topo.addLink(host, switch, None) 18 | 19 | network = Mininet(topo, controller=None) 20 | network.start() 21 | print "*** Dumping host connections" 22 | dumpNodeConnections(network.hosts) 23 | CLI(network) 24 | network.stop() 25 | -------------------------------------------------------------------------------- /shared/lesson01/server1_flows.txt: -------------------------------------------------------------------------------- 1 | table=0,in_port=1,actions=set_field:100->tun_id,resubmit(,1) 2 | table=0,in_port=2,actions=set_field:200->tun_id,resubmit(,1) 3 | table=0,actions=resubmit(,1) 4 | 5 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:01,actions=output:1 6 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:01,actions=output:2 7 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:02,actions=output:10 8 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:02,actions=output:10 9 | 10 | table=1,tun_id=100,arp,nw_dst=10.0.0.1,actions=output:1 11 | table=1,tun_id=200,arp,nw_dst=10.0.0.1,actions=output:2 12 | table=1,tun_id=100,arp,nw_dst=10.0.0.2,actions=output:10 13 | table=1,tun_id=200,arp,nw_dst=10.0.0.2,actions=output:10 14 | table=1,priority=100,actions=drop 15 | -------------------------------------------------------------------------------- /shared/lesson01/server2_flows.txt: -------------------------------------------------------------------------------- 1 | table=0,in_port=1,actions=set_field:100->tun_id,resubmit(,1) 2 | table=0,in_port=2,actions=set_field:200->tun_id,resubmit(,1) 3 | table=0,actions=resubmit(,1) 4 | 5 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:02,actions=output:1 6 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:02,actions=output:2 7 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:01,actions=output:10 8 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:01,actions=output:10 9 | 10 | table=1,tun_id=100,arp,nw_dst=10.0.0.2,actions=output:1 11 | table=1,tun_id=200,arp,nw_dst=10.0.0.2,actions=output:2 12 | table=1,tun_id=100,arp,nw_dst=10.0.0.1,actions=output:10 13 | table=1,tun_id=200,arp,nw_dst=10.0.0.1,actions=output:10 14 | table=1,priority=100,actions=drop 15 | -------------------------------------------------------------------------------- /shared/lesson02/server1_flows.txt: -------------------------------------------------------------------------------- 1 | table=0,in_port=1,actions=set_field:100->tun_id,resubmit(,1) 2 | table=0,in_port=2,actions=set_field:200->tun_id,resubmit(,1) 3 | table=0,actions=resubmit(,1) 4 | 5 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:01,actions=output:1 6 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:01,actions=output:2 7 | table=1,tun_id=100,dl_dst=00:00:00:00:aa:02,actions=output:10 8 | table=1,tun_id=200,dl_dst=00:00:00:00:aa:02,actions=output:10 9 | 10 | table=1,tun_id=100,arp,nw_dst=10.0.0.1,actions=output:1 11 | table=1,tun_id=200,arp,nw_dst=10.0.0.1,actions=output:2 12 | table=1,tun_id=100,arp,nw_dst=10.0.0.2,actions=output:10 13 | table=1,tun_id=200,arp,nw_dst=10.0.0.2,actions=output:10 14 | table=1,priority=100,actions=drop 15 | -------------------------------------------------------------------------------- /shared/lesson03/sender.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ovsdb_client import OVSDBClient 3 | 4 | nickname = sys.argv[2] 5 | message_id = 0 6 | 7 | while True: 8 | message = raw_input('Your message: ') 9 | 10 | client = OVSDBClient(sys.argv[1], 6640) 11 | message_id += 1 12 | 13 | response = client.request({ 14 | 'id': message_id, 15 | 'method': 'transact', 16 | 'params': [ 17 | 'chat', 18 | { 19 | 'op': 'insert', 20 | 'table': 'Message', 21 | 'row': { 22 | 'sender': nickname, 23 | 'message': message 24 | } 25 | } 26 | ] 27 | }) 28 | 29 | if response and response['error']: 30 | raise "Could not send message!" 31 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/stamps.ovsschema: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stamps", 3 | "version": "1.0.0", 4 | "cksum": "1234567 891011", 5 | "tables": { 6 | "Stamp": { 7 | "columns": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "url": { 12 | "type": "string" 13 | }, 14 | "categories": { 15 | "type": { 16 | "max": "unlimited", 17 | "min": 0, 18 | "key": { 19 | "type": "string" 20 | } 21 | } 22 | } 23 | }, 24 | "isRoot": true, 25 | "indexes": [ 26 | ["name"] 27 | ] 28 | } 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /shared/lesson03/todo.ovsschema: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo", 3 | "version": "1.0.0", 4 | "cksum": "1234567 891011", 5 | "tables": { 6 | "List": { 7 | "columns": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "items": { 12 | "type": { 13 | "key": { 14 | "type": "uuid", 15 | "refTable": "Item" 16 | }, 17 | "min": 0, 18 | "max": "unlimited" 19 | } 20 | } 21 | }, 22 | "isRoot": true, 23 | "indexes": [ 24 | ["name"] 25 | ] 26 | }, 27 | "Item": { 28 | "columns": { 29 | "status": { 30 | "type": "string" 31 | }, 32 | "description": { 33 | "type": "string" 34 | } 35 | } 36 | } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Open vStamps 5 | 6 | 7 |

OVSDB: Open vStamps Database

8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% for stamp in stamps %} 16 | 17 | 18 | 19 | 30 | 31 | 32 | {% endfor %} 33 |
ThumbnailSourceCategoriesActions
{{ stamp['url'][0:50] }}... 20 |
    21 | {% if stamp['categories'][0] == 'set' %} 22 | {% for category in stamp['categories'][1] %} 23 |
  • {{ category }}
  • 24 | {% endfor %} 25 | {% else %} 26 |
  • {{ stamp['categories'] }}
  • 27 | {% endif %} 28 |
29 |
View
34 | 35 | 36 | -------------------------------------------------------------------------------- /shared/lesson01/commands.txt: -------------------------------------------------------------------------------- 1 | RUN IN SERVER 1 2 | =============== 3 | 4 | sudo su - 5 | 6 | cd /vagrant/shared/lesson01 7 | 8 | ./server1_topology.py 9 | 10 | sh ovs-vsctl add-port s1 vtep -- set interface vtep type=vxlan option:remote_ip=192.168.2.20 option:key=flow ofport_request=10 11 | 12 | sh ovs-vsctl show 13 | 14 | sh ovs-ofctl show s1 15 | 16 | sh ovs-ofctl add-flows s1 server1_flows.txt 17 | 18 | 19 | 20 | RUN IN SERVER 2 21 | =============== 22 | 23 | sudo su - 24 | 25 | cd /vagrant/shared/lesson01 26 | 27 | ./server2_topology.py 28 | 29 | sh ovs-vsctl add-port s2 vtep -- set interface vtep type=vxlan option:remote_ip=192.168.1.10 option:key=flow ofport_request=10 30 | 31 | sh ovs-vsctl show 32 | 33 | sh ovs-ofctl show s2 34 | 35 | sh ovs-ofctl add-flows s2 server2_flows.txt 36 | 37 | 38 | 39 | RUN IN SERVER 1 40 | =============== 41 | 42 | red1 ping 10.0.0.2 43 | 44 | 45 | 46 | RUN IN SERVER 2 47 | =============== 48 | 49 | red2 ifconfig red2-eth0 down 50 | 51 | (red1 should stop receiving ping replies at this point) 52 | 53 | red2 ifconfig red2-eth0 up 54 | 55 | (red1 should resume receiving ping replies) 56 | -------------------------------------------------------------------------------- /shared/lesson03/receiver.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from ovsdb_client import OVSDBClient 3 | 4 | client = OVSDBClient(sys.argv[1], 6640) 5 | 6 | print "Connecting to chat room..." 7 | 8 | response = client.request({ 9 | 'id': 1, 10 | 'method': 'monitor', 11 | 'params': [ 12 | 'chat', 13 | 1, 14 | { 15 | 'Message': {} 16 | } 17 | ] 18 | }) 19 | 20 | if response['error']: 21 | raise "Could not connect to chat room!" 22 | else: 23 | print "Connected." 24 | 25 | while True: 26 | # Check for messages from server 27 | message = None 28 | try: 29 | message = client.receive() 30 | except OVSDBClient.ReceiveTimeoutError: 31 | pass 32 | 33 | # Skip the rest if no message received 34 | if not message: 35 | continue 36 | 37 | # Respond to inactivity probe from OVSDB 38 | if message['method'] == 'echo': 39 | print "(OVSDB) PING?" 40 | client.send({ 41 | 'id': message['id'], 42 | 'error': None, 43 | 'result': message['params'] 44 | }) 45 | print "PONG!" 46 | 47 | # Extract message from update notification 48 | if message['method'] == 'update': 49 | row = message['params'][1]['Message'].values()[0]['new'] 50 | print "({}) {}".format(row['sender'], row['message']) 51 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | import sys 5 | 6 | from flask import Flask, render_template, request 7 | 8 | sys.path.append(os.path.abspath(os.path.join(__file__, '../..'))) 9 | 10 | from ovsdb_client import OVSDBClient 11 | 12 | app = Flask(__name__) 13 | 14 | 15 | @app.route('/') 16 | def index(): 17 | client = OVSDBClient('localhost', 6640) 18 | 19 | query = { 20 | 'id': 1, 21 | 'method': 'transact', 22 | 'params': [ 23 | 'stamps', 24 | { 25 | 'op': 'select', 26 | 'table': 'Stamp', 27 | 'where': [] 28 | } 29 | ] 30 | } 31 | 32 | response = client.request(query) 33 | return render_template('index.html', stamps=response['result'][0]['rows']) 34 | 35 | 36 | @app.route('/view') 37 | def view(): 38 | client = OVSDBClient('localhost', 6640) 39 | 40 | query = { 41 | 'id': 2, 42 | 'method': 'transact', 43 | 'params': [ 44 | 'stamps', 45 | { 46 | 'op': 'select', 47 | 'table': 'Stamp', 48 | 'where': [ 49 | ['_uuid', '==', ['uuid', request.args.get('id')]] 50 | ] 51 | } 52 | ] 53 | } 54 | 55 | response = client.request(query) 56 | return render_template('view.html', stamp=response['result'][0]['rows'][0]) 57 | 58 | 59 | if __name__ == "__main__": 60 | app.debug = True 61 | app.run(host='192.168.101.10', port=8080) 62 | -------------------------------------------------------------------------------- /shared/lesson02/attach_blue2.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "transact", 3 | "params": [ 4 | "hardware_vtep", 5 | { 6 | "op": "insert", 7 | "table": "Logical_Switch", 8 | "row": { 9 | "name": "blue-network", 10 | "tunnel_key": 200 11 | }, 12 | "uuid-name": "BLUENETWORK" 13 | }, 14 | { 15 | "op": "insert", 16 | "table": "Ucast_Macs_Remote", 17 | "row": { 18 | "logical_switch": ["named-uuid", "BLUENETWORK"], 19 | "MAC": "00:00:00:00:aa:01", 20 | "locator": [ 21 | "set", 22 | [ 23 | ["uuid", "***REPLACE WITH UUID OF 192.168.1.10***"] 24 | ] 25 | ] 26 | }, 27 | "uuid-name": "BLUE1" 28 | }, 29 | { 30 | "op": "mutate", 31 | "table": "Physical_Port", 32 | "where": [ 33 | ["name", "==", "s2-eth2"] 34 | ], 35 | "mutations": [ 36 | [ 37 | "vlan_bindings", 38 | "delete", 39 | [ 40 | "set", 41 | [ 42 | 0 43 | ] 44 | ] 45 | ], 46 | [ 47 | "vlan_bindings", 48 | "insert", 49 | [ 50 | "map", 51 | [ 52 | [ 53 | 0, 54 | ["named-uuid", "BLUENETWORK"] 55 | ] 56 | ] 57 | ] 58 | ] 59 | ] 60 | } 61 | ], 62 | "id": 10 63 | } 64 | -------------------------------------------------------------------------------- /shared/lesson03/stamps/seed_stamps.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | sys.path.append(os.path.abspath(os.path.join(__file__, '../..'))) 5 | 6 | from ovsdb_client import OVSDBClient 7 | 8 | stamps = [ 9 | { 10 | 'name': 'Forever', 11 | 'url': 'http://www.spellman.org/images/forever_stamp.jpg', 12 | 'categories': [ 13 | 'historic' 14 | ] 15 | }, 16 | 17 | { 18 | 'name': 'Aborigine', 19 | 'url': 'http://www.smh.com.au/ffxImage/' 20 | 'urlpicture_id_1023864658551_2002/06/28/nat_stamp29.jpg', 21 | 'categories': [ 22 | 'historic', 23 | 'australia' 24 | ] 25 | }, 26 | 27 | { 28 | 'name': 'Amur Tiger Cub', 29 | 'url': 'http://goodnature.nathab.com/' 30 | 'wp-content/uploads/2011/10/Tiger-face1.png', 31 | 'categories': [ 32 | 'animals' 33 | ] 34 | }, 35 | 36 | { 37 | 'name': 'Spiderman', 38 | 'url': 'http://a.abcnews.go.com/images/Entertainment/' 39 | 'ap_comicstamp_01_ssv.jpg', 40 | 'categories': [ 41 | 'cartoons' 42 | ] 43 | }, 44 | 45 | { 46 | 'name': 'Homer Simpson', 47 | 'url': 'http://s08214.biss.wikispaces.net/file/' 48 | 'view/homer-stamp-14.jpg/125737775/homer-stamp-14.jpg', 49 | 'categories': [ 50 | 'cartoons' 51 | ] 52 | } 53 | ] 54 | 55 | operations = ['stamps'] 56 | 57 | for stamp in stamps: 58 | operations.append({ 59 | 'op': 'insert', 60 | 'table': 'Stamp', 61 | 'row': { 62 | 'name': stamp['name'], 63 | 'url': stamp['url'], 64 | 'categories': [ 65 | 'set', 66 | stamp['categories'] 67 | ] 68 | } 69 | }) 70 | 71 | client = OVSDBClient('localhost', 6640) 72 | 73 | response = client.request({ 74 | 'id': 1, 75 | 'method': 'transact', 76 | 'params': operations 77 | }) 78 | 79 | 80 | if response['error'] is None: 81 | print "Stamps database seeded" 82 | else: 83 | print response 84 | -------------------------------------------------------------------------------- /shared/lesson02/attach_red2.json: -------------------------------------------------------------------------------- 1 | { 2 | "method": "transact", 3 | "params": [ 4 | "hardware_vtep", 5 | { 6 | "op": "insert", 7 | "table": "Logical_Switch", 8 | "row": { 9 | "name": "red-network", 10 | "tunnel_key": 100 11 | }, 12 | "uuid-name": "REDNETWORK" 13 | }, 14 | { 15 | "op": "insert", 16 | "table": "Physical_Locator", 17 | "row": { 18 | "dst_ip": "192.168.1.10", 19 | "encapsulation_type": "vxlan_over_ipv4" 20 | }, 21 | "uuid-name": "SERVER1" 22 | }, 23 | { 24 | "op": "insert", 25 | "table": "Ucast_Macs_Remote", 26 | "row": { 27 | "logical_switch": ["named-uuid", "REDNETWORK"], 28 | "MAC": "00:00:00:00:aa:01", 29 | "locator": [ 30 | "set", 31 | [ 32 | ["named-uuid", "SERVER1"] 33 | ] 34 | ] 35 | }, 36 | "uuid-name": "RED1" 37 | }, 38 | { 39 | "op": "mutate", 40 | "table": "Physical_Port", 41 | "where": [ 42 | ["name", "==", "s2-eth1"] 43 | ], 44 | "mutations": [ 45 | [ 46 | "vlan_bindings", 47 | "delete", 48 | [ 49 | "set", 50 | [ 51 | 0 52 | ] 53 | ] 54 | ], 55 | [ 56 | "vlan_bindings", 57 | "insert", 58 | [ 59 | "map", 60 | [ 61 | [ 62 | 0, 63 | ["named-uuid", "REDNETWORK"] 64 | ] 65 | ] 66 | ] 67 | ] 68 | ] 69 | } 70 | ], 71 | "id": 10 72 | } 73 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Open vSwitch Lab 2 | ================ 3 | 4 | This is a companion project to the Open vSwitch series of articles at 5 | http://www.relaxdiego.com. 6 | 7 | 8 | Requirements 9 | ------------ 10 | 11 | 1. VirtualBox. Install from https://www.virtualbox.org/wiki/Downloads 12 | 13 | 2. Vagrant v1.6.3 or above. Install from http://www.vagrantup.com/downloads.html 14 | 15 | 3. The vagrant-vbguest plugin. Install with `vagrant plugin install vagrant-vbguest` 16 | 17 | 18 | Installation 19 | ------------ 20 | 21 | Estimated time for the following steps including automated provisioning: 22 | 30 minutes. Time will vary depending on your connection speed. 23 | 24 | 1. Clone this repo and cd to it 25 | 26 | 2. Run `vagrant up` 27 | 28 | 29 | Whoa! What Just Happened??? 30 | --------------------------- 31 | 32 | You just created two servers that can talk to each other over the "Internet!" 33 | 34 | +-----------+ +-----------+ 35 | | |192.168.1.10/24 +------------+ 192.168.2.20/24| | 36 | | server1 |-------------------| "INTERNET" |-------------------| server2 | 37 | | | +------------+ | | 38 | +-----------+ +-----------+ 39 | 40 | The two servers are Ubuntu VMs with mininet and OVS 2.x installed while the 41 | "Internet" is really a VM with IP forwarding enabled. All networks above are 42 | implemented as VirtualBox internal networks which are not directly accessible 43 | from anywhere else including your local machine. However, you can SSH to any 44 | of the three VMs using `vagrant ssh`. For example: `vagrant ssh internet`, or 45 | `vagrant ssh server1` and then get to the internal networks from there. 46 | 47 | 48 | Stopping and starting the VMs 49 | ----------------------------- 50 | 51 | To suspend your VMs, run `vagrant suspend`. To shut them down, run 52 | `vagrant halt`. 53 | 54 | 55 | Viewing VM status 56 | ----------------- 57 | 58 | `vagrant status` 59 | 60 | 61 | Troubleshooting 62 | --------------- 63 | 64 | PROBLEM: I'm getting an error about Vagrant not able to compile nokogiri 65 | 66 | SOLUTION: You're missing developer tools needed to locally compile binaries 67 | needed by nokogiri. In OS X, install the OS X Developer Tools for your OS X 68 | version. 69 | -------------------------------------------------------------------------------- /puppet/manifests/nodes.pp: -------------------------------------------------------------------------------- 1 | node basenode { 2 | 3 | exec { "apt_get_update": 4 | command => "/usr/bin/apt-get -y update" 5 | } 6 | 7 | # Ensure the above command runs before we attempt 8 | # to install any package in other manifests 9 | Exec["apt_get_update"] -> Package<| |> 10 | 11 | package { "unzip": 12 | ensure => installed, 13 | } 14 | 15 | exec { "download_ovs": 16 | command => "/usr/bin/wget https://www.dropbox.com/sh/k5t492id6hc6ynf/AAA2GzeOowsf7Kdjv6eC5Yfca?dl=1 -O /vagrant/shared/ovs.zip", 17 | cwd => "/root", 18 | creates => "/vagrant/shared/ovs.zip", 19 | } 20 | 21 | exec { "extract_ovs": 22 | command => "/usr/bin/unzip /vagrant/shared/ovs.zip -d /vagrant/shared/ovs", 23 | cwd => "/vagrant/shared", 24 | require => [ 25 | Package["unzip"], 26 | Exec["download_ovs"], 27 | ], 28 | returns => [0, 2], 29 | creates => "/vagrant/shared/ovs/openvswitch-common_2.3.0-1_amd64.deb", 30 | } 31 | 32 | package { "ovs_common": 33 | name => "openvswitch-common", 34 | ensure => installed, 35 | provider => dpkg, 36 | source => "/vagrant/shared/ovs/openvswitch-common_2.3.0-1_amd64.deb", 37 | require => [ 38 | Exec["extract_ovs"], 39 | ] 40 | } 41 | 42 | package { "ovs_switch": 43 | name => "openvswitch-switch", 44 | ensure => installed, 45 | provider => dpkg, 46 | source => "/vagrant/shared/ovs/openvswitch-switch_2.3.0-1_amd64.deb", 47 | require => [ 48 | Package["ovs_common"], 49 | ], 50 | } 51 | 52 | } 53 | 54 | 55 | 56 | node servernode inherits basenode { 57 | 58 | package { "ovs_python": 59 | name => "python-openvswitch", 60 | ensure => installed, 61 | provider => dpkg, 62 | source => "/vagrant/shared/ovs/python-openvswitch_2.3.0-1_all.deb", 63 | require => [ 64 | Package["ovs_common"], 65 | ], 66 | } 67 | 68 | package { "ovs_vtep": 69 | name => "openvswitch-vtep", 70 | ensure => installed, 71 | provider => dpkg, 72 | source => "/vagrant/shared/ovs/openvswitch-vtep_2.3.0-1_amd64.deb", 73 | require => [ 74 | Package["ovs_python"], 75 | ], 76 | } 77 | 78 | package { "mininet": 79 | ensure => installed, 80 | require => [ 81 | Package["ovs_switch"], 82 | ] 83 | } 84 | 85 | } 86 | 87 | 88 | 89 | import 'nodes/*.pp' 90 | -------------------------------------------------------------------------------- /shared/lesson04/start_guests: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import argparse 4 | import json 5 | from mininet.cli import CLI 6 | from mininet.net import Mininet 7 | from mininet.log import setLogLevel 8 | from mininet.topo import Topo 9 | from mininet.util import dumpNodeConnections 10 | import uuid 11 | 12 | 13 | def initialize(switch_id, 14 | number_of_guests=10): 15 | 16 | setLogLevel('info') 17 | 18 | # Build the topology 19 | topo = Topo() 20 | switch_name = "br{}".format(switch_id) 21 | switch = topo.addSwitch(switch_name) 22 | 23 | for number in range(1, number_of_guests + 1): 24 | guest_name = "g{}".format(number, switch_id) 25 | ip = "192.168.{}.{}/24".format(number, switch_id) 26 | mac = "00:00:00:00:{:02x}:{:02x}".format(number, switch_id) 27 | host = topo.addHost(name=guest_name, ip=ip, mac=mac) 28 | topo.addLink(host, switch, None) 29 | 30 | # Instantiate the network using above topology 31 | network = Mininet(topo, controller=None) 32 | network.start() 33 | 34 | # Configure the switch with settings required by NSX 35 | switch = network.switches[0] 36 | switch.cmd("ovs-vsctl br-set-external-id " 37 | "{0} bridge-id {0}".format(switch_name)) 38 | switch.cmd("ovs-vsctl set Bridge {0} " 39 | "fail-mode=standalone".format(switch_name)) 40 | 41 | # Set the external ids of each bridge port so that NSX 42 | # will recognize them as virtual interfaces 43 | for host in network.hosts: 44 | br_intf_name = host.connectionsTo(switch)[0][1].name 45 | ops = json.dumps([ 46 | 'Open_vSwitch', 47 | { 48 | 'op': 'select', 49 | 'table': 'Interface', 50 | 'where': [['name', '==', br_intf_name]], 51 | 'columns': ['_uuid'] 52 | } 53 | ]) 54 | response = switch.cmd("ovsdb-client transact '{}'".format(ops)) 55 | br_intf_uuid = json.loads(response)[0]['rows'][0]['_uuid'][1] 56 | 57 | external_ids = { 58 | 'attached-mac': host.MAC(), 59 | 'iface-id': uuid.uuid4(), 60 | 'iface-status': 'active', 61 | 'vm-id': uuid.uuid4() 62 | } 63 | 64 | for key, value in external_ids.items(): 65 | switch.cmd("ovs-vsctl set Interface {} external_ids:{}={}" 66 | .format(br_intf_uuid, key, value) 67 | ) 68 | 69 | print "*** Host connections:" 70 | dumpNodeConnections(network.hosts) 71 | CLI(network) 72 | network.stop() 73 | 74 | if __name__ == "__main__": 75 | parser = argparse.ArgumentParser( 76 | description='Creates a mininet topology of 1 bridge and X guests') 77 | 78 | parser.add_argument('-s', '--switch-id', required=True, 79 | dest='switch_id', metavar='NUMBER', action='store', 80 | help='A numeric identifier for the switch') 81 | parser.add_argument('-g', '--guests', required=False, 82 | dest='number_of_guests', metavar='NUMBER', 83 | action='store', default=10, 84 | help='The number of guests to spawn. Default is 10') 85 | 86 | args = parser.parse_args() 87 | 88 | initialize(switch_id=int(args.switch_id), 89 | number_of_guests=int(args.number_of_guests)) 90 | -------------------------------------------------------------------------------- /puppet/manifests/nodes.pp.build_from_source: -------------------------------------------------------------------------------- 1 | node basenode { 2 | 3 | exec { "apt_get_update": 4 | command => "/usr/bin/apt-get -y update" 5 | } 6 | 7 | # Ensure the above command runs before we attempt 8 | # to install any package in other manifests 9 | Exec["apt_get_update"] -> Package<| |> 10 | 11 | $base_packages = [ 12 | "git", 13 | "autoconf", 14 | "automake1.10", 15 | "build-essential", 16 | "debhelper", 17 | "fakeroot", 18 | "libffi-dev", 19 | "libssl-dev", 20 | "libtool", 21 | "pkg-config", 22 | "python-all", 23 | "python-qt4", 24 | "python-zopeinterface", 25 | "python-twisted-conch" 26 | ] 27 | 28 | package { $base_packages: 29 | ensure => installed, 30 | } 31 | 32 | exec { "download_ovs": 33 | command => "/usr/bin/wget https://github.com/openvswitch/ovs/archive/v2.3.tar.gz -O /root/ovs.tar.gz", 34 | cwd => "/root", 35 | creates => "/root/ovs.tar.gz", 36 | } 37 | 38 | file { "/root/ovs": 39 | ensure => directory, 40 | } 41 | 42 | exec { "extract_ovs": 43 | command => "/bin/tar xvfz ovs.tar.gz -C /root/ovs --strip-components=1", 44 | cwd => "/root", 45 | require => [ 46 | Exec["download_ovs"], 47 | ], 48 | creates => "/root/ovs/README", 49 | } 50 | 51 | exec { "build_ovs": 52 | command => "/usr/bin/fakeroot debian/rules binary", 53 | environment => "DEB_BUILD_OPTIONS='parallel=8 nocheck'", 54 | cwd => "/root/ovs", 55 | logoutput => true, 56 | loglevel => verbose, 57 | timeout => 0, 58 | creates => "/root/openvswitch-common_2.3.0-1_amd64.deb", 59 | require => [ 60 | Package["build-essential"], 61 | Package["fakeroot"], 62 | Package[$base_packages], 63 | Exec["extract_ovs"], 64 | ], 65 | } 66 | 67 | package { "ovs_common": 68 | name => "openvswitch-common", 69 | ensure => installed, 70 | provider => dpkg, 71 | source => "/root/openvswitch-common_2.3.0-1_amd64.deb?dl=0", 72 | require => [ Exec["build_ovs"] ], 73 | } 74 | 75 | package { "ovs_switch": 76 | name => "openvswitch-switch", 77 | ensure => installed, 78 | provider => dpkg, 79 | source => "/root/openvswitch-switch_2.3.0-1_amd64.deb", 80 | require => [ Package["ovs_common"] ], 81 | } 82 | 83 | } 84 | 85 | 86 | 87 | node servernode inherits basenode { 88 | 89 | package { "ovs_python": 90 | name => "python-openvswitch", 91 | ensure => installed, 92 | provider => dpkg, 93 | source => "/root/python-openvswitch_2.3.0-1_all.deb", 94 | require => [ Package["ovs_switch"] ], 95 | } 96 | 97 | package { "ovs_vtep": 98 | name => "openvswitch-vtep", 99 | ensure => installed, 100 | provider => dpkg, 101 | source => "/root/openvswitch-vtep_2.3.0-1_amd64.deb", 102 | require => [ Package["ovs_python"] ], 103 | } 104 | 105 | package { "mininet": 106 | ensure => installed, 107 | require => [ 108 | Package["ovs_switch"], 109 | ] 110 | } 111 | 112 | } 113 | 114 | 115 | 116 | import 'nodes/*.pp' 117 | -------------------------------------------------------------------------------- /shared/lesson03/ovsdb_client.py: -------------------------------------------------------------------------------- 1 | import json 2 | import select 3 | import socket 4 | import time 5 | 6 | 7 | class OVSDBClient(object): 8 | 9 | # =========== 10 | # EXCEPTIONS 11 | # =========== 12 | 13 | class ConnectionError(Exception): 14 | 15 | def __init__(self, hostname, port, errno, message): 16 | message = "Unable to connect to %s:%s. More info: (%i) %s" \ 17 | % (hostname, port, errno, message) 18 | 19 | super(self.__class__, self).__init__(message) 20 | 21 | class ReceiveTimeoutError(Exception): 22 | 23 | def __init__(self, timeout, buff): 24 | if not buff: 25 | buff = "" 26 | message = "Did not get a notification after %i seconds. " \ 27 | "Data received so far: %s" % (timeout, buff) 28 | 29 | super(self.__class__, self).__init__(message) 30 | 31 | class RequestTimeoutError(Exception): 32 | 33 | def __init__(self, formatted_json, timeout, buff): 34 | if not buff: 35 | buff = "" 36 | message = "Request timed out after %i seconds. Request " \ 37 | "details: %s. Data received so far: %s" \ 38 | % (timeout, formatted_json, buff) 39 | 40 | super(self.__class__, self).__init__(message) 41 | 42 | # =========== 43 | # INITIALIZER 44 | # =========== 45 | 46 | def __init__(self, hostname=None, port=None, request_timeout=5): 47 | self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 48 | 49 | try: 50 | self._socket.connect((hostname, port)) 51 | except socket.error as (errno, message): 52 | raise OVSDBClient.ConnectionError(hostname, port, errno, message) 53 | 54 | self._request_timeout = request_timeout 55 | 56 | # ================= 57 | # PUBLIC PROPERTIES 58 | # ================= 59 | 60 | @property 61 | def buffer(self, max_chunk_size=12288): 62 | try: 63 | ready_to_read, ready_to_write, in_error = \ 64 | select.select([self._socket], [], [], 0) 65 | if self._socket in ready_to_read: 66 | self._buffer += self._socket.recv(max_chunk_size) 67 | else: 68 | self._buffer += "" 69 | except socket.error: 70 | self._buffer += "" 71 | 72 | return self._buffer 73 | 74 | # =============== 75 | # PUBLIC METHODS 76 | # =============== 77 | 78 | def receive(self): 79 | self._flush_buffer() 80 | message = "" 81 | start = time.time() 82 | 83 | while len(message) == 0 or message.count("}") < message.count("{"): 84 | message = self.buffer 85 | duration = time.time() - start 86 | if duration > self._request_timeout: 87 | raise OVSDBClient.ReceiveTimeoutError(duration, message) 88 | 89 | return json.loads(message) 90 | 91 | def request(self, dict): 92 | message = json.dumps(dict) 93 | self._flush_buffer() 94 | 95 | self._socket.send(message) 96 | 97 | response = "" 98 | start = time.time() 99 | 100 | while len(response) == 0 or \ 101 | response.count("}") < response.count("{"): 102 | response = self.buffer 103 | duration = time.time() - start 104 | if duration > self._request_timeout: 105 | raise OVSDBClient.RequestTimeoutError( 106 | message, duration, response) 107 | 108 | return json.loads(response) 109 | 110 | def send(self, dict): 111 | self._flush_buffer() 112 | self._socket.send(json.dumps(dict)) 113 | 114 | # ================ 115 | # PRIVATE METHODS 116 | # ================ 117 | 118 | def _flush_buffer(self): 119 | self._buffer = "" 120 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | # NOTE: Netmask is assumed 255.255.255.0 for all 7 | 8 | SERVER1_TUNNEL_IP = "192.168.1.10" 9 | SERVER1_TUNNEL_GATEWAY = "192.168.1.254" 10 | SERVER1_MGMT_IP = "192.168.101.10" 11 | 12 | SERVER2_TUNNEL_IP = "192.168.2.20" 13 | SERVER2_TUNNEL_GATEWAY = "192.168.2.254" 14 | SERVER2_MGMT_IP = "192.168.101.20" 15 | 16 | INTERNET_MGMT_IP = "192.168.101.254" 17 | 18 | 19 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 20 | config.vm.box = "ubuntu/trusty64" 21 | 22 | 23 | # "INTERNET" 24 | config.vm.define "internet" do |server| 25 | server.vm.hostname = "internet" 26 | 27 | # Server 1 tunnel transport network 28 | server.vm.network "private_network", 29 | virtualbox__intnet: "server1_net", 30 | ip: SERVER1_TUNNEL_GATEWAY, 31 | netmask: "255.255.255.0" 32 | 33 | # Server 2 tunnel transport network 34 | server.vm.network "private_network", 35 | virtualbox__intnet: "server2_net", 36 | ip: SERVER2_TUNNEL_GATEWAY, 37 | netmask: "255.255.255.0" 38 | 39 | # Management/Control network 40 | server.vm.network "private_network", 41 | ip: INTERNET_MGMT_IP, 42 | netmask: "255.255.255.0" 43 | 44 | server.vm.provider "virtualbox" do |vb| 45 | vb.customize ["modifyvm", :id, "--memory", 256] 46 | vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"] 47 | end 48 | 49 | server.vm.provision "shell", inline: <<-SCRIPT 50 | [ "$(sysctl --values net.ipv4.ip_forward)" -eq "1" ] || { 51 | # Persist across reboots 52 | echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf # enable ip4 forwarding 53 | sysctl -p # apply settings from /etc/sysctl.conf 54 | } 55 | SCRIPT 56 | 57 | server.vm.provision "puppet" do |puppet| 58 | puppet.manifests_path = "puppet/manifests" 59 | puppet.manifest_file = "site.pp" 60 | puppet.options = "--verbose --debug" 61 | end 62 | end 63 | 64 | 65 | # SERVER 1 66 | config.vm.define "server1" do |server| 67 | server.vm.hostname = "server1" 68 | 69 | # Tunnel transport network 70 | server.vm.network "private_network", 71 | virtualbox__intnet: "server1_net", 72 | ip: SERVER1_TUNNEL_IP, 73 | netmask: "255.255.255.0", 74 | auto_config: false 75 | 76 | # Provision tunnel transport interface via file, so we can 77 | # add a persistent static route to tunnel gateway 78 | server.vm.provision "shell", inline: <<-SCRIPT 79 | [ -e /etc/network/interfaces.d/eth1.cfg ] || { 80 | cat << EOT >/etc/network/interfaces.d/eth1.cfg 81 | auto eth1 82 | iface eth1 inet static 83 | address #{SERVER1_TUNNEL_IP} 84 | netmask 255.255.255.0 85 | up ip route add #{SERVER2_TUNNEL_IP}/32 via #{SERVER1_TUNNEL_GATEWAY} dev eth1 86 | 87 | EOT 88 | ifup eth1 89 | } 90 | SCRIPT 91 | 92 | # Management/Control network 93 | server.vm.network "private_network", 94 | ip: SERVER1_MGMT_IP, 95 | netmask: "255.255.255.0" 96 | 97 | server.vm.provider "virtualbox" do |vb| 98 | vb.customize ["modifyvm", :id, "--memory", 256] 99 | vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"] 100 | end 101 | 102 | server.vm.provision "puppet" do |puppet| 103 | puppet.manifests_path = "puppet/manifests" 104 | puppet.manifest_file = "site.pp" 105 | puppet.options = "--verbose --debug" 106 | end 107 | end 108 | 109 | 110 | # SERVER 2 111 | config.vm.define "server2" do |server| 112 | server.vm.hostname = "server2" 113 | 114 | # Tunnel transport network 115 | server.vm.network "private_network", 116 | virtualbox__intnet: "server2_net", 117 | ip: SERVER2_TUNNEL_IP, 118 | netmask: "255.255.255.0", 119 | auto_config: false 120 | 121 | # Provision tunnel transport interface via file, so we can 122 | # add a persistent static route to tunnel gateway 123 | server.vm.provision "shell", inline: <<-SCRIPT 124 | [ -e /etc/network/interfaces.d/eth1.cfg ] || { 125 | cat << EOT >/etc/network/interfaces.d/eth1.cfg 126 | auto eth1 127 | iface eth1 inet static 128 | address #{SERVER2_TUNNEL_IP} 129 | netmask 255.255.255.0 130 | up ip route add #{SERVER1_TUNNEL_IP}/32 via #{SERVER2_TUNNEL_GATEWAY} dev eth1 131 | 132 | EOT 133 | ifup eth1 134 | } 135 | SCRIPT 136 | 137 | # Management/Control network 138 | server.vm.network "private_network", 139 | ip: SERVER2_MGMT_IP, 140 | netmask: "255.255.255.0" 141 | 142 | server.vm.provider "virtualbox" do |vb| 143 | vb.customize ["modifyvm", :id, "--memory", 256] 144 | vb.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/v-root", "1"] 145 | end 146 | 147 | server.vm.provision "puppet" do |puppet| 148 | puppet.manifests_path = "puppet/manifests" 149 | puppet.manifest_file = "site.pp" 150 | puppet.options = "--verbose --debug" 151 | end 152 | end 153 | 154 | 155 | end 156 | -------------------------------------------------------------------------------- /shared/lesson02/ovs-vtep: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # Copyright (C) 2013 Nicira, Inc. All Rights Reserved. 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 | # Limitations: 17 | # - Doesn't support multicast other than "unknown-dst" 18 | 19 | # NOTE: This is a modifed version of https://github.com/openvswitch/ovs/blob/master/vtep/ovs-vtep 20 | 21 | import argparse 22 | import re 23 | import subprocess 24 | import sys 25 | import time 26 | import types 27 | 28 | import ovs.dirs 29 | import ovs.util 30 | import ovs.daemon 31 | import ovs.unixctl.server 32 | import ovs.vlog 33 | 34 | 35 | VERSION = "0.99" 36 | 37 | root_prefix = "" 38 | 39 | __pychecker__ = 'no-reuseattr' # Remove in pychecker >= 0.8.19. 40 | vlog = ovs.vlog.Vlog("ovs-vtep") 41 | exiting = False 42 | 43 | Tunnel_Ip = "" 44 | Lswitches = {} 45 | Bindings = {} 46 | ls_count = 0 47 | tun_id = 0 48 | 49 | def call_prog(prog, args_list): 50 | cmd = [prog, "-vconsole:off"] + args_list 51 | output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate() 52 | if len(output) == 0 or output[0] == None: 53 | output = "" 54 | else: 55 | output = output[0].strip() 56 | return output 57 | 58 | def ovs_vsctl(args): 59 | return call_prog("ovs-vsctl", args.split()) 60 | 61 | def ovs_ofctl(args): 62 | return call_prog("ovs-ofctl", args.split()) 63 | 64 | def vtep_ctl(args): 65 | return call_prog("vtep-ctl", args.split()) 66 | 67 | 68 | def unixctl_exit(conn, unused_argv, unused_aux): 69 | global exiting 70 | exiting = True 71 | conn.reply(None) 72 | 73 | 74 | class Logical_Switch(object): 75 | def __init__(self, ls_name): 76 | global ls_count 77 | self.name = ls_name 78 | ls_count += 1 79 | self.short_name = "vtep_ls" + str(ls_count) 80 | vlog.info("creating lswitch %s (%s)" % (self.name, self.short_name)) 81 | self.ports = {} 82 | self.tunnels = {} 83 | self.local_macs = set() 84 | self.remote_macs = {} 85 | self.unknown_dsts = set() 86 | self.tunnel_key = 0 87 | self.setup_ls() 88 | 89 | def __del__(self): 90 | vlog.info("destroying lswitch %s" % self.name) 91 | 92 | def setup_ls(self): 93 | column = vtep_ctl("--columns=tunnel_key find logical_switch " 94 | "name=%s" % self.name) 95 | tunnel_key = column.partition(":")[2].strip() 96 | if (tunnel_key and type(eval(tunnel_key)) == types.IntType): 97 | self.tunnel_key = tunnel_key 98 | vlog.info("using tunnel key %s in %s" 99 | % (self.tunnel_key, self.name)) 100 | else: 101 | self.tunnel_key = 0 102 | vlog.warn("invalid tunnel key for %s, using 0" % self.name) 103 | 104 | ovs_vsctl("--may-exist add-br %s" % self.short_name) 105 | ovs_vsctl("br-set-external-id %s vtep_logical_switch true" 106 | % self.short_name) 107 | ovs_vsctl("br-set-external-id %s logical_switch_name %s" 108 | % (self.short_name, self.name)) 109 | 110 | vtep_ctl("clear-local-macs %s" % self.name) 111 | vtep_ctl("add-mcast-local %s unknown-dst %s" % (self.name, Tunnel_Ip)) 112 | 113 | ovs_ofctl("del-flows %s" % self.short_name) 114 | ovs_ofctl("add-flow %s priority=0,action=drop" % self.short_name) 115 | 116 | def update_flood(self): 117 | flood_ports = self.ports.values() 118 | 119 | # Traffic flowing from one 'unknown-dst' should not be flooded to 120 | # port belonging to another 'unknown-dst'. 121 | for tunnel in self.unknown_dsts: 122 | port_no = self.tunnels[tunnel][0] 123 | ovs_ofctl("add-flow %s table=1,priority=1,in_port=%s,action=%s" 124 | % (self.short_name, port_no, ",".join(flood_ports))) 125 | 126 | # Traffic coming from a VTEP physical port should only be flooded to 127 | # one 'unknown-dst' and to all other physical ports that belong to that 128 | # VTEP device and this logical switch. 129 | for tunnel in self.unknown_dsts: 130 | port_no = self.tunnels[tunnel][0] 131 | flood_ports.append(port_no) 132 | break 133 | 134 | ovs_ofctl("add-flow %s table=1,priority=0,action=%s" 135 | % (self.short_name, ",".join(flood_ports))) 136 | 137 | def add_lbinding(self, lbinding): 138 | vlog.info("adding %s binding to %s" % (lbinding, self.name)) 139 | port_no = ovs_vsctl("get Interface %s ofport" % lbinding) 140 | self.ports[lbinding] = port_no 141 | ovs_ofctl("add-flow %s in_port=%s,action=learn(table=1," 142 | "priority=1000,idle_timeout=15,cookie=0x5000," 143 | "NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[]," 144 | "output:NXM_OF_IN_PORT[]),resubmit(,1)" 145 | % (self.short_name, port_no)) 146 | 147 | self.update_flood() 148 | 149 | def del_lbinding(self, lbinding): 150 | vlog.info("removing %s binding from %s" % (lbinding, self.name)) 151 | port_no = self.ports[lbinding] 152 | ovs_ofctl("del-flows %s in_port=%s" % (self.short_name, port_no)); 153 | del self.ports[lbinding] 154 | self.update_flood() 155 | 156 | def add_tunnel(self, tunnel): 157 | global tun_id 158 | vlog.info("adding tunnel %s" % tunnel) 159 | encap, ip = tunnel.split("/") 160 | 161 | if encap != "vxlan_over_ipv4": 162 | vlog.warn("unsupported tunnel format %s" % encap) 163 | return 164 | 165 | tun_id += 1 166 | tun_name = "vx" + str(tun_id) 167 | 168 | ovs_vsctl("add-port %s %s -- set Interface %s type=vxlan " 169 | "options:key=%s options:remote_ip=%s" 170 | % (self.short_name, tun_name, tun_name, self.tunnel_key, ip)) 171 | 172 | for i in range(10): 173 | port_no = ovs_vsctl("get Interface %s ofport" % tun_name) 174 | if port_no != "-1": 175 | break 176 | elif i == 9: 177 | vlog.warn("couldn't create tunnel %s" % tunnel) 178 | ovs_vsctl("del-port %s %s" % (self.short_name, tun_name)) 179 | return 180 | 181 | # Give the system a moment to allocate the port number 182 | time.sleep(0.5) 183 | 184 | self.tunnels[tunnel] = (port_no, tun_name) 185 | 186 | ovs_ofctl("add-flow %s table=0,priority=1000,in_port=%s," 187 | "actions=resubmit(,1)" 188 | % (self.short_name, port_no)) 189 | 190 | def del_tunnel(self, tunnel): 191 | vlog.info("removing tunnel %s" % tunnel) 192 | 193 | port_no, tun_name = self.tunnels[tunnel] 194 | ovs_ofctl("del-flows %s table=0,in_port=%s" 195 | % (self.short_name, port_no)) 196 | ovs_vsctl("del-port %s %s" % (self.short_name, tun_name)) 197 | 198 | del self.tunnels[tunnel] 199 | 200 | def update_local_macs(self): 201 | flows = ovs_ofctl("dump-flows %s cookie=0x5000/-1,table=1" 202 | % self.short_name).splitlines() 203 | macs = set() 204 | for f in flows: 205 | mac = re.split(r'.*dl_dst=(.*) .*', f) 206 | if len(mac) == 3: 207 | macs.add(mac[1]) 208 | 209 | for mac in macs.difference(self.local_macs): 210 | vlog.info("adding local ucast %s to %s" % (mac, self.name)) 211 | vtep_ctl("add-ucast-local %s %s %s" % (self.name, mac, Tunnel_Ip)) 212 | 213 | for mac in self.local_macs.difference(macs): 214 | vlog.info("removing local ucast %s from %s" % (mac, self.name)) 215 | vtep_ctl("del-ucast-local %s %s" % (self.name, mac)) 216 | 217 | self.local_macs = macs 218 | 219 | def add_remote_mac(self, mac, tunnel): 220 | port_no = self.tunnels.get(tunnel, (0,""))[0] 221 | if not port_no: 222 | return 223 | 224 | ovs_ofctl("add-flow %s table=1,priority=1000,dl_dst=%s,action=%s" 225 | % (self.short_name, mac, port_no)) 226 | 227 | def del_remote_mac(self, mac): 228 | ovs_ofctl("del-flows %s table=1,dl_dst=%s" % (self.short_name, mac)) 229 | 230 | def update_remote_macs(self): 231 | remote_macs = {} 232 | unknown_dsts = set() 233 | tunnels = set() 234 | parse_ucast = True 235 | 236 | mac_list = vtep_ctl("list-remote-macs %s" % self.name).splitlines() 237 | for line in mac_list: 238 | if (line.find("mcast-mac-remote") != -1): 239 | parse_ucast = False 240 | continue 241 | 242 | entry = re.split(r' (.*) -> (.*)', line) 243 | if len(entry) != 4: 244 | continue 245 | 246 | if parse_ucast: 247 | remote_macs[entry[1]] = entry[2] 248 | else: 249 | if entry[1] != "unknown-dst": 250 | continue 251 | 252 | unknown_dsts.add(entry[2]) 253 | 254 | tunnels.add(entry[2]) 255 | 256 | old_tunnels = set(self.tunnels.keys()) 257 | 258 | for tunnel in tunnels.difference(old_tunnels): 259 | self.add_tunnel(tunnel) 260 | 261 | for tunnel in old_tunnels.difference(tunnels): 262 | self.del_tunnel(tunnel) 263 | 264 | for mac in remote_macs.keys(): 265 | if (self.remote_macs.get(mac) != remote_macs[mac]): 266 | self.add_remote_mac(mac, remote_macs[mac]) 267 | 268 | for mac in self.remote_macs.keys(): 269 | if not remote_macs.has_key(mac): 270 | self.del_remote_mac(mac) 271 | 272 | self.remote_macs = remote_macs 273 | 274 | if (self.unknown_dsts != unknown_dsts): 275 | self.unknown_dsts = unknown_dsts 276 | self.update_flood() 277 | 278 | def update_stats(self): 279 | # Map Open_vSwitch's "interface:statistics" to columns of 280 | # vtep's logical_binding_stats. Since we are using the 'interface' from 281 | # the logical switch to collect stats, packets transmitted from it 282 | # is received in the physical switch and vice versa. 283 | stats_map = {'tx_packets':'packets_to_local', 284 | 'tx_bytes':'bytes_to_local', 285 | 'rx_packets':'packets_from_local', 286 | 'rx_bytes':'bytes_from_local'} 287 | 288 | # Go through all the logical switch's interfaces that end with "-l" 289 | # and copy the statistics to logical_binding_stats. 290 | for interface in self.ports.iterkeys(): 291 | if not interface.endswith("--l"): 292 | continue 293 | vlan, pp_name, logical = interface.split("--") 294 | uuid = vtep_ctl("get physical_port %s vlan_stats:%s" 295 | % (pp_name, vlan)) 296 | if not uuid: 297 | continue 298 | 299 | for (mapfrom, mapto) in stats_map.iteritems(): 300 | value = ovs_vsctl("get interface %s statistics:%s" 301 | % (interface, mapfrom)).strip('"') 302 | vtep_ctl("set logical_binding_stats %s %s=%s" 303 | % (uuid, mapto, value)) 304 | 305 | def run(self): 306 | self.update_local_macs() 307 | self.update_remote_macs() 308 | self.update_stats() 309 | 310 | def add_binding(ps_name, binding, ls): 311 | vlog.info("adding binding %s" % binding) 312 | 313 | vlan, pp_name = binding.split("--") 314 | pbinding = binding+"--p" 315 | lbinding = binding+"--l" 316 | 317 | # Create a patch port that connects the VLAN+port to the lswitch. 318 | # Do them as two separate calls so if one side already exists, the 319 | # other side is created. 320 | ovs_vsctl("add-port %s %s " 321 | " -- set Interface %s type=patch options:peer=%s" 322 | % (ps_name, pbinding, pbinding, lbinding)) 323 | ovs_vsctl("add-port %s %s " 324 | " -- set Interface %s type=patch options:peer=%s" 325 | % (ls.short_name, lbinding, lbinding, pbinding)) 326 | 327 | port_no = ovs_vsctl("get Interface %s ofport" % pp_name) 328 | patch_no = ovs_vsctl("get Interface %s ofport" % pbinding) 329 | vlan_ = vlan.lstrip('0') 330 | if vlan_: 331 | ovs_ofctl("add-flow %s in_port=%s,dl_vlan=%s,action=strip_vlan,%s" 332 | % (ps_name, port_no, vlan_, patch_no)) 333 | ovs_ofctl("add-flow %s in_port=%s,action=mod_vlan_vid:%s,%s" 334 | % (ps_name, patch_no, vlan_, port_no)) 335 | else: 336 | ovs_ofctl("add-flow %s in_port=%s,action=%s" 337 | % (ps_name, port_no, patch_no)) 338 | ovs_ofctl("add-flow %s in_port=%s,action=%s" 339 | % (ps_name, patch_no, port_no)) 340 | 341 | # Create a logical_bindings_stats record. 342 | if not vlan_: 343 | vlan_ = "0" 344 | vtep_ctl("set physical_port %s vlan_stats:%s=@stats --\ 345 | --id=@stats create logical_binding_stats packets_from_local=0"\ 346 | % (pp_name, vlan_)) 347 | 348 | ls.add_lbinding(lbinding) 349 | Bindings[binding] = ls.name 350 | 351 | def del_binding(ps_name, binding, ls): 352 | vlog.info("removing binding %s" % binding) 353 | 354 | vlan, pp_name = binding.split("--") 355 | pbinding = binding+"--p" 356 | lbinding = binding+"--l" 357 | 358 | port_no = ovs_vsctl("get Interface %s ofport" % pp_name) 359 | patch_no = ovs_vsctl("get Interface %s ofport" % pbinding) 360 | vlan_ = vlan.lstrip('0') 361 | if vlan_: 362 | ovs_ofctl("del-flows %s in_port=%s,dl_vlan=%s" 363 | % (ps_name, port_no, vlan_)) 364 | ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no)) 365 | else: 366 | ovs_ofctl("del-flows %s in_port=%s" % (ps_name, port_no)) 367 | ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no)) 368 | 369 | ls.del_lbinding(lbinding) 370 | 371 | # Destroy the patch port that connects the VLAN+port to the lswitch 372 | ovs_vsctl("del-port %s %s -- del-port %s %s" 373 | % (ps_name, pbinding, ls.short_name, lbinding)) 374 | 375 | # Remove the record that links vlan with stats in logical_binding_stats. 376 | vtep_ctl("remove physical_port %s vlan_stats %s" % (pp_name, vlan)) 377 | 378 | del Bindings[binding] 379 | 380 | def handle_physical(ps_name): 381 | # Gather physical ports except the patch ports we created 382 | ovs_ports = ovs_vsctl("list-ports %s" % ps_name).split() 383 | ovs_port_set = set([port for port in ovs_ports if port[-2:] != "-p"]) 384 | 385 | vtep_pp_set = set(vtep_ctl("list-ports %s" % ps_name).split()) 386 | 387 | for pp_name in ovs_port_set.difference(vtep_pp_set): 388 | vlog.info("adding %s to %s" % (pp_name, ps_name)) 389 | vtep_ctl("add-port %s %s" % (ps_name, pp_name)) 390 | 391 | for pp_name in vtep_pp_set.difference(ovs_port_set): 392 | vlog.info("deleting %s from %s" % (pp_name, ps_name)) 393 | vtep_ctl("del-port %s %s" % (ps_name, pp_name)) 394 | 395 | new_bindings = set() 396 | for pp_name in vtep_pp_set: 397 | binding_set = set(vtep_ctl("list-bindings %s %s" 398 | % (ps_name, pp_name)).splitlines()) 399 | 400 | for b in binding_set: 401 | vlan, ls_name = b.split() 402 | if ls_name not in Lswitches: 403 | Lswitches[ls_name] = Logical_Switch(ls_name) 404 | 405 | binding = "%s--%s" % (vlan, pp_name) 406 | ls = Lswitches[ls_name] 407 | new_bindings.add(binding) 408 | 409 | if Bindings.has_key(binding): 410 | if Bindings[binding] == ls_name: 411 | continue 412 | else: 413 | del_binding(ps_name, binding, Lswitches[Bindings[binding]]) 414 | 415 | add_binding(ps_name, binding, ls) 416 | 417 | 418 | dead_bindings = set(Bindings.keys()).difference(new_bindings) 419 | for binding in dead_bindings: 420 | ls_name = Bindings[binding] 421 | ls = Lswitches[ls_name] 422 | 423 | del_binding(ps_name, binding, ls) 424 | 425 | if not len(ls.ports): 426 | ovs_vsctl("del-br %s" % Lswitches[ls_name].short_name) 427 | del Lswitches[ls_name] 428 | 429 | def setup(ps_name): 430 | br_list = ovs_vsctl("list-br").split() 431 | if (ps_name not in br_list): 432 | ovs.util.ovs_fatal(0, "couldn't find OVS bridge %s" % ps_name, vlog) 433 | 434 | call_prog("vtep-ctl", ["set", "physical_switch", ps_name, 435 | 'description="OVS VTEP Emulator"']) 436 | 437 | tunnel_ips = vtep_ctl("get physical_switch %s tunnel_ips" 438 | % ps_name).strip('[]"').split(", ") 439 | if len(tunnel_ips) != 1 or not tunnel_ips[0]: 440 | ovs.util.ovs_fatal(0, "exactly one 'tunnel_ips' should be set", vlog) 441 | 442 | global Tunnel_Ip 443 | Tunnel_Ip = tunnel_ips[0] 444 | 445 | ovs_ofctl("del-flows %s" % ps_name) 446 | 447 | # Remove any logical bridges from the previous run 448 | for br in br_list: 449 | if ovs_vsctl("br-get-external-id %s vtep_logical_switch" 450 | % br) == "true": 451 | # Remove the remote side of any logical switch 452 | ovs_ports = ovs_vsctl("list-ports %s" % br).split() 453 | for port in ovs_ports: 454 | port_type = ovs_vsctl("get Interface %s type" 455 | % port).strip('"') 456 | if port_type != "patch": 457 | continue 458 | 459 | peer = ovs_vsctl("get Interface %s options:peer" 460 | % port).strip('"') 461 | if (peer): 462 | ovs_vsctl("del-port %s" % peer) 463 | 464 | ovs_vsctl("del-br %s" % br) 465 | 466 | 467 | def main(): 468 | parser = argparse.ArgumentParser() 469 | parser.add_argument("ps_name", metavar="PS-NAME", 470 | help="Name of physical switch.") 471 | parser.add_argument("--root-prefix", metavar="DIR", 472 | help="Use DIR as alternate root directory" 473 | " (for testing).") 474 | parser.add_argument("--version", action="version", 475 | version="%s %s" % (ovs.util.PROGRAM_NAME, VERSION)) 476 | 477 | ovs.vlog.add_args(parser) 478 | ovs.daemon.add_args(parser) 479 | args = parser.parse_args() 480 | ovs.vlog.handle_args(args) 481 | ovs.daemon.handle_args(args) 482 | 483 | global root_prefix 484 | if args.root_prefix: 485 | root_prefix = args.root_prefix 486 | 487 | ps_name = args.ps_name 488 | 489 | ovs.daemon.daemonize() 490 | 491 | ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None) 492 | error, unixctl = ovs.unixctl.server.UnixctlServer.create(None, 493 | version=VERSION) 494 | if error: 495 | ovs.util.ovs_fatal(error, "could not create unixctl server", vlog) 496 | 497 | setup(ps_name) 498 | 499 | while True: 500 | unixctl.run() 501 | if exiting: 502 | break 503 | 504 | handle_physical(ps_name) 505 | 506 | for ls_name, ls in Lswitches.items(): 507 | ls.run() 508 | 509 | poller = ovs.poller.Poller() 510 | unixctl.wait(poller) 511 | poller.timer_wait(1000) 512 | poller.block() 513 | 514 | unixctl.close() 515 | 516 | if __name__ == '__main__': 517 | try: 518 | main() 519 | except SystemExit: 520 | # Let system.exit() calls complete normally 521 | raise 522 | except: 523 | vlog.exception("traceback") 524 | sys.exit(ovs.daemon.RESTART_EXIT_CODE) 525 | --------------------------------------------------------------------------------