]> git.proxmox.com Git - ovs.git/blob - vtep/ovs-vtep
93af9a95567a1e9f654f1b555028c1604ca73671
[ovs.git] / vtep / 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 import argparse
20 import re
21 import subprocess
22 import sys
23 import time
24 import types
25
26 import ovs.dirs
27 import ovs.util
28 import ovs.daemon
29 import ovs.unixctl.server
30 import ovs.vlog
31
32
33 VERSION = "0.99"
34
35 root_prefix = ""
36
37 __pychecker__ = 'no-reuseattr' # Remove in pychecker >= 0.8.19.
38 vlog = ovs.vlog.Vlog("ovs-vtep")
39 exiting = False
40
41 Tunnel_Ip = ""
42 Lswitches = {}
43 Bindings = {}
44 ls_count = 0
45 tun_id = 0
46
47 def call_prog(prog, args_list):
48 cmd = [prog, "-vconsole:off"] + args_list
49 output = subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()
50 if len(output) == 0 or output[0] == None:
51 output = ""
52 else:
53 output = output[0].strip()
54 return output
55
56 def ovs_vsctl(args):
57 return call_prog("ovs-vsctl", args.split())
58
59 def ovs_ofctl(args):
60 return call_prog("ovs-ofctl", args.split())
61
62 def vtep_ctl(args):
63 return call_prog("vtep-ctl", args.split())
64
65
66 def unixctl_exit(conn, unused_argv, unused_aux):
67 global exiting
68 exiting = True
69 conn.reply(None)
70
71
72 class Logical_Switch(object):
73 def __init__(self, ls_name):
74 global ls_count
75 self.name = ls_name
76 ls_count += 1
77 self.short_name = "vtep_ls" + str(ls_count)
78 vlog.info("creating lswitch %s (%s)" % (self.name, self.short_name))
79 self.ports = {}
80 self.tunnels = {}
81 self.local_macs = set()
82 self.remote_macs = {}
83 self.unknown_dsts = set()
84 self.tunnel_key = 0
85 self.setup_ls()
86
87 def __del__(self):
88 vlog.info("destroying lswitch %s" % self.name)
89
90 def setup_ls(self):
91 column = vtep_ctl("--columns=tunnel_key find logical_switch "
92 "name=%s" % self.name)
93 tunnel_key = column.partition(":")[2].strip()
94 if (tunnel_key and type(eval(tunnel_key)) == types.IntType):
95 self.tunnel_key = tunnel_key
96 vlog.info("using tunnel key %s in %s"
97 % (self.tunnel_key, self.name))
98 else:
99 self.tunnel_key = 0
100 vlog.warn("invalid tunnel key for %s, using 0" % self.name)
101
102 ovs_vsctl("--may-exist add-br %s" % self.short_name)
103 ovs_vsctl("br-set-external-id %s vtep_logical_switch true"
104 % self.short_name)
105 ovs_vsctl("br-set-external-id %s logical_switch_name %s"
106 % (self.short_name, self.name))
107
108 vtep_ctl("clear-local-macs %s" % self.name)
109 vtep_ctl("add-mcast-local %s unknown-dst %s" % (self.name, Tunnel_Ip))
110
111 ovs_ofctl("del-flows %s" % self.short_name)
112 ovs_ofctl("add-flow %s priority=0,action=drop" % self.short_name)
113
114 def update_flood(self):
115 flood_ports = self.ports.values()
116
117 for tunnel in self.unknown_dsts:
118 port_no = self.tunnels[tunnel][0]
119 flood_ports.append(port_no)
120
121 ovs_ofctl("add-flow %s table=1,priority=0,action=%s"
122 % (self.short_name, ",".join(flood_ports)))
123
124 def add_lbinding(self, lbinding):
125 vlog.info("adding %s binding to %s" % (lbinding, self.name))
126 port_no = ovs_vsctl("get Interface %s ofport" % lbinding)
127 self.ports[lbinding] = port_no
128 ovs_ofctl("add-flow %s in_port=%s,action=learn(table=1,"
129 "priority=1000,idle_timeout=15,cookie=0x5000,"
130 "NXM_OF_ETH_DST[]=NXM_OF_ETH_SRC[],"
131 "output:NXM_OF_IN_PORT[]),resubmit(,1)"
132 % (self.short_name, port_no))
133
134 self.update_flood()
135
136 def del_lbinding(self, lbinding):
137 vlog.info("removing %s binding from %s" % (lbinding, self.name))
138 port_no = self.ports[lbinding]
139 ovs_ofctl("del-flows %s in_port=%s" % (self.short_name, port_no));
140 del self.ports[lbinding]
141 self.update_flood()
142
143 def add_tunnel(self, tunnel):
144 global tun_id
145 vlog.info("adding tunnel %s" % tunnel)
146 encap, ip = tunnel.split("/")
147
148 if encap != "vxlan_over_ipv4":
149 vlog.warn("unsupported tunnel format %s" % encap)
150 return
151
152 tun_id += 1
153 tun_name = "vx" + str(tun_id)
154
155 ovs_vsctl("add-port %s %s -- set Interface %s type=vxlan "
156 "options:key=%s options:remote_ip=%s"
157 % (self.short_name, tun_name, tun_name, self.tunnel_key, ip))
158
159 for i in range(10):
160 port_no = ovs_vsctl("get Interface %s ofport" % tun_name)
161 if port_no != "-1":
162 break
163 elif i == 9:
164 vlog.warn("couldn't create tunnel %s" % tunnel)
165 ovs_vsctl("del-port %s %s" % (self.short_name, tun_name))
166 return
167
168 # Give the system a moment to allocate the port number
169 time.sleep(0.5)
170
171 self.tunnels[tunnel] = (port_no, tun_name)
172
173 ovs_ofctl("add-flow %s table=0,priority=1000,in_port=%s,"
174 "actions=resubmit(,1)"
175 % (self.short_name, port_no))
176
177 def del_tunnel(self, tunnel):
178 vlog.info("removing tunnel %s" % tunnel)
179
180 port_no, tun_name = self.tunnels[tunnel]
181 ovs_ofctl("del-flows %s table=0,in_port=%s"
182 % (self.short_name, port_no))
183 ovs_vsctl("del-port %s %s" % (self.short_name, tun_name))
184
185 del self.tunnels[tunnel]
186
187 def update_local_macs(self):
188 flows = ovs_ofctl("dump-flows %s cookie=0x5000/-1,table=1"
189 % self.short_name).splitlines()
190 macs = set()
191 for f in flows:
192 mac = re.split(r'.*dl_dst=(.*) .*', f)
193 if len(mac) == 3:
194 macs.add(mac[1])
195
196 for mac in macs.difference(self.local_macs):
197 vlog.info("adding local ucast %s to %s" % (mac, self.name))
198 vtep_ctl("add-ucast-local %s %s %s" % (self.name, mac, Tunnel_Ip))
199
200 for mac in self.local_macs.difference(macs):
201 vlog.info("removing local ucast %s from %s" % (mac, self.name))
202 vtep_ctl("del-ucast-local %s %s" % (self.name, mac))
203
204 self.local_macs = macs
205
206 def add_remote_mac(self, mac, tunnel):
207 port_no = self.tunnels.get(tunnel, (0,""))[0]
208 if not port_no:
209 return
210
211 ovs_ofctl("add-flow %s table=1,priority=1000,dl_dst=%s,action=%s"
212 % (self.short_name, mac, port_no))
213
214 def del_remote_mac(self, mac):
215 ovs_ofctl("del-flows %s table=1,dl_dst=%s" % (self.short_name, mac))
216
217 def update_remote_macs(self):
218 remote_macs = {}
219 unknown_dsts = set()
220 tunnels = set()
221 parse_ucast = True
222
223 mac_list = vtep_ctl("list-remote-macs %s" % self.name).splitlines()
224 for line in mac_list:
225 if (line.find("mcast-mac-remote") != -1):
226 parse_ucast = False
227 continue
228
229 entry = re.split(r' (.*) -> (.*)', line)
230 if len(entry) != 4:
231 continue
232
233 if parse_ucast:
234 remote_macs[entry[1]] = entry[2]
235 else:
236 if entry[1] != "unknown-dst":
237 continue
238
239 unknown_dsts.add(entry[2])
240
241 tunnels.add(entry[2])
242
243 old_tunnels = set(self.tunnels.keys())
244
245 for tunnel in tunnels.difference(old_tunnels):
246 self.add_tunnel(tunnel)
247
248 for tunnel in old_tunnels.difference(tunnels):
249 self.del_tunnel(tunnel)
250
251 for mac in remote_macs.keys():
252 if (self.remote_macs.get(mac) != remote_macs[mac]):
253 self.add_remote_mac(mac, remote_macs[mac])
254
255 for mac in self.remote_macs.keys():
256 if not remote_macs.has_key(mac):
257 self.del_remote_mac(mac)
258
259 self.remote_macs = remote_macs
260
261 if (self.unknown_dsts != unknown_dsts):
262 self.unknown_dsts = unknown_dsts
263 self.update_flood()
264
265 def update_stats(self):
266 # Map Open_vSwitch's "interface:statistics" to columns of
267 # vtep's logical_binding_stats. Since we are using the 'interface' from
268 # the logical switch to collect stats, packets transmitted from it
269 # is received in the physical switch and vice versa.
270 stats_map = {'tx_packets':'packets_to_local',
271 'tx_bytes':'bytes_to_local',
272 'rx_packets':'packets_from_local',
273 'rx_bytes':'bytes_from_local'}
274
275 # Go through all the logical switch's interfaces that end with "-l"
276 # and copy the statistics to logical_binding_stats.
277 for interface in self.ports.iterkeys():
278 if not interface.endswith("-l"):
279 continue
280 vlan, pp_name, logical = interface.split("-")
281 uuid = vtep_ctl("get physical_port %s vlan_stats:%s"
282 % (pp_name, vlan))
283 if not uuid:
284 continue
285
286 for (mapfrom, mapto) in stats_map.iteritems():
287 value = ovs_vsctl("get interface %s statistics:%s"
288 % (interface, mapfrom)).strip('"')
289 vtep_ctl("set logical_binding_stats %s %s=%s"
290 % (uuid, mapto, value))
291
292 def run(self):
293 self.update_local_macs()
294 self.update_remote_macs()
295 self.update_stats()
296
297 def add_binding(ps_name, binding, ls):
298 vlog.info("adding binding %s" % binding)
299
300 vlan, pp_name = binding.split("-")
301 pbinding = binding+"-p"
302 lbinding = binding+"-l"
303
304 # Create a patch port that connects the VLAN+port to the lswitch.
305 # Do them as two separate calls so if one side already exists, the
306 # other side is created.
307 ovs_vsctl("add-port %s %s "
308 " -- set Interface %s type=patch options:peer=%s"
309 % (ps_name, pbinding, pbinding, lbinding))
310 ovs_vsctl("add-port %s %s "
311 " -- set Interface %s type=patch options:peer=%s"
312 % (ls.short_name, lbinding, lbinding, pbinding))
313
314 port_no = ovs_vsctl("get Interface %s ofport" % pp_name)
315 patch_no = ovs_vsctl("get Interface %s ofport" % pbinding)
316 vlan_ = vlan.lstrip('0')
317 if vlan_:
318 ovs_ofctl("add-flow %s in_port=%s,dl_vlan=%s,action=strip_vlan,%s"
319 % (ps_name, port_no, vlan_, patch_no))
320 ovs_ofctl("add-flow %s in_port=%s,action=mod_vlan_vid:%s,%s"
321 % (ps_name, patch_no, vlan_, port_no))
322 else:
323 ovs_ofctl("add-flow %s in_port=%s,action=%s"
324 % (ps_name, port_no, patch_no))
325 ovs_ofctl("add-flow %s in_port=%s,action=%s"
326 % (ps_name, patch_no, port_no))
327
328 # Create a logical_bindings_stats record.
329 if not vlan_:
330 vlan_ = "0"
331 vtep_ctl("set physical_port %s vlan_stats:%s=@stats --\
332 --id=@stats create logical_binding_stats packets_from_local=0"\
333 % (pp_name, vlan_))
334
335 ls.add_lbinding(lbinding)
336 Bindings[binding] = ls.name
337
338 def del_binding(ps_name, binding, ls):
339 vlog.info("removing binding %s" % binding)
340
341 vlan, pp_name = binding.split("-")
342 pbinding = binding+"-p"
343 lbinding = binding+"-l"
344
345 port_no = ovs_vsctl("get Interface %s ofport" % pp_name)
346 patch_no = ovs_vsctl("get Interface %s ofport" % pbinding)
347 vlan_ = vlan.lstrip('0')
348 if vlan_:
349 ovs_ofctl("del-flows %s in_port=%s,dl_vlan=%s"
350 % (ps_name, port_no, vlan_))
351 ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no))
352 else:
353 ovs_ofctl("del-flows %s in_port=%s" % (ps_name, port_no))
354 ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no))
355
356 ls.del_lbinding(lbinding)
357
358 # Destroy the patch port that connects the VLAN+port to the lswitch
359 ovs_vsctl("del-port %s %s -- del-port %s %s"
360 % (ps_name, pbinding, ls.short_name, lbinding))
361
362 # Remove the record that links vlan with stats in logical_binding_stats.
363 vtep_ctl("remove physical_port %s vlan_stats %s" % (pp_name, vlan))
364
365 del Bindings[binding]
366
367 def handle_physical(ps_name):
368 # Gather physical ports except the patch ports we created
369 ovs_ports = ovs_vsctl("list-ports %s" % ps_name).split()
370 ovs_port_set = set([port for port in ovs_ports if port[-2:] != "-p"])
371
372 vtep_pp_set = set(vtep_ctl("list-ports %s" % ps_name).split())
373
374 for pp_name in ovs_port_set.difference(vtep_pp_set):
375 vlog.info("adding %s to %s" % (pp_name, ps_name))
376 vtep_ctl("add-port %s %s" % (ps_name, pp_name))
377
378 for pp_name in vtep_pp_set.difference(ovs_port_set):
379 vlog.info("deleting %s from %s" % (pp_name, ps_name))
380 vtep_ctl("del-port %s %s" % (ps_name, pp_name))
381
382 new_bindings = set()
383 for pp_name in vtep_pp_set:
384 binding_set = set(vtep_ctl("list-bindings %s %s"
385 % (ps_name, pp_name)).splitlines())
386
387 for b in binding_set:
388 vlan, ls_name = b.split()
389 if ls_name not in Lswitches:
390 Lswitches[ls_name] = Logical_Switch(ls_name)
391
392 binding = "%s-%s" % (vlan, pp_name)
393 ls = Lswitches[ls_name]
394 new_bindings.add(binding)
395
396 if Bindings.has_key(binding):
397 if Bindings[binding] == ls_name:
398 continue
399 else:
400 del_binding(ps_name, binding, Lswitches[Bindings[binding]])
401
402 add_binding(ps_name, binding, ls)
403
404
405 dead_bindings = set(Bindings.keys()).difference(new_bindings)
406 for binding in dead_bindings:
407 ls_name = Bindings[binding]
408 ls = Lswitches[ls_name]
409
410 del_binding(ps_name, binding, ls)
411
412 if not len(ls.ports):
413 ovs_vsctl("del-br %s" % Lswitches[ls_name].short_name)
414 del Lswitches[ls_name]
415
416 def setup(ps_name):
417 br_list = ovs_vsctl("list-br").split()
418 if (ps_name not in br_list):
419 ovs.util.ovs_fatal(0, "couldn't find OVS bridge %s" % ps_name, vlog)
420
421 call_prog("vtep-ctl", ["set", "physical_switch", ps_name,
422 'description="OVS VTEP Emulator"'])
423
424 tunnel_ips = vtep_ctl("get physical_switch %s tunnel_ips"
425 % ps_name).strip('[]"').split(", ")
426 if len(tunnel_ips) != 1 or not tunnel_ips[0]:
427 ovs.util.ovs_fatal(0, "exactly one 'tunnel_ips' should be set", vlog)
428
429 global Tunnel_Ip
430 Tunnel_Ip = tunnel_ips[0]
431
432 ovs_ofctl("del-flows %s" % ps_name)
433
434 # Remove any logical bridges from the previous run
435 for br in br_list:
436 if ovs_vsctl("br-get-external-id %s vtep_logical_switch"
437 % br) == "true":
438 # Remove the remote side of any logical switch
439 ovs_ports = ovs_vsctl("list-ports %s" % br).split()
440 for port in ovs_ports:
441 port_type = ovs_vsctl("get Interface %s type"
442 % port).strip('"')
443 if port_type != "patch":
444 continue
445
446 peer = ovs_vsctl("get Interface %s options:peer"
447 % port).strip('"')
448 if (peer):
449 ovs_vsctl("del-port %s" % peer)
450
451 ovs_vsctl("del-br %s" % br)
452
453
454 def main():
455 parser = argparse.ArgumentParser()
456 parser.add_argument("ps_name", metavar="PS-NAME",
457 help="Name of physical switch.")
458 parser.add_argument("--root-prefix", metavar="DIR",
459 help="Use DIR as alternate root directory"
460 " (for testing).")
461 parser.add_argument("--version", action="version",
462 version="%s %s" % (ovs.util.PROGRAM_NAME, VERSION))
463
464 ovs.vlog.add_args(parser)
465 ovs.daemon.add_args(parser)
466 args = parser.parse_args()
467 ovs.vlog.handle_args(args)
468 ovs.daemon.handle_args(args)
469
470 global root_prefix
471 if args.root_prefix:
472 root_prefix = args.root_prefix
473
474 ps_name = args.ps_name
475
476 ovs.daemon.daemonize()
477
478 ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
479 error, unixctl = ovs.unixctl.server.UnixctlServer.create(None,
480 version=VERSION)
481 if error:
482 ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
483
484 setup(ps_name)
485
486 while True:
487 unixctl.run()
488 if exiting:
489 break
490
491 handle_physical(ps_name)
492
493 for ls_name, ls in Lswitches.items():
494 ls.run()
495
496 poller = ovs.poller.Poller()
497 unixctl.wait(poller)
498 poller.timer_wait(1000)
499 poller.block()
500
501 unixctl.close()
502
503 if __name__ == '__main__':
504 try:
505 main()
506 except SystemExit:
507 # Let system.exit() calls complete normally
508 raise
509 except:
510 vlog.exception("traceback")
511 sys.exit(ovs.daemon.RESTART_EXIT_CODE)