ovn-nbctl: Update logical switch commands.
[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_NB = ""
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_NB)
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_NB
86     OVN_NB = ovs_vsctl("get", "Open_vSwitch", ".",
87                            "external_ids:ovn-nb").strip('"')
88     if not OVN_NB:
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("ls-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: ls-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("ls-del", nid)
193     except Exception as e:
194         error = "delete_network: ls-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("lsp-add", nid, eid)
232     except Exception as e:
233         error = "create_endpoint: lsp-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("lsp-set-addresses", eid,
247                   mac_address + " " + ip_address)
248     except Exception as e:
249         error = "create_endpoint: lsp-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_lsp_addresses(eid):
265     ret = ovn_nbctl("--if-exists", "get", "Logical_Switch_Port", eid,
266                     "addresses")
267     if not ret:
268         error = "endpoint not found in OVN database"
269         return (None, None, error)
270     addresses = ast.literal_eval(ret)
271     if len(addresses) == 0:
272         error = "unexpected return while fetching addresses"
273         return (None, None, error)
274     (mac_address, ip_address) = addresses[0].split()
275     return (mac_address, ip_address, None)
276
277
278 @app.route('/NetworkDriver.EndpointOperInfo', methods=['POST'])
279 def show_endpoint():
280     if not request.data:
281         abort(400)
282
283     data = json.loads(request.data)
284
285     nid = data.get("NetworkID", "")
286     if not nid:
287         abort(400)
288
289     eid = data.get("EndpointID", "")
290     if not eid:
291         abort(400)
292
293     try:
294         (mac_address, ip_address, error) = get_lsp_addresses(eid)
295         if error:
296             jsonify({'Err': error})
297     except Exception as e:
298         error = "show_endpoint: get Logical_Switch_Port addresses. (%s)" \
299                 % (str(e))
300         return jsonify({'Err': error})
301
302     veth_outside = eid[0:15]
303     return jsonify({"Value": {"ip_address": ip_address,
304                               "mac_address": mac_address,
305                               "veth_outside": veth_outside
306                               }})
307
308
309 @app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
310 def delete_endpoint():
311     if not request.data:
312         abort(400)
313
314     data = json.loads(request.data)
315
316     nid = data.get("NetworkID", "")
317     if not nid:
318         abort(400)
319
320     eid = data.get("EndpointID", "")
321     if not eid:
322         abort(400)
323
324     try:
325         ovn_nbctl("lsp-del", eid)
326     except Exception as e:
327         error = "delete_endpoint: lsp-del %s" % (str(e))
328         return jsonify({'Err': error})
329
330     return jsonify({})
331
332
333 @app.route('/NetworkDriver.Join', methods=['POST'])
334 def network_join():
335     if not request.data:
336         abort(400)
337
338     data = json.loads(request.data)
339
340     nid = data.get("NetworkID", "")
341     if not nid:
342         abort(400)
343
344     eid = data.get("EndpointID", "")
345     if not eid:
346         abort(400)
347
348     sboxkey = data.get("SandboxKey", "")
349     if not sboxkey:
350         abort(400)
351
352     # sboxkey is of the form: /var/run/docker/netns/CONTAINER_ID
353     vm_id = sboxkey.rsplit('/')[-1]
354
355     try:
356         (mac_address, ip_address, error) = get_lsp_addresses(eid)
357         if error:
358             jsonify({'Err': error})
359     except Exception as e:
360         error = "network_join: %s" % (str(e))
361         return jsonify({'Err': error})
362
363     veth_outside = eid[0:15]
364     veth_inside = eid[0:13] + "_c"
365     command = "ip link add %s type veth peer name %s" \
366               % (veth_inside, veth_outside)
367     try:
368         call_popen(shlex.split(command))
369     except Exception as e:
370         error = "network_join: failed to create veth pair (%s)" % (str(e))
371         return jsonify({'Err': error})
372
373     command = "ip link set dev %s address %s" \
374               % (veth_inside, mac_address)
375
376     try:
377         call_popen(shlex.split(command))
378     except Exception as e:
379         error = "network_join: failed to set veth mac address (%s)" % (str(e))
380         return jsonify({'Err': error})
381
382     command = "ip link set %s up" % (veth_outside)
383
384     try:
385         call_popen(shlex.split(command))
386     except Exception as e:
387         error = "network_join: failed to up the veth interface (%s)" % (str(e))
388         return jsonify({'Err': error})
389
390     try:
391         ovs_vsctl("add-port", OVN_BRIDGE, veth_outside)
392         ovs_vsctl("set", "interface", veth_outside,
393                   "external_ids:attached-mac=" + mac_address,
394                   "external_ids:iface-id=" + eid,
395                   "external_ids:vm-id=" + vm_id,
396                   "external_ids:iface-status=active")
397     except Exception as e:
398         error = "network_join: failed to create a port (%s)" % (str(e))
399         return jsonify({'Err': error})
400
401     return jsonify({"InterfaceName": {
402                                         "SrcName": veth_inside,
403                                         "DstPrefix": "eth"
404                                      },
405                     "Gateway": "",
406                     "GatewayIPv6": ""})
407
408
409 @app.route('/NetworkDriver.Leave', methods=['POST'])
410 def network_leave():
411     if not request.data:
412         abort(400)
413
414     data = json.loads(request.data)
415
416     nid = data.get("NetworkID", "")
417     if not nid:
418         abort(400)
419
420     eid = data.get("EndpointID", "")
421     if not eid:
422         abort(400)
423
424     veth_outside = eid[0:15]
425     command = "ip link delete %s" % (veth_outside)
426     try:
427         call_popen(shlex.split(command))
428     except Exception as e:
429         error = "network_leave: failed to delete veth pair (%s)" % (str(e))
430         return jsonify({'Err': error})
431
432     try:
433         ovs_vsctl("--if-exists", "del-port", veth_outside)
434     except Exception as e:
435         error = "network_leave: failed to delete port (%s)" % (str(e))
436         return jsonify({'Err': error})
437
438     return jsonify({})
439
440 if __name__ == '__main__':
441     prepare()
442     app.run(host='0.0.0.0')