]>
Commit | Line | Data |
---|---|---|
eaa923e3 GS |
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" | |
d61fbedc | 39 | OVN_NB = "" |
eaa923e3 GS |
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) | |
d61fbedc | 67 | database_option = "%s=%s" % ("--db", OVN_NB) |
eaa923e3 GS |
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 | ||
d61fbedc GS |
85 | global OVN_NB |
86 | OVN_NB = ovs_vsctl("get", "Open_vSwitch", ".", | |
87 | "external_ids:ovn-nb").strip('"') | |
88 | if not OVN_NB: | |
eaa923e3 GS |
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: | |
ea46a4e9 | 170 | ovn_nbctl("ls-add", network, "--", "set", "Logical_Switch", |
eaa923e3 GS |
171 | network, "external_ids:subnet=" + subnet, |
172 | "external_ids:gateway_ip=" + gateway_ip) | |
173 | except Exception as e: | |
ea46a4e9 | 174 | error = "create_network: ls-add %s" % (str(e)) |
eaa923e3 GS |
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: | |
ea46a4e9 | 192 | ovn_nbctl("ls-del", nid) |
eaa923e3 | 193 | except Exception as e: |
ea46a4e9 | 194 | error = "delete_network: ls-del %s" % (str(e)) |
eaa923e3 GS |
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: | |
31ed1192 | 231 | ovn_nbctl("lsp-add", nid, eid) |
eaa923e3 | 232 | except Exception as e: |
31ed1192 | 233 | error = "create_endpoint: lsp-add (%s)" % (str(e)) |
eaa923e3 GS |
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: | |
31ed1192 | 246 | ovn_nbctl("lsp-set-addresses", eid, |
eaa923e3 GS |
247 | mac_address + " " + ip_address) |
248 | except Exception as e: | |
31ed1192 | 249 | error = "create_endpoint: lsp-set-addresses (%s)" % (str(e)) |
eaa923e3 GS |
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 | ||
80f408f4 JP |
264 | def get_lsp_addresses(eid): |
265 | ret = ovn_nbctl("--if-exists", "get", "Logical_Switch_Port", eid, | |
266 | "addresses") | |
eaa923e3 GS |
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: | |
80f408f4 | 294 | (mac_address, ip_address, error) = get_lsp_addresses(eid) |
eaa923e3 GS |
295 | if error: |
296 | jsonify({'Err': error}) | |
297 | except Exception as e: | |
31ed1192 JP |
298 | error = "show_endpoint: get Logical_Switch_Port addresses. (%s)" \ |
299 | % (str(e)) | |
eaa923e3 GS |
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: | |
31ed1192 | 325 | ovn_nbctl("lsp-del", eid) |
eaa923e3 | 326 | except Exception as e: |
31ed1192 | 327 | error = "delete_endpoint: lsp-del %s" % (str(e)) |
eaa923e3 GS |
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: | |
80f408f4 | 356 | (mac_address, ip_address, error) = get_lsp_addresses(eid) |
eaa923e3 GS |
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') |