Add Docker integration for OVN.
[cascardo/ovs.git] / ovn / utilities / ovn-docker-overlay-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 ast
18 import atexit
19 import json
20 import os
21 import random
22 import re
23 import shlex
24 import subprocess
25 import sys
26
27 import ovs.dirs
28 import ovs.util
29 import ovs.daemon
30 import ovs.vlog
31
32 from flask import Flask, jsonify
33 from flask import request, abort
34
35 app = Flask(__name__)
36 vlog = ovs.vlog.Vlog("ovn-docker-overlay-driver")
37
38 OVN_BRIDGE = "br-int"
39 OVN_REMOTE = ""
40 PLUGIN_DIR = "/etc/docker/plugins"
41 PLUGIN_FILE = "/etc/docker/plugins/openvswitch.spec"
42
43
44 def call_popen(cmd):
45     child = subprocess.Popen(cmd, stdout=subprocess.PIPE)
46     output = child.communicate()
47     if child.returncode:
48         raise RuntimeError("Fatal error executing %s" % (cmd))
49     if len(output) == 0 or output[0] == None:
50         output = ""
51     else:
52         output = output[0].strip()
53     return output
54
55
56 def call_prog(prog, args_list):
57     cmd = [prog, "--timeout=5", "-vconsole:off"] + args_list
58     return call_popen(cmd)
59
60
61 def ovs_vsctl(*args):
62     return call_prog("ovs-vsctl", list(args))
63
64
65 def ovn_nbctl(*args):
66     args_list = list(args)
67     database_option = "%s=%s" % ("--db", OVN_REMOTE)
68     args_list.insert(0, database_option)
69     return call_prog("ovn-nbctl", args_list)
70
71
72 def cleanup():
73     if os.path.isfile(PLUGIN_FILE):
74         os.remove(PLUGIN_FILE)
75
76
77 def ovn_init_overlay():
78     br_list = ovs_vsctl("list-br").split()
79     if OVN_BRIDGE not in br_list:
80         ovs_vsctl("--", "--may-exist", "add-br", OVN_BRIDGE,
81                   "--", "set", "bridge", OVN_BRIDGE,
82                   "external_ids:bridge-id=" + OVN_BRIDGE,
83                   "other-config:disable-in-band=true", "fail-mode=secure")
84
85     global OVN_REMOTE
86     OVN_REMOTE = ovs_vsctl("get", "Open_vSwitch", ".",
87                            "external_ids:ovn-remote").strip('"')
88     if not OVN_REMOTE:
89         sys.exit("OVN central database's ip address not set")
90
91     ovs_vsctl("set", "open_vswitch", ".",
92               "external_ids:ovn-bridge=" + OVN_BRIDGE)
93
94
95 def prepare():
96     parser = argparse.ArgumentParser()
97
98     ovs.vlog.add_args(parser)
99     ovs.daemon.add_args(parser)
100     args = parser.parse_args()
101     ovs.vlog.handle_args(args)
102     ovs.daemon.handle_args(args)
103     ovn_init_overlay()
104
105     if not os.path.isdir(PLUGIN_DIR):
106         os.makedirs(PLUGIN_DIR)
107
108     ovs.daemon.daemonize()
109     try:
110         fo = open(PLUGIN_FILE, "w")
111         fo.write("tcp://0.0.0.0:5000")
112         fo.close()
113     except Exception as e:
114         ovs.util.ovs_fatal(0, "Failed to write to spec file (%s)" % str(e),
115                            vlog)
116
117     atexit.register(cleanup)
118
119
120 @app.route('/Plugin.Activate', methods=['POST'])
121 def plugin_activate():
122     return jsonify({"Implements": ["NetworkDriver"]})
123
124
125 @app.route('/NetworkDriver.GetCapabilities', methods=['POST'])
126 def get_capability():
127     return jsonify({"Scope": "global"})
128
129
130 @app.route('/NetworkDriver.DiscoverNew', methods=['POST'])
131 def new_discovery():
132     return jsonify({})
133
134
135 @app.route('/NetworkDriver.DiscoverDelete', methods=['POST'])
136 def delete_discovery():
137     return jsonify({})
138
139
140 @app.route('/NetworkDriver.CreateNetwork', methods=['POST'])
141 def create_network():
142     if not request.data:
143         abort(400)
144
145     data = json.loads(request.data)
146
147     # NetworkID will have docker generated network uuid and it
148     # becomes 'name' in a OVN Logical switch record.
149     network = data.get("NetworkID", "")
150     if not network:
151         abort(400)
152
153     # Limit subnet handling to ipv4 till ipv6 usecase is clear.
154     ipv4_data = data.get("IPv4Data", "")
155     if not ipv4_data:
156         error = "create_network: No ipv4 subnet provided"
157         return jsonify({'Err': error})
158
159     subnet = ipv4_data[0].get("Pool", "")
160     if not subnet:
161         error = "create_network: no subnet in ipv4 data from libnetwork"
162         return jsonify({'Err': error})
163
164     gateway_ip = ipv4_data[0].get("Gateway", "").rsplit('/', 1)[0]
165     if not gateway_ip:
166         error = "create_network: no gateway in ipv4 data from libnetwork"
167         return jsonify({'Err': error})
168
169     try:
170         ovn_nbctl("lswitch-add", network, "--", "set", "Logical_Switch",
171                   network, "external_ids:subnet=" + subnet,
172                   "external_ids:gateway_ip=" + gateway_ip)
173     except Exception as e:
174         error = "create_network: lswitch-add %s" % (str(e))
175         return jsonify({'Err': error})
176
177     return jsonify({})
178
179
180 @app.route('/NetworkDriver.DeleteNetwork', methods=['POST'])
181 def delete_network():
182     if not request.data:
183         abort(400)
184
185     data = json.loads(request.data)
186
187     nid = data.get("NetworkID", "")
188     if not nid:
189         abort(400)
190
191     try:
192         ovn_nbctl("lswitch-del", nid)
193     except Exception as e:
194         error = "delete_network: lswitch-del %s" % (str(e))
195         return jsonify({'Err': error})
196
197     return jsonify({})
198
199
200 @app.route('/NetworkDriver.CreateEndpoint', methods=['POST'])
201 def create_endpoint():
202     if not request.data:
203         abort(400)
204
205     data = json.loads(request.data)
206
207     nid = data.get("NetworkID", "")
208     if not nid:
209         abort(400)
210
211     eid = data.get("EndpointID", "")
212     if not eid:
213         abort(400)
214
215     interface = data.get("Interface", "")
216     if not interface:
217         error = "create_endpoint: no interfaces structure supplied by " \
218                 "libnetwork"
219         return jsonify({'Err': error})
220
221     ip_address_and_mask = interface.get("Address", "")
222     if not ip_address_and_mask:
223         error = "create_endpoint: ip address not provided by libnetwork"
224         return jsonify({'Err': error})
225
226     ip_address = ip_address_and_mask.rsplit('/', 1)[0]
227     mac_address_input = interface.get("MacAddress", "")
228     mac_address_output = ""
229
230     try:
231         ovn_nbctl("lport-add", nid, eid)
232     except Exception as e:
233         error = "create_endpoint: lport-add (%s)" % (str(e))
234         return jsonify({'Err': error})
235
236     if not mac_address_input:
237         mac_address = "02:%02x:%02x:%02x:%02x:%02x" % (random.randint(0, 255),
238                                                        random.randint(0, 255),
239                                                        random.randint(0, 255),
240                                                        random.randint(0, 255),
241                                                        random.randint(0, 255))
242     else:
243         mac_address = mac_address_input
244
245     try:
246         ovn_nbctl("lport-set-addresses", eid,
247                   mac_address + " " + ip_address)
248     except Exception as e:
249         error = "create_endpoint: lport-set-addresses (%s)" % (str(e))
250         return jsonify({'Err': error})
251
252     # Only return a mac address if one did not come as request.
253     mac_address_output = ""
254     if not mac_address_input:
255         mac_address_output = mac_address
256
257     return jsonify({"Interface": {
258                                     "Address": "",
259                                     "AddressIPv6": "",
260                                     "MacAddress": mac_address_output
261                                     }})
262
263
264 def get_logical_port_addresses(eid):
265     ret = ovn_nbctl("--if-exists", "get", "Logical_port", eid, "addresses")
266     if not ret:
267         error = "endpoint not found in OVN database"
268         return (None, None, error)
269     addresses = ast.literal_eval(ret)
270     if len(addresses) == 0:
271         error = "unexpected return while fetching addresses"
272         return (None, None, error)
273     (mac_address, ip_address) = addresses[0].split()
274     return (mac_address, ip_address, None)
275
276
277 @app.route('/NetworkDriver.EndpointOperInfo', methods=['POST'])
278 def show_endpoint():
279     if not request.data:
280         abort(400)
281
282     data = json.loads(request.data)
283
284     nid = data.get("NetworkID", "")
285     if not nid:
286         abort(400)
287
288     eid = data.get("EndpointID", "")
289     if not eid:
290         abort(400)
291
292     try:
293         (mac_address, ip_address, error) = get_logical_port_addresses(eid)
294         if error:
295             jsonify({'Err': error})
296     except Exception as e:
297         error = "show_endpoint: get Logical_port addresses. (%s)" % (str(e))
298         return jsonify({'Err': error})
299
300     veth_outside = eid[0:15]
301     return jsonify({"Value": {"ip_address": ip_address,
302                               "mac_address": mac_address,
303                               "veth_outside": veth_outside
304                               }})
305
306
307 @app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
308 def delete_endpoint():
309     if not request.data:
310         abort(400)
311
312     data = json.loads(request.data)
313
314     nid = data.get("NetworkID", "")
315     if not nid:
316         abort(400)
317
318     eid = data.get("EndpointID", "")
319     if not eid:
320         abort(400)
321
322     try:
323         ovn_nbctl("lport-del", eid)
324     except Exception as e:
325         error = "delete_endpoint: lport-del %s" % (str(e))
326         return jsonify({'Err': error})
327
328     return jsonify({})
329
330
331 @app.route('/NetworkDriver.Join', methods=['POST'])
332 def network_join():
333     if not request.data:
334         abort(400)
335
336     data = json.loads(request.data)
337
338     nid = data.get("NetworkID", "")
339     if not nid:
340         abort(400)
341
342     eid = data.get("EndpointID", "")
343     if not eid:
344         abort(400)
345
346     sboxkey = data.get("SandboxKey", "")
347     if not sboxkey:
348         abort(400)
349
350     # sboxkey is of the form: /var/run/docker/netns/CONTAINER_ID
351     vm_id = sboxkey.rsplit('/')[-1]
352
353     try:
354         (mac_address, ip_address, error) = get_logical_port_addresses(eid)
355         if error:
356             jsonify({'Err': error})
357     except Exception as e:
358         error = "network_join: %s" % (str(e))
359         return jsonify({'Err': error})
360
361     veth_outside = eid[0:15]
362     veth_inside = eid[0:13] + "_c"
363     command = "ip link add %s type veth peer name %s" \
364               % (veth_inside, veth_outside)
365     try:
366         call_popen(shlex.split(command))
367     except Exception as e:
368         error = "network_join: failed to create veth pair (%s)" % (str(e))
369         return jsonify({'Err': error})
370
371     command = "ip link set dev %s address %s" \
372               % (veth_inside, mac_address)
373
374     try:
375         call_popen(shlex.split(command))
376     except Exception as e:
377         error = "network_join: failed to set veth mac address (%s)" % (str(e))
378         return jsonify({'Err': error})
379
380     command = "ip link set %s up" % (veth_outside)
381
382     try:
383         call_popen(shlex.split(command))
384     except Exception as e:
385         error = "network_join: failed to up the veth interface (%s)" % (str(e))
386         return jsonify({'Err': error})
387
388     try:
389         ovs_vsctl("add-port", OVN_BRIDGE, veth_outside)
390         ovs_vsctl("set", "interface", veth_outside,
391                   "external_ids:attached-mac=" + mac_address,
392                   "external_ids:iface-id=" + eid,
393                   "external_ids:vm-id=" + vm_id,
394                   "external_ids:iface-status=active")
395     except Exception as e:
396         error = "network_join: failed to create a port (%s)" % (str(e))
397         return jsonify({'Err': error})
398
399     return jsonify({"InterfaceName": {
400                                         "SrcName": veth_inside,
401                                         "DstPrefix": "eth"
402                                      },
403                     "Gateway": "",
404                     "GatewayIPv6": ""})
405
406
407 @app.route('/NetworkDriver.Leave', methods=['POST'])
408 def network_leave():
409     if not request.data:
410         abort(400)
411
412     data = json.loads(request.data)
413
414     nid = data.get("NetworkID", "")
415     if not nid:
416         abort(400)
417
418     eid = data.get("EndpointID", "")
419     if not eid:
420         abort(400)
421
422     veth_outside = eid[0:15]
423     command = "ip link delete %s" % (veth_outside)
424     try:
425         call_popen(shlex.split(command))
426     except Exception as e:
427         error = "network_leave: failed to delete veth pair (%s)" % (str(e))
428         return jsonify({'Err': error})
429
430     try:
431         ovs_vsctl("--if-exists", "del-port", veth_outside)
432     except Exception as e:
433         error = "network_leave: failed to delete port (%s)" % (str(e))
434         return jsonify({'Err': error})
435
436     return jsonify({})
437
438 if __name__ == '__main__':
439     prepare()
440     app.run(host='0.0.0.0')