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