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