Add Docker integration for OVN.
[cascardo/ovs.git] / ovn / utilities / ovn-docker-underlay-driver
1 #! /usr/bin/python
2 # Copyright (C) 2015 Nicira, Inc.
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 import argparse
17 import atexit
18 import getpass
19 import json
20 import os
21 import re
22 import shlex
23 import subprocess
24 import sys
25 import time
26 import uuid
27
28 import ovs.dirs
29 import ovs.util
30 import ovs.daemon
31 import ovs.unixctl.server
32 import ovs.vlog
33
34 from neutronclient.v2_0 import client
35 from flask import Flask, jsonify
36 from flask import request, abort
37
38 app = Flask(__name__)
39 vlog = ovs.vlog.Vlog("ovn-docker-underlay-driver")
40
41 AUTH_STRATEGY = ""
42 AUTH_URL = ""
43 ENDPOINT_URL = ""
44 OVN_BRIDGE = ""
45 PASSWORD = ""
46 PLUGIN_DIR = "/etc/docker/plugins"
47 PLUGIN_FILE = "/etc/docker/plugins/openvswitch.spec"
48 TENANT_ID = ""
49 USERNAME = ""
50 VIF_ID = ""
51
52
53 def call_popen(cmd):
54     child = subprocess.Popen(cmd, stdout=subprocess.PIPE)
55     output = child.communicate()
56     if child.returncode:
57         raise RuntimeError("Fatal error executing %s" % (cmd))
58     if len(output) == 0 or output[0] == None:
59         output = ""
60     else:
61         output = output[0].strip()
62     return output
63
64
65 def call_prog(prog, args_list):
66     cmd = [prog, "--timeout=5", "-vconsole:off"] + args_list
67     return call_popen(cmd)
68
69
70 def ovs_vsctl(*args):
71     return call_prog("ovs-vsctl", list(args))
72
73
74 def cleanup():
75     if os.path.isfile(PLUGIN_FILE):
76         os.remove(PLUGIN_FILE)
77
78
79 def ovn_init_underlay(args):
80     global USERNAME, PASSWORD, TENANT_ID, AUTH_URL, AUTH_STRATEGY, VIF_ID
81     global OVN_BRIDGE
82
83     if not args.bridge:
84         sys.exit("OVS bridge name not provided")
85     OVN_BRIDGE = args.bridge
86
87     VIF_ID = os.environ.get('OS_VIF_ID', '')
88     if not VIF_ID:
89         sys.exit("env OS_VIF_ID not set")
90     USERNAME = os.environ.get('OS_USERNAME', '')
91     if not USERNAME:
92         sys.exit("env OS_USERNAME not set")
93     TENANT_ID = os.environ.get('OS_TENANT_ID', '')
94     if not TENANT_ID:
95         sys.exit("env OS_TENANT_ID not set")
96     AUTH_URL = os.environ.get('OS_AUTH_URL', '')
97     if not AUTH_URL:
98         sys.exit("env OS_AUTH_URL not set")
99     AUTH_STRATEGY = "keystone"
100
101     PASSWORD = os.environ.get('OS_PASSWORD', '')
102     if not PASSWORD:
103         PASSWORD = getpass.getpass()
104
105
106 def prepare():
107     parser = argparse.ArgumentParser()
108     parser.add_argument('--bridge', help="The Bridge to which containers "
109                         "interfaces connect to.")
110
111     ovs.vlog.add_args(parser)
112     ovs.daemon.add_args(parser)
113     args = parser.parse_args()
114     ovs.vlog.handle_args(args)
115     ovs.daemon.handle_args(args)
116     ovn_init_underlay(args)
117
118     if not os.path.isdir(PLUGIN_DIR):
119         os.makedirs(PLUGIN_DIR)
120
121     ovs.daemon.daemonize()
122     try:
123         fo = open(PLUGIN_FILE, "w")
124         fo.write("tcp://127.0.0.1:5000")
125         fo.close()
126     except Exception as e:
127         ovs.util.ovs_fatal(0, "Failed to write to spec file (%s)" % str(e),
128                            vlog)
129
130     atexit.register(cleanup)
131
132
133 @app.route('/Plugin.Activate', methods=['POST'])
134 def plugin_activate():
135     return jsonify({"Implements": ["NetworkDriver"]})
136
137
138 @app.route('/NetworkDriver.GetCapabilities', methods=['POST'])
139 def get_capability():
140     return jsonify({"Scope": "global"})
141
142
143 @app.route('/NetworkDriver.DiscoverNew', methods=['POST'])
144 def new_discovery():
145     return jsonify({})
146
147
148 @app.route('/NetworkDriver.DiscoverDelete', methods=['POST'])
149 def delete_discovery():
150     return jsonify({})
151
152
153 def neutron_login():
154     try:
155         neutron = client.Client(username=USERNAME,
156                                 password=PASSWORD,
157                                 tenant_id=TENANT_ID,
158                                 auth_url=AUTH_URL,
159                                 endpoint_url=ENDPOINT_URL,
160                                 auth_strategy=AUTH_STRATEGY)
161     except Exception as e:
162         raise RuntimeError("Failed to login into Neutron(%s)" % str(e))
163     return neutron
164
165
166 def get_networkuuid_by_name(neutron, name):
167     param = {'fields': 'id', 'name': name}
168     ret = neutron.list_networks(**param)
169     if len(ret['networks']) > 1:
170         raise RuntimeError("More than one network for the given name")
171     elif len(ret['networks']) == 0:
172         network = None
173     else:
174         network = ret['networks'][0]['id']
175     return network
176
177
178 def get_subnetuuid_by_name(neutron, name):
179     param = {'fields': 'id', 'name': name}
180     ret = neutron.list_subnets(**param)
181     if len(ret['subnets']) > 1:
182         raise RuntimeError("More than one subnet for the given name")
183     elif len(ret['subnets']) == 0:
184         subnet = None
185     else:
186         subnet = ret['subnets'][0]['id']
187     return subnet
188
189
190 @app.route('/NetworkDriver.CreateNetwork', methods=['POST'])
191 def create_network():
192     if not request.data:
193         abort(400)
194
195     data = json.loads(request.data)
196
197     # NetworkID will have docker generated network uuid and it
198     # becomes 'name' in a neutron network record.
199     network = data.get("NetworkID", "")
200     if not network:
201         abort(400)
202
203     # Limit subnet handling to ipv4 till ipv6 usecase is clear.
204     ipv4_data = data.get("IPv4Data", "")
205     if not ipv4_data:
206         error = "create_network: No ipv4 subnet provided"
207         return jsonify({'Err': error})
208
209     subnet = ipv4_data[0].get("Pool", "")
210     if not subnet:
211         error = "create_network: no subnet in ipv4 data from libnetwork"
212         return jsonify({'Err': error})
213
214     gateway_ip = ipv4_data[0].get("Gateway", "").rsplit('/', 1)[0]
215     if not gateway_ip:
216         error = "create_network: no gateway in ipv4 data from libnetwork"
217         return jsonify({'Err': error})
218
219     try:
220         neutron = neutron_login()
221     except Exception as e:
222         error = "create_network: neutron login. (%s)" % (str(e))
223         return jsonify({'Err': error})
224
225     try:
226         if get_networkuuid_by_name(neutron, network):
227             error = "create_network: network has already been created"
228             return jsonify({'Err': error})
229     except Exception as e:
230         error = "create_network: neutron network uuid by name. (%s)" % (str(e))
231         return jsonify({'Err': error})
232
233     try:
234         body = {'network': {'name': network, 'admin_state_up': True}}
235         ret = neutron.create_network(body)
236         network_id = ret['network']['id']
237     except Exception as e:
238         error = "create_network: neutron net-create call. (%s)" % str(e)
239         return jsonify({'Err': error})
240
241     subnet_name = "docker-%s" % (network)
242
243     try:
244         body = {'subnet': {'network_id': network_id,
245                            'ip_version': 4,
246                            'cidr': subnet,
247                            'gateway_ip': gateway_ip,
248                            'name': subnet_name}}
249         created_subnet = neutron.create_subnet(body)
250     except Exception as e:
251         error = "create_network: neutron subnet-create call. (%s)" % str(e)
252         return jsonify({'Err': error})
253
254     return jsonify({})
255
256
257 @app.route('/NetworkDriver.DeleteNetwork', methods=['POST'])
258 def delete_network():
259     if not request.data:
260         abort(400)
261
262     data = json.loads(request.data)
263
264     nid = data.get("NetworkID", "")
265     if not nid:
266         abort(400)
267
268     try:
269         neutron = neutron_login()
270     except Exception as e:
271         error = "delete_network: neutron login. (%s)" % (str(e))
272         return jsonify({'Err': error})
273
274     try:
275         network = get_networkuuid_by_name(neutron, nid)
276         if not network:
277             error = "delete_network: failed in network by name. (%s)" % (nid)
278             return jsonify({'Err': error})
279     except Exception as e:
280         error = "delete_network: network uuid by name. (%s)" % (str(e))
281         return jsonify({'Err': error})
282
283     try:
284         neutron.delete_network(network)
285     except Exception as e:
286         error = "delete_network: neutron net-delete. (%s)" % str(e)
287         return jsonify({'Err': error})
288
289     return jsonify({})
290
291
292 def reserve_vlan():
293     reserved_vlan = 0
294     vlans = ovs_vsctl("--if-exists", "get", "Open_vSwitch", ".",
295                       "external_ids:vlans").strip('"')
296     if not vlans:
297         reserved_vlan = 1
298         ovs_vsctl("set", "Open_vSwitch", ".",
299                   "external_ids:vlans=" + str(reserved_vlan))
300         return reserved_vlan
301
302     vlan_set = str(vlans).split(',')
303
304     for vlan in range(1, 4095):
305         if str(vlan) not in vlan_set:
306             vlan_set.append(str(vlan))
307             reserved_vlan = vlan
308             vlans = re.sub(r'[ \[\]\']', '', str(vlan_set))
309             ovs_vsctl("set", "Open_vSwitch", ".",
310                       "external_ids:vlans=" + vlans)
311             return reserved_vlan
312
313     if not reserved_vlan:
314         raise RuntimeError("No more vlans available on this host")
315
316
317 def unreserve_vlan(reserved_vlan):
318     vlans = ovs_vsctl("--if-exists", "get", "Open_vSwitch", ".",
319                       "external_ids:vlans").strip('"')
320     if not vlans:
321         return
322
323     vlan_set = str(vlans).split(',')
324     if str(reserved_vlan) not in vlan_set:
325         return
326
327     vlan_set.remove(str(reserved_vlan))
328     vlans = re.sub(r'[ \[\]\']', '', str(vlan_set))
329     if vlans:
330         ovs_vsctl("set", "Open_vSwitch", ".", "external_ids:vlans=" + vlans)
331     else:
332         ovs_vsctl("remove", "Open_vSwitch", ".", "external_ids", "vlans")
333
334
335 def create_port_underlay(neutron, network, eid, ip_address, mac_address):
336     reserved_vlan = reserve_vlan()
337     if mac_address:
338         body = {'port': {'network_id': network,
339                          'binding:profile': {'parent_name': VIF_ID,
340                                              'tag': int(reserved_vlan)},
341                          'mac_address': mac_address,
342                          'fixed_ips': [{'ip_address': ip_address}],
343                          'name': eid,
344                          'admin_state_up': True}}
345     else:
346         body = {'port': {'network_id': network,
347                          'binding:profile': {'parent_name': VIF_ID,
348                                              'tag': int(reserved_vlan)},
349                          'fixed_ips': [{'ip_address': ip_address}],
350                          'name': eid,
351                          'admin_state_up': True}}
352
353     try:
354         ret = neutron.create_port(body)
355         mac_address = ret['port']['mac_address']
356     except Exception as e:
357         unreserve_vlan(reserved_vlan)
358         raise RuntimeError("Failed in creation of neutron port (%s)." % str(e))
359
360     ovs_vsctl("set", "Open_vSwitch", ".",
361               "external_ids:" + eid + "_vlan=" + str(reserved_vlan))
362
363     return mac_address
364
365
366 def get_endpointuuid_by_name(neutron, name):
367     param = {'fields': 'id', 'name': name}
368     ret = neutron.list_ports(**param)
369     if len(ret['ports']) > 1:
370         raise RuntimeError("More than one endpoint for the given name")
371     elif len(ret['ports']) == 0:
372         endpoint = None
373     else:
374         endpoint = ret['ports'][0]['id']
375     return endpoint
376
377
378 @app.route('/NetworkDriver.CreateEndpoint', methods=['POST'])
379 def create_endpoint():
380     if not request.data:
381         abort(400)
382
383     data = json.loads(request.data)
384
385     nid = data.get("NetworkID", "")
386     if not nid:
387         abort(400)
388
389     eid = data.get("EndpointID", "")
390     if not eid:
391         abort(400)
392
393     interface = data.get("Interface", "")
394     if not interface:
395         error = "create_endpoint: no interfaces supplied by libnetwork"
396         return jsonify({'Err': error})
397
398     ip_address_and_mask = interface.get("Address", "")
399     if not ip_address_and_mask:
400         error = "create_endpoint: ip address not provided by libnetwork"
401         return jsonify({'Err': error})
402
403     ip_address = ip_address_and_mask.rsplit('/', 1)[0]
404     mac_address_input = interface.get("MacAddress", "")
405     mac_address_output = ""
406
407     try:
408         neutron = neutron_login()
409     except Exception as e:
410         error = "create_endpoint: neutron login. (%s)" % (str(e))
411         return jsonify({'Err': error})
412
413     try:
414         endpoint = get_endpointuuid_by_name(neutron, eid)
415         if endpoint:
416             error = "create_endpoint: Endpoint has already been created"
417             return jsonify({'Err': error})
418     except Exception as e:
419         error = "create_endpoint: endpoint uuid by name. (%s)" % (str(e))
420         return jsonify({'Err': error})
421
422     try:
423         network = get_networkuuid_by_name(neutron, nid)
424         if not network:
425             error = "Failed to get neutron network record for (%s)" % (nid)
426             return jsonify({'Err': error})
427     except Exception as e:
428         error = "create_endpoint: network uuid by name. (%s)" % (str(e))
429         return jsonify({'Err': error})
430
431     try:
432         mac_address = create_port_underlay(neutron, network, eid, ip_address,
433                                            mac_address_input)
434     except Exception as e:
435         error = "create_endpoint: neutron port-create (%s)" % (str(e))
436         return jsonify({'Err': error})
437
438     if not mac_address_input:
439         mac_address_output = mac_address
440
441     return jsonify({"Interface": {
442                                     "Address": "",
443                                     "AddressIPv6": "",
444                                     "MacAddress": mac_address_output
445                                     }})
446
447
448 @app.route('/NetworkDriver.EndpointOperInfo', methods=['POST'])
449 def show_endpoint():
450     if not request.data:
451         abort(400)
452
453     data = json.loads(request.data)
454
455     nid = data.get("NetworkID", "")
456     if not nid:
457         abort(400)
458
459     eid = data.get("EndpointID", "")
460     if not eid:
461         abort(400)
462
463     try:
464         neutron = neutron_login()
465     except Exception as e:
466         error = "%s" % (str(e))
467         return jsonify({'Err': error})
468
469     try:
470         endpoint = get_endpointuuid_by_name(neutron, eid)
471         if not endpoint:
472             error = "show_endpoint: Failed to get endpoint by name"
473             return jsonify({'Err': error})
474     except Exception as e:
475         error = "show_endpoint: get endpoint by name. (%s)" % (str(e))
476         return jsonify({'Err': error})
477
478     try:
479         ret = neutron.show_port(endpoint)
480         mac_address = ret['port']['mac_address']
481         ip_address = ret['port']['fixed_ips'][0]['ip_address']
482     except Exception as e:
483         error = "show_endpoint: show port (%s)" % (str(e))
484         return jsonify({'Err': error})
485
486     veth_outside = eid[0:15]
487     return jsonify({"Value": {"ip_address": ip_address,
488                               "mac_address": mac_address,
489                               "veth_outside": veth_outside
490                               }})
491
492
493 @app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
494 def delete_endpoint():
495     if not request.data:
496         abort(400)
497
498     data = json.loads(request.data)
499
500     nid = data.get("NetworkID", "")
501     if not nid:
502         abort(400)
503
504     eid = data.get("EndpointID", "")
505     if not eid:
506         abort(400)
507
508     try:
509         neutron = neutron_login()
510     except Exception as e:
511         error = "delete_endpoint: neutron login (%s)" % (str(e))
512         return jsonify({'Err': error})
513
514     endpoint = get_endpointuuid_by_name(neutron, eid)
515     if not endpoint:
516         return jsonify({})
517
518     reserved_vlan = ovs_vsctl("--if-exists", "get", "Open_vSwitch", ".",
519                               "external_ids:" + eid + "_vlan").strip('"')
520     if reserved_vlan:
521         unreserve_vlan(reserved_vlan)
522         ovs_vsctl("remove", "Open_vSwitch", ".", "external_ids",
523                   eid + "_vlan")
524
525     try:
526         neutron.delete_port(endpoint)
527     except Exception as e:
528         error = "delete_endpoint: neutron port-delete. (%s)" % (str(e))
529         return jsonify({'Err': error})
530
531     return jsonify({})
532
533
534 @app.route('/NetworkDriver.Join', methods=['POST'])
535 def network_join():
536     if not request.data:
537         abort(400)
538
539     data = json.loads(request.data)
540
541     nid = data.get("NetworkID", "")
542     if not nid:
543         abort(400)
544
545     eid = data.get("EndpointID", "")
546     if not eid:
547         abort(400)
548
549     sboxkey = data.get("SandboxKey", "")
550     if not sboxkey:
551         abort(400)
552
553     # sboxkey is of the form: /var/run/docker/netns/CONTAINER_ID
554     vm_id = sboxkey.rsplit('/')[-1]
555
556     try:
557         neutron = neutron_login()
558     except Exception as e:
559         error = "network_join: neutron login. (%s)" % (str(e))
560         return jsonify({'Err': error})
561
562     subnet_name = "docker-%s" % (nid)
563     try:
564         subnet = get_subnetuuid_by_name(neutron, subnet_name)
565         if not subnet:
566             error = "network_join: can't find subnet in neutron"
567             return jsonify({'Err': error})
568     except Exception as e:
569         error = "network_join: subnet uuid by name. (%s)" % (str(e))
570         return jsonify({'Err': error})
571
572     try:
573         ret = neutron.show_subnet(subnet)
574         gateway_ip = ret['subnet']['gateway_ip']
575         if not gateway_ip:
576             error = "network_join: no gateway_ip for the subnet"
577             return jsonify({'Err': error})
578     except Exception as e:
579         error = "network_join: neutron show subnet. (%s)" % (str(e))
580         return jsonify({'Err': error})
581
582     try:
583         endpoint = get_endpointuuid_by_name(neutron, eid)
584         if not endpoint:
585             error = "network_join: Failed to get endpoint by name"
586             return jsonify({'Err': error})
587     except Exception as e:
588         error = "network_join: neutron endpoint by name. (%s)" % (str(e))
589         return jsonify({'Err': error})
590
591     try:
592         ret = neutron.show_port(endpoint)
593         mac_address = ret['port']['mac_address']
594     except Exception as e:
595         error = "network_join: neutron show port. (%s)" % (str(e))
596         return jsonify({'Err': error})
597
598     veth_outside = eid[0:15]
599     veth_inside = eid[0:13] + "_c"
600     command = "ip link add %s type veth peer name %s" \
601               % (veth_inside, veth_outside)
602     try:
603         call_popen(shlex.split(command))
604     except Exception as e:
605         error = "network_join: failed to create veth pair. (%s)" % (str(e))
606         return jsonify({'Err': error})
607
608     command = "ip link set dev %s address %s" \
609               % (veth_inside, mac_address)
610
611     try:
612         call_popen(shlex.split(command))
613     except Exception as e:
614         error = "network_join: failed to set veth mac address. (%s)" % (str(e))
615         return jsonify({'Err': error})
616
617     command = "ip link set %s up" % (veth_outside)
618
619     try:
620         call_popen(shlex.split(command))
621     except Exception as e:
622         error = "network_join: failed to up the veth iface. (%s)" % (str(e))
623         return jsonify({'Err': error})
624
625     try:
626         reserved_vlan = ovs_vsctl("--if-exists", "get", "Open_vSwitch", ".",
627                                   "external_ids:" + eid + "_vlan").strip('"')
628         if not reserved_vlan:
629             error = "network_join: no reserved vlan for this endpoint"
630             return jsonify({'Err': error})
631         ovs_vsctl("add-port", OVN_BRIDGE, veth_outside, "tag=" + reserved_vlan)
632     except Exception as e:
633         error = "network_join: failed to create a OVS port. (%s)" % (str(e))
634         return jsonify({'Err': error})
635
636     return jsonify({"InterfaceName": {
637                                         "SrcName": veth_inside,
638                                         "DstPrefix": "eth"
639                                      },
640                     "Gateway": gateway_ip,
641                     "GatewayIPv6": ""})
642
643
644 @app.route('/NetworkDriver.Leave', methods=['POST'])
645 def network_leave():
646     if not request.data:
647         abort(400)
648
649     data = json.loads(request.data)
650
651     nid = data.get("NetworkID", "")
652     if not nid:
653         abort(400)
654
655     eid = data.get("EndpointID", "")
656     if not eid:
657         abort(400)
658
659     veth_outside = eid[0:15]
660     command = "ip link delete %s" % (veth_outside)
661     try:
662         call_popen(shlex.split(command))
663     except Exception as e:
664         error = "network_leave: failed to delete veth pair. (%s)" % (str(e))
665         return jsonify({'Err': error})
666
667     try:
668         ovs_vsctl("--if-exists", "del-port", veth_outside)
669     except Exception as e:
670         error = "network_leave: Failed to delete port (%s)" % (str(e))
671         return jsonify({'Err': error})
672
673     return jsonify({})
674
675 if __name__ == '__main__':
676     prepare()
677     app.run(host='127.0.0.1')