netdev: Change netdev_class_rwlock to recursive mutex, for POSIX safety.
[sliver-openvswitch.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             vlan, pp_name, logical = interface.split("-")
292             uuid = vtep_ctl("get physical_port %s vlan_stats:%s"
293                             % (pp_name, vlan))
294             if not uuid:
295                 continue
296
297             for (mapfrom, mapto) in stats_map.iteritems():
298                 value = ovs_vsctl("get interface %s statistics:%s"
299                                 % (interface, mapfrom)).strip('"')
300                 vtep_ctl("set logical_binding_stats %s %s=%s"
301                         % (uuid, mapto, value))
302
303     def run(self):
304         self.update_local_macs()
305         self.update_remote_macs()
306         self.update_stats()
307
308 def add_binding(ps_name, binding, ls):
309     vlog.info("adding binding %s" % binding)
310
311     vlan, pp_name = binding.split("-")
312     pbinding = binding+"-p"
313     lbinding = binding+"-l"
314
315     # Create a patch port that connects the VLAN+port to the lswitch.
316     # Do them as two separate calls so if one side already exists, the
317     # other side is created.
318     ovs_vsctl("add-port %s %s "
319               " -- set Interface %s type=patch options:peer=%s"
320               % (ps_name, pbinding, pbinding, lbinding))
321     ovs_vsctl("add-port %s %s "
322               " -- set Interface %s type=patch options:peer=%s"
323               % (ls.short_name, lbinding, lbinding, pbinding))
324
325     port_no = ovs_vsctl("get Interface %s ofport" % pp_name)
326     patch_no = ovs_vsctl("get Interface %s ofport" % pbinding)
327     vlan_ = vlan.lstrip('0')
328     if vlan_:
329         ovs_ofctl("add-flow %s in_port=%s,dl_vlan=%s,action=strip_vlan,%s"
330                   % (ps_name, port_no, vlan_, patch_no))
331         ovs_ofctl("add-flow %s in_port=%s,action=mod_vlan_vid:%s,%s"
332                   % (ps_name, patch_no, vlan_, port_no))
333     else:
334         ovs_ofctl("add-flow %s in_port=%s,action=%s"
335                   % (ps_name, port_no, patch_no))
336         ovs_ofctl("add-flow %s in_port=%s,action=%s"
337                   % (ps_name, patch_no, port_no))
338
339     # Create a logical_bindings_stats record.
340     if not vlan_:
341         vlan_ = "0"
342     vtep_ctl("set physical_port %s vlan_stats:%s=@stats --\
343             --id=@stats create logical_binding_stats packets_from_local=0"\
344             % (pp_name, vlan_))
345
346     ls.add_lbinding(lbinding)
347     Bindings[binding] = ls.name
348
349 def del_binding(ps_name, binding, ls):
350     vlog.info("removing binding %s" % binding)
351
352     vlan, pp_name = binding.split("-")
353     pbinding = binding+"-p"
354     lbinding = binding+"-l"
355
356     port_no = ovs_vsctl("get Interface %s ofport" % pp_name)
357     patch_no = ovs_vsctl("get Interface %s ofport" % pbinding)
358     vlan_ = vlan.lstrip('0')
359     if vlan_:
360         ovs_ofctl("del-flows %s in_port=%s,dl_vlan=%s"
361                   % (ps_name, port_no, vlan_))
362         ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no))
363     else:
364         ovs_ofctl("del-flows %s in_port=%s" % (ps_name, port_no))
365         ovs_ofctl("del-flows %s in_port=%s" % (ps_name, patch_no))
366
367     ls.del_lbinding(lbinding)
368
369     # Destroy the patch port that connects the VLAN+port to the lswitch
370     ovs_vsctl("del-port %s %s -- del-port %s %s"
371               % (ps_name, pbinding, ls.short_name, lbinding))
372
373     # Remove the record that links vlan with stats in logical_binding_stats.
374     vtep_ctl("remove physical_port %s vlan_stats %s" % (pp_name, vlan))
375
376     del Bindings[binding]
377
378 def handle_physical(ps_name):
379     # Gather physical ports except the patch ports we created
380     ovs_ports = ovs_vsctl("list-ports %s" % ps_name).split()
381     ovs_port_set = set([port for port in ovs_ports if port[-2:] != "-p"])
382
383     vtep_pp_set = set(vtep_ctl("list-ports %s" % ps_name).split())
384
385     for pp_name in ovs_port_set.difference(vtep_pp_set):
386         vlog.info("adding %s to %s" % (pp_name, ps_name))
387         vtep_ctl("add-port %s %s" % (ps_name, pp_name))
388
389     for pp_name in vtep_pp_set.difference(ovs_port_set):
390         vlog.info("deleting %s from %s" % (pp_name, ps_name))
391         vtep_ctl("del-port %s %s" % (ps_name, pp_name))
392
393     new_bindings = set()
394     for pp_name in vtep_pp_set:
395         binding_set = set(vtep_ctl("list-bindings %s %s"
396                                    % (ps_name, pp_name)).splitlines())
397
398         for b in binding_set:
399             vlan, ls_name = b.split()
400             if ls_name not in Lswitches:
401                 Lswitches[ls_name] = Logical_Switch(ls_name)
402
403             binding = "%s-%s" % (vlan, pp_name)
404             ls = Lswitches[ls_name]
405             new_bindings.add(binding)
406
407             if Bindings.has_key(binding):
408                 if Bindings[binding] == ls_name:
409                     continue
410                 else:
411                     del_binding(ps_name, binding, Lswitches[Bindings[binding]])
412
413             add_binding(ps_name, binding, ls)
414
415
416     dead_bindings = set(Bindings.keys()).difference(new_bindings)
417     for binding in dead_bindings:
418         ls_name = Bindings[binding]
419         ls = Lswitches[ls_name]
420
421         del_binding(ps_name, binding, ls)
422
423         if not len(ls.ports):
424             ovs_vsctl("del-br %s" % Lswitches[ls_name].short_name)
425             del Lswitches[ls_name]
426
427 def setup(ps_name):
428     br_list = ovs_vsctl("list-br").split()
429     if (ps_name not in br_list):
430         ovs.util.ovs_fatal(0, "couldn't find OVS bridge %s" % ps_name, vlog)
431
432     call_prog("vtep-ctl", ["set", "physical_switch", ps_name,
433                            'description="OVS VTEP Emulator"'])
434
435     tunnel_ips = vtep_ctl("get physical_switch %s tunnel_ips"
436                           % ps_name).strip('[]"').split(", ")
437     if len(tunnel_ips) != 1 or not tunnel_ips[0]:
438         ovs.util.ovs_fatal(0, "exactly one 'tunnel_ips' should be set", vlog)
439
440     global Tunnel_Ip
441     Tunnel_Ip = tunnel_ips[0]
442
443     ovs_ofctl("del-flows %s" % ps_name)
444
445     # Remove any logical bridges from the previous run
446     for br in br_list:
447         if ovs_vsctl("br-get-external-id %s vtep_logical_switch"
448                      % br) == "true":
449             # Remove the remote side of any logical switch
450             ovs_ports = ovs_vsctl("list-ports %s" % br).split()
451             for port in ovs_ports:
452                 port_type = ovs_vsctl("get Interface %s type"
453                                       % port).strip('"')
454                 if port_type != "patch":
455                     continue
456
457                 peer = ovs_vsctl("get Interface %s options:peer"
458                                  % port).strip('"')
459                 if (peer):
460                     ovs_vsctl("del-port %s" % peer)
461
462             ovs_vsctl("del-br %s" % br)
463
464
465 def main():
466     parser = argparse.ArgumentParser()
467     parser.add_argument("ps_name", metavar="PS-NAME",
468                         help="Name of physical switch.")
469     parser.add_argument("--root-prefix", metavar="DIR",
470                         help="Use DIR as alternate root directory"
471                         " (for testing).")
472     parser.add_argument("--version", action="version",
473                         version="%s %s" % (ovs.util.PROGRAM_NAME, VERSION))
474
475     ovs.vlog.add_args(parser)
476     ovs.daemon.add_args(parser)
477     args = parser.parse_args()
478     ovs.vlog.handle_args(args)
479     ovs.daemon.handle_args(args)
480
481     global root_prefix
482     if args.root_prefix:
483         root_prefix = args.root_prefix
484
485     ps_name = args.ps_name
486
487     ovs.daemon.daemonize()
488
489     ovs.unixctl.command_register("exit", "", 0, 0, unixctl_exit, None)
490     error, unixctl = ovs.unixctl.server.UnixctlServer.create(None,
491                                                              version=VERSION)
492     if error:
493         ovs.util.ovs_fatal(error, "could not create unixctl server", vlog)
494
495     setup(ps_name)
496
497     while True:
498         unixctl.run()
499         if exiting:
500             break
501
502         handle_physical(ps_name)
503
504         for ls_name, ls in Lswitches.items():
505             ls.run()
506
507         poller = ovs.poller.Poller()
508         unixctl.wait(poller)
509         poller.timer_wait(1000)
510         poller.block()
511
512     unixctl.close()
513
514 if __name__ == '__main__':
515     try:
516         main()
517     except SystemExit:
518         # Let system.exit() calls complete normally
519         raise
520     except:
521         vlog.exception("traceback")
522         sys.exit(ovs.daemon.RESTART_EXIT_CODE)