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