ovs-vtep: Clear left-over local mac information.
[cascardo/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             vtep_ctl("clear-local-macs %s" % Lswitches[ls_name].name)
428             del Lswitches[ls_name]
429
430 def setup(ps_name):
431     br_list = ovs_vsctl("list-br").split()
432     if (ps_name not in br_list):
433         ovs.util.ovs_fatal(0, "couldn't find OVS bridge %s" % ps_name, vlog)
434
435     call_prog("vtep-ctl", ["set", "physical_switch", ps_name,
436                            'description="OVS VTEP Emulator"'])
437
438     tunnel_ips = vtep_ctl("get physical_switch %s tunnel_ips"
439                           % ps_name).strip('[]"').split(", ")
440     if len(tunnel_ips) != 1 or not tunnel_ips[0]:
441         ovs.util.ovs_fatal(0, "exactly one 'tunnel_ips' should be set", vlog)
442
443     global Tunnel_Ip
444     Tunnel_Ip = tunnel_ips[0]
445
446     ovs_ofctl("del-flows %s" % ps_name)
447
448     # Remove any logical bridges from the previous run
449     for br in br_list:
450         if ovs_vsctl("br-get-external-id %s vtep_logical_switch"
451                      % br) == "true":
452             # Remove the remote side of any logical switch
453             ovs_ports = ovs_vsctl("list-ports %s" % br).split()
454             for port in ovs_ports:
455                 port_type = ovs_vsctl("get Interface %s type"
456                                       % port).strip('"')
457                 if port_type != "patch":
458                     continue
459
460                 peer = ovs_vsctl("get Interface %s options:peer"
461                                  % port).strip('"')
462                 if (peer):
463                     ovs_vsctl("del-port %s" % peer)
464
465             ovs_vsctl("del-br %s" % br)
466
467
468 def main():
469     parser = argparse.ArgumentParser()
470     parser.add_argument("ps_name", metavar="PS-NAME",
471                         help="Name of physical switch.")
472     parser.add_argument("--root-prefix", metavar="DIR",
473                         help="Use DIR as alternate root directory"
474                         " (for testing).")
475     parser.add_argument("--version", action="version",
476                         version="%s %s" % (ovs.util.PROGRAM_NAME, VERSION))
477
478     ovs.vlog.add_args(parser)
479     ovs.daemon.add_args(parser)
480     args = parser.parse_args()
481     ovs.vlog.handle_args(args)
482     ovs.daemon.handle_args(args)
483
484     global root_prefix
485     if args.root_prefix:
486         root_prefix = args.root_prefix
487
488     ps_name = args.ps_name
489
490     ovs.daemon.daemonize()
491
492     ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
493     error, unixctl = ovs.unixctl.server.UnixctlServer.create(None,
494                                                              version=VERSION)
495     if error:
496         ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
497
498     setup(ps_name)
499
500     while True:
501         unixctl.run()
502         if exiting:
503             break
504
505         handle_physical(ps_name)
506
507         for ls_name, ls in Lswitches.items():
508             ls.run()
509
510         poller = ovs.poller.Poller()
511         unixctl.wait(poller)
512         poller.timer_wait(1000)
513         poller.block()
514
515     unixctl.close()
516
517 if __name__ == '__main__':
518     try:
519         main()
520     except SystemExit:
521         # Let system.exit() calls complete normally
522         raise
523     except:
524         vlog.exception("traceback")
525         sys.exit(ovs.daemon.RESTART_EXIT_CODE)