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