cleanup networking api, rest and cli and unittest
[osm/vim-emu.git] / src / emuvim / api / openstack / manage.py
1 """Openstack manage component of PG Sandman.
2
3 .. module:: manage
4 :synopsis: Module containing the OpenstackManage class.
5 .. moduleauthor: PG Sandman
6
7 """
8
9 import logging
10 import threading
11 import uuid
12 import networkx as nx
13 import chain_api
14 import json
15 import random
16 from emuvim.api.openstack.resources import Net, Port
17 from mininet.node import OVSSwitch, RemoteController, Node
18
19
20 class OpenstackManage(object):
21 """
22 OpenstackManage is a singleton and management component for the emulator.
23 It is the brain of the Openstack component and manages everything that is not datacenter specific like
24 network chains or load balancers.
25 """
26 __instance = None
27
28 def __new__(cls):
29 if OpenstackManage.__instance is None:
30 OpenstackManage.__instance = object.__new__(cls)
31 return OpenstackManage.__instance
32
33 def __init__(self, ip="0.0.0.0", port=4000):
34 # we are a singleton, only initialize once!
35 self.lock = threading.Lock()
36 with self.lock:
37 if hasattr(self, "init"):
38 return
39 self.init = True
40
41 self.endpoints = dict()
42 self.cookies = set()
43 self.cookies.add(0)
44 self.ip = ip
45 self.port = port
46 self._net = None
47 # to keep track which src_vnf(input port on the switch) handles a load balancer
48 self.lb_flow_cookies = dict()
49 self.chain_flow_cookies = dict()
50
51 # for the visualization also store the complete chain data incl. paths
52 self.full_chain_data = dict()
53 self.full_lb_data = dict()
54
55 # flow groups could be handled for each switch separately, but this global group counter should be easier to
56 # debug and to maintain
57 self.flow_groups = dict()
58
59 # we want one global chain api. this should not be datacenter dependent!
60 self.chain = chain_api.ChainApi(ip, port, self)
61 self.thread = threading.Thread(target=self.chain._start_flask, args=())
62 self.thread.daemon = True
63 self.thread.name = self.chain.__class__
64 self.thread.start()
65
66 # floating ip network setup
67 self.floating_switch = None
68 self.floating_network = None
69 self.floating_netmask = "192.168.100.0/24"
70 self.floating_nodes = dict()
71 self.floating_cookies = dict()
72 self.floating_intf = None
73 self.floating_links = dict()
74
75 @property
76 def net(self):
77 return self._net
78
79 @net.setter
80 def net(self, value):
81 if self._net is None:
82 self._net = value
83 self.init_floating_network()
84 self._net = value
85
86 def init_floating_network(self):
87 """
88 Initialize the floating network component for the emulator.
89 Will not do anything if already initialized.
90 """
91 if self.net is not None and self.floating_switch is None:
92 # create a floating network
93 fn = self.floating_network = Net("default")
94 fn.id = str(uuid.uuid4())
95 fn.set_cidr(self.floating_netmask)
96
97 # create a subnet
98 fn.subnet_id = str(uuid.uuid4())
99 fn.subnet_name = fn.name + "-sub"
100
101 # create a port for the host
102 port = Port("root-port")
103 #port.id = str(uuid.uuid4())
104 port.net_name = fn.name
105
106 # get next free ip
107 root_ip = fn.get_new_ip_address(port.name)
108 port.ip_address = root_ip
109 # floating ip network setup
110 # wierd way of getting a datacenter object
111 first_dc = self.net.dcs.values()[0]
112 # set a dpid for the switch. for this we have to get the id of the next possible dc
113 self.floating_switch = self.net.addSwitch("fs1", dpid=hex(first_dc._get_next_dc_dpid())[2:])
114 # this is the interface appearing on the physical host
115 self.floating_root = Node('root', inNamespace=False)
116 self.net.hosts.append(self.floating_root)
117 self.net.nameToNode['root'] = self.floating_root
118 self.floating_intf = self.net.addLink(self.floating_root, self.floating_switch).intf1
119 self.floating_root.setIP(root_ip, intf=self.floating_intf)
120 self.floating_nodes[(self.floating_root.name, root_ip)] = self.floating_root
121
122
123 def stop_floating_network(self):
124 self._net = None
125 self.floating_switch = None
126
127 def add_endpoint(self, ep):
128 """
129 Registers an openstack endpoint with manage
130
131 :param ep: Openstack API endpoint
132 :type ep: :class:`heat.openstack_api_endpoint`
133 """
134 key = "%s:%s" % (ep.ip, ep.port)
135 self.endpoints[key] = ep
136
137 def get_cookie(self):
138 """
139 Get an unused cookie.
140
141 :return: Cookie
142 :rtype: ``int``
143 """
144 cookie = int(max(self.cookies) + 1)
145 self.cookies.add(cookie)
146 return cookie
147
148 def get_flow_group(self, src_vnf_name, src_vnf_interface):
149 """
150 Gets free group that is not currently used by any other flow for the specified interface / VNF.
151
152 :param src_vnf_name: Source VNF name
153 :type src_vnf_name: ``str``
154 :param src_vnf_interface: Source VNF interface name
155 :type src_vnf_interface: ``str``
156 :return: Flow group identifier.
157 :rtype: ``int``
158 """
159 if (src_vnf_name, src_vnf_interface) not in self.flow_groups:
160 grp = int(len(self.flow_groups) + 1)
161 self.flow_groups[(src_vnf_name, src_vnf_interface)] = grp
162 else:
163 grp = self.flow_groups[(src_vnf_name, src_vnf_interface)]
164 return grp
165
166 def check_vnf_intf_pair(self, vnf_name, vnf_intf_name):
167 """
168 Checks if a VNF exists and has the given interface
169
170 :param vnf_name: Name of the VNF to be checked
171 :type vnf_name: ``str``
172 :param vnf_intf_name: Name of the interface that belongst to the VNF
173 :type vnf_intf_name: ``str``
174 :return: ``True`` if it is valid pair, else ``False``
175 :rtype: ``bool``
176 """
177
178 if vnf_name in self.net:
179 vnf = self.net.getNodeByName(vnf_name)
180 return vnf_intf_name in vnf.nameToIntf
181
182 def network_action_start(self, vnf_src_name, vnf_dst_name, **kwargs):
183 """
184 Starts a network chain for a source destination pair
185
186 :param vnf_src_name: Name of the source VNF
187 :type vnf_src_name: ``str``
188 :param vnf_dst_name: Name of the source VNF interface
189 :type vnf_dst_name: ``str``
190 :param \**kwargs: See below
191
192 :Keyword Arguments:
193 * *vnf_src_interface* (``str``): Name of source interface.
194 * *vnf_dst_interface* (``str``): Name of destination interface.
195 * *weight* (``int``): This value is fed into the shortest path computation if no path is specified.
196 * *match* (``str``): A custom match entry for the openflow flow rules. Only vlanid or port possible.
197 * *bidirectional* (``bool``): If set the chain will be set in both directions, else it will just set up \
198 from source to destination.
199 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
200 able to modify the correct flows.
201 * *no_route* (``bool``): If set a layer 3 route to the target interface will not be set up.
202 :return: The cookie chosen for the flow.
203 :rtype: ``int``
204 """
205 try:
206 vnf_src_interface = kwargs.get('vnf_src_interface')
207 vnf_dst_interface = kwargs.get('vnf_dst_interface')
208 layer2 = kwargs.get('layer2', True)
209 match = kwargs.get('match')
210 flow = (vnf_src_name, vnf_src_interface, vnf_dst_name, vnf_dst_interface)
211 if flow in self.chain_flow_cookies:
212 raise Exception("There is already a chain at the specified src/dst pair!")
213 # set up a layer 2 chain, this allows multiple chains for the same interface
214 src_node = self.net.getNodeByName(vnf_src_name)
215 dst_node = self.net.getNodeByName(vnf_dst_name)
216 dst_intf = dst_node.intf(vnf_dst_interface)
217 if layer2:
218 switch, inport = self._get_connected_switch_data(vnf_src_name, vnf_src_interface)
219 self.setup_arp_reply_at(switch, inport, dst_intf.IP(), dst_intf.MAC())
220 if isinstance(match, str):
221 match += ",dl_dst=%s" % dst_intf.MAC()
222 else:
223 match = "dl_dst=%s" % dst_intf.MAC()
224
225 cookie = kwargs.get('cookie', self.get_cookie())
226 self.cookies.add(cookie)
227 c = self.net.setChain(
228 vnf_src_name, vnf_dst_name,
229 vnf_src_interface=vnf_src_interface,
230 vnf_dst_interface=vnf_dst_interface,
231 cmd='add-flow',
232 weight=kwargs.get('weight'),
233 match=match,
234 bidirectional=False,
235 cookie=cookie,
236 path=kwargs.get('path'))
237
238 # to keep this logic seperate of the core son-emu do the housekeeping here
239 data = dict()
240 data["src_vnf"] = vnf_src_name
241 data["src_intf"] = vnf_src_interface
242 data["dst_vnf"] = vnf_dst_name
243 data["dst_intf"] = vnf_dst_interface
244 data["cookie"] = cookie
245 data["layer2"] = layer2
246 if kwargs.get('path') is not None:
247 data["path"] = kwargs.get('path')
248 else:
249 data["path"] = self._get_path(vnf_src_name, vnf_dst_name, vnf_src_interface,
250 vnf_dst_interface)[0]
251
252 # add route to dst ip to this interface
253 # this might block on containers that are still setting up, so start a new thread
254 if not kwargs.get('no_route'):
255 # son_emu does not like concurrent commands for a container so we need to lock this if multiple chains
256 # on the same interface are created
257 src_node.setHostRoute(dst_node.intf(vnf_dst_interface).IP(), vnf_src_interface)
258
259 try:
260 son_emu_data = json.loads(self.get_son_emu_chain_data(vnf_src_name))
261 except:
262 son_emu_data = dict()
263 if "son_emu_data" not in son_emu_data:
264 son_emu_data["son_emu_data"] = dict()
265 if "interfaces" not in son_emu_data["son_emu_data"]:
266 son_emu_data["son_emu_data"]["interfaces"] = dict()
267 if vnf_src_interface not in son_emu_data["son_emu_data"]["interfaces"]:
268 son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface] = list()
269 son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface].append(dst_intf.IP())
270
271 self.set_son_emu_chain_data(vnf_src_name, son_emu_data)
272
273 if kwargs.get('bidirectional', False):
274 # call the reverse direction
275 path = kwargs.get('path')
276 if path is not None:
277 path = list(reversed(path))
278 self.network_action_start(vnf_dst_name, vnf_src_name, vnf_src_interface=vnf_dst_interface,
279 vnf_dst_interface=vnf_src_interface, bidirectional=False,
280 layer2=kwargs.get('layer2', False), path=path,
281 no_route=kwargs.get('no_route'))
282
283 self.full_chain_data[flow] = data
284 self.chain_flow_cookies[flow] = cookie
285 return cookie
286 except Exception as ex:
287 logging.exception("RPC error.")
288 raise Exception(ex.message)
289
290 def network_action_stop(self, vnf_src_name, vnf_dst_name, **kwargs):
291 """
292 Starts a network chain for a source destination pair
293
294 :param vnf_src_name: Name of the source VNF
295 :type vnf_src_name: ``str``
296 :param vnf_dst_name: Name of the source VNF interface
297 :type vnf_dst_name: ``str``
298 :param \**kwargs: See below
299
300 :Keyword Arguments:
301 * *vnf_src_interface* (``str``): Name of source interface.
302 * *vnf_dst_interface* (``str``): Name of destination interface.
303 * *bidirectional* (``bool``): If set the chain will be torn down in both directions, else it will just\
304 be torn down from source to destination.
305 * *cookie* (``int``): Cookie value used by openflow. Used to identify the flows in the switches to be \
306 able to modify the correct flows.
307 """
308 try:
309 if 'cookie' in kwargs:
310 return self.delete_flow_by_cookie(kwargs.get('cookie'))
311
312 if kwargs.get('bidirectional', False):
313 self.delete_chain_by_intf(vnf_dst_name, kwargs.get('vnf_dst_interface'),
314 vnf_src_name, kwargs.get('vnf_src_interface'))
315
316 return self.delete_chain_by_intf(vnf_src_name, kwargs.get('vnf_src_interface'),
317 vnf_dst_name, kwargs.get('vnf_dst_interface'))
318 except Exception as ex:
319 logging.exception("RPC error.")
320 return ex.message
321
322 def set_son_emu_chain_data(self, vnf_name, data):
323 """
324 Set son-emu chain data for this node.
325
326 :param vnf_name: The name of the vnf where the data is stored.
327 :type vnf_name: ``str``
328 :param data: Raw data to store on the node.
329 :type data: ``str``
330 """
331 self.net.getNodeByName(vnf_name).cmd("echo \'%s\' > /tmp/son_emu_data.json" % json.dumps(data))
332 ip_list = []
333 for intf in data['son_emu_data']['interfaces'].values():
334 ip_list.extend(intf)
335
336 self.net.getNodeByName(vnf_name).cmd("echo \'%s\' > /tmp/son_emu_data" % "\n".join(ip_list))
337
338 def get_son_emu_chain_data(self, vnf_name):
339 """
340 Get the current son-emu chain data set for this node.
341
342 :param vnf_name: The name of the vnf where the data is stored.
343 :type vnf_name: ``str``
344 :return: raw data stored on the node
345 :rtype: ``str``
346 """
347 return self.net.getNodeByName(vnf_name).cmd("cat /tmp/son_emu_data.json")
348
349 def _get_connected_switch_data(self, vnf_name, vnf_interface):
350 """
351 Get the switch an interface is connected to
352 :param vnf_name: Name of the VNF
353 :type vnf_name: ``str``
354 :param vnf_interface: Name of the VNF interface
355 :type vnf_interface: ``str``
356 :return: List containing the switch, and the inport number
357 :rtype: [``str``, ``int``]
358 """
359 src_sw = None
360 src_sw_inport_nr = None
361 for connected_sw in self.net.DCNetwork_graph.neighbors(vnf_name):
362 link_dict = self.net.DCNetwork_graph[vnf_name][connected_sw]
363 for link in link_dict:
364 if (link_dict[link]['src_port_id'] == vnf_interface or
365 link_dict[link][
366 'src_port_name'] == vnf_interface):
367 # found the right link and connected switch
368 src_sw = connected_sw
369 src_sw_inport_nr = link_dict[link]['dst_port_nr']
370 break
371
372 return src_sw, src_sw_inport_nr
373
374 def _get_path(self, src_vnf, dst_vnf, src_vnf_intf, dst_vnf_intf):
375 """
376 Own implementation of the get_path function from DCNetwork, because we just want the path and not set up
377 flows on the way.
378
379 :param src_vnf: Name of the source VNF
380 :type src_vnf: ``str``
381 :param dst_vnf: Name of the destination VNF
382 :type dst_vnf: ``str``
383 :param src_vnf_intf: Name of the source VNF interface
384 :type src_vnf_intf: ``str``
385 :param dst_vnf_intf: Name of the destination VNF interface
386 :type dst_vnf_intf: ``str``
387 :return: path, src_sw, dst_sw
388 :rtype: ``list``, ``str``, ``str``
389 """
390 # modified version of the _chainAddFlow from emuvim.dcemulator.net._chainAddFlow
391 src_sw = None
392 dst_sw = None
393 logging.debug("Find shortest path from vnf %s to %s",
394 src_vnf, dst_vnf)
395
396 for connected_sw in self.net.DCNetwork_graph.neighbors(src_vnf):
397 link_dict = self.net.DCNetwork_graph[src_vnf][connected_sw]
398 for link in link_dict:
399 if (link_dict[link]['src_port_id'] == src_vnf_intf or
400 link_dict[link][
401 'src_port_name'] == src_vnf_intf):
402 # found the right link and connected switch
403 src_sw = connected_sw
404 break
405
406 for connected_sw in self.net.DCNetwork_graph.neighbors(dst_vnf):
407 link_dict = self.net.DCNetwork_graph[connected_sw][dst_vnf]
408 for link in link_dict:
409 if link_dict[link]['dst_port_id'] == dst_vnf_intf or \
410 link_dict[link][
411 'dst_port_name'] == dst_vnf_intf:
412 # found the right link and connected
413 dst_sw = connected_sw
414 break
415 logging.debug("From switch %s to %s " % (src_sw, dst_sw))
416
417 # get shortest path
418 try:
419 # returns the first found shortest path
420 # if all shortest paths are wanted, use: all_shortest_paths
421 path = nx.shortest_path(self.net.DCNetwork_graph, src_sw, dst_sw)
422 except:
423 logging.exception("No path could be found between {0} and {1} using src_sw={2} and dst_sw={3}".format(
424 src_vnf, dst_vnf, src_sw, dst_sw))
425 logging.debug("Graph nodes: %r" % self.net.DCNetwork_graph.nodes())
426 logging.debug("Graph edges: %r" % self.net.DCNetwork_graph.edges())
427 for e, v in self.net.DCNetwork_graph.edges():
428 logging.debug("%r" % self.net.DCNetwork_graph[e][v])
429 return "No path could be found between {0} and {1}".format(src_vnf, dst_vnf)
430
431 logging.info("Shortest path between {0} and {1}: {2}".format(src_vnf, dst_vnf, path))
432 return path, src_sw, dst_sw
433
434 def add_loadbalancer(self, src_vnf_name, src_vnf_interface, lb_data):
435 """
436 This function will set up a loadbalancer at the given interface.
437
438 :param src_vnf_name: Name of the source VNF
439 :type src_vnf_name: ``str``
440 :param src_vnf_interface: Name of the destination VNF
441 :type src_vnf_interface: ``str``
442 :param lb_data: A dictionary containing the destination data as well as custom path settings
443 :type lb_data: ``dict``
444
445 :Example:
446 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
447 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
448 "s1", "dc2.s1"]}}}
449 """
450 net = self.net
451 src_sw_inport_nr = 0
452 src_sw = None
453 dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict())
454 # a custom path can be specified as a list of switches
455 custom_paths = lb_data.get('path', dict())
456 dest_vnf_outport_nrs = list()
457
458 logging.debug("Call to add_loadbalancer at %s intfs:%s" % (src_vnf_name, src_vnf_interface))
459
460 if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_interface):
461 raise Exception(u"Source VNF %s or intfs %s does not exist" % (src_vnf_name, src_vnf_interface))
462
463 # find the switch belonging to the source interface, as well as the inport nr
464 for connected_sw in net.DCNetwork_graph.neighbors(src_vnf_name):
465 link_dict = net.DCNetwork_graph[src_vnf_name][connected_sw]
466 for link in link_dict:
467 if link_dict[link]['src_port_name'] == src_vnf_interface:
468 src_sw = connected_sw
469 src_sw_inport_nr = link_dict[link]['dst_port_nr']
470 break
471
472 if src_sw is None or src_sw_inport_nr == 0:
473 raise Exception(u"Source VNF or interface can not be found.")
474
475 # get all target interface outport numbers
476 for vnf_name in dest_intfs_mapping:
477 if vnf_name not in net.DCNetwork_graph:
478 raise Exception(u"Target VNF %s is not known." % vnf_name)
479 for connected_sw in net.DCNetwork_graph.neighbors(vnf_name):
480 link_dict = net.DCNetwork_graph[vnf_name][connected_sw]
481 for link in link_dict:
482 if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]:
483 dest_vnf_outport_nrs.append(int(link_dict[link]['dst_port_nr']))
484 # get first switch
485 if (src_vnf_name, src_vnf_interface) not in self.lb_flow_cookies:
486 self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)] = list()
487
488 src_intf = None
489 src_ip = None
490 src_mac = None
491 for intf in net[src_vnf_name].intfs.values():
492 if intf.name == src_vnf_interface:
493 src_mac = intf.mac
494 src_ip = intf.ip
495 src_intf = intf
496
497 # set up paths for each destination vnf individually
498 index = 0
499 cookie = self.get_cookie()
500 main_cmd = "add-flow -OOpenFlow13"
501 self.lb_flow_cookies[(src_vnf_name, src_vnf_interface)].append(cookie)
502
503 # bookkeeping
504 data = dict()
505 data["src_vnf"] = src_vnf_name
506 data["src_intf"] = src_vnf_interface
507 data["paths"] = list()
508 data["cookie"] = cookie
509
510 # lb mac for src -> target connections
511 lb_mac = "31:33:70:%02x:%02x:%02x" % (random.randint(0, 255),random.randint(0, 255),random.randint(0, 255))
512
513 # calculate lb ip as src_intf.ip +1
514 octets = src_ip.split('.')
515 octets[3] = str(int(octets[3]) + 1)
516 plus_one = '.'.join(octets)
517
518 # set up arp reply as well as add the route to the interface
519 self.setup_arp_reply_at(src_sw, src_sw_inport_nr, plus_one, lb_mac, cookie=cookie)
520 net.getNodeByName(src_vnf_name).setHostRoute(plus_one, src_vnf_interface)
521
522 for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items():
523 path, src_sw, dst_sw = self._get_path(src_vnf_name, dst_vnf_name,
524 src_vnf_interface, dst_vnf_interface)
525
526 # use custom path if one is supplied
527 # json does not support hashing on tuples so we use nested dicts
528 if custom_paths is not None and dst_vnf_name in custom_paths:
529 if dst_vnf_interface in custom_paths[dst_vnf_name]:
530 path = custom_paths[dst_vnf_name][dst_vnf_interface]
531 logging.debug("Taking custom path from %s to %s: %s" % (src_vnf_name, dst_vnf_name, path))
532
533 if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_interface):
534 self.delete_loadbalancer(src_vnf_name, src_vnf_interface)
535 raise Exception(u"VNF %s or intfs %s does not exist" % (dst_vnf_name, dst_vnf_interface))
536 if isinstance(path, dict):
537 self.delete_loadbalancer(src_vnf_name, src_vnf_interface)
538 raise Exception(u"Can not find a valid path. Are you specifying the right interfaces?.")
539
540 target_mac = "fa:17:00:03:13:37"
541 target_ip = "0.0.0.0"
542 for intf in net[dst_vnf_name].intfs.values():
543 if intf.name == dst_vnf_interface:
544 target_mac = str(intf.mac)
545 target_ip = str(intf.ip)
546 dst_sw_outport_nr = dest_vnf_outport_nrs[index]
547 current_hop = src_sw
548 switch_inport_nr = src_sw_inport_nr
549
550 #self.setup_arp_reply_at(src_sw, src_sw_inport_nr, target_ip, target_mac, cookie=cookie)
551 net.getNodeByName(dst_vnf_name).setHostRoute(src_ip, dst_vnf_interface)
552
553 # choose free vlan if path contains more than 1 switch
554 if len(path) > 1:
555 vlan = net.vlans.pop()
556 if vlan == 0:
557 vlan = net.vlans.pop()
558 else:
559 vlan = None
560
561 single_flow_data = dict()
562 single_flow_data["dst_vnf"] = dst_vnf_name
563 single_flow_data["dst_intf"] = dst_vnf_interface
564 single_flow_data["path"] = path
565 single_flow_data["vlan"] = vlan
566 single_flow_data["cookie"] = cookie
567
568 data["paths"].append(single_flow_data)
569
570 # src to target
571 for i in range(0, len(path)):
572 if i < len(path) - 1:
573 next_hop = path[i + 1]
574 else:
575 # last switch reached
576 next_hop = dst_vnf_name
577 next_node = net.getNodeByName(next_hop)
578 if next_hop == dst_vnf_name:
579 switch_outport_nr = dst_sw_outport_nr
580 logging.info("end node reached: {0}".format(dst_vnf_name))
581 elif not isinstance(next_node, OVSSwitch):
582 logging.info("Next node: {0} is not a switch".format(next_hop))
583 return "Next node: {0} is not a switch".format(next_hop)
584 else:
585 # take first link between switches by default
586 index_edge_out = 0
587 switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
588
589 cmd = 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr, cookie)
590 cmd_back = 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr, cookie)
591 # if a vlan is picked, the connection is routed through multiple switches
592 if vlan is not None:
593 if path.index(current_hop) == 0: # first node
594 # flow #index set up
595 cmd = 'in_port=%s' % src_sw_inport_nr
596 cmd += ',cookie=%s' % cookie
597 cmd += ',table=%s' % cookie
598 cmd += ',ip'
599 cmd += ',reg1=%s' % index
600 cmd += ',actions='
601 # set vlan id
602 cmd += ',push_vlan:0x8100'
603 masked_vlan = vlan | 0x1000
604 cmd += ',set_field:%s->vlan_vid' % masked_vlan
605 cmd += ',set_field:%s->eth_dst' % target_mac
606 cmd += ',set_field:%s->ip_dst' % target_ip
607 cmd += ',output:%s' % switch_outport_nr
608
609 # last switch for reverse route
610 # remove any vlan tags
611 cmd_back += ',dl_vlan=%s' % vlan
612 cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr
613 elif next_hop == dst_vnf_name: # last switch
614 # remove any vlan tags
615 cmd += ',dl_vlan=%s' % vlan
616 cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr
617 # set up arp replys at the port so the dst nodes know the src
618 self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
619
620 # reverse route
621 cmd_back = 'in_port=%s' % switch_outport_nr
622 cmd_back += ',cookie=%s' % cookie
623 cmd_back += ',ip'
624 cmd_back += ',actions='
625 cmd_back += 'push_vlan:0x8100'
626 masked_vlan = vlan | 0x1000
627 cmd_back += ',set_field:%s->vlan_vid' % masked_vlan
628 cmd_back += ',set_field:%s->eth_src' % lb_mac
629 cmd_back += ',set_field:%s->ip_src' % plus_one
630 cmd_back += ',output:%s' % switch_inport_nr
631 else: # middle nodes
632 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
633 # if we just output it on the same port as it came in
634 if switch_inport_nr == switch_outport_nr:
635 cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
636 cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
637 else:
638 cmd += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_outport_nr)
639 cmd_back += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_inport_nr)
640 # output the packet at the correct outport
641 else:
642 cmd = 'in_port=%s' % src_sw_inport_nr
643 cmd += ',cookie=%s' % cookie
644 cmd += ',table=%s' % cookie
645 cmd += ',ip'
646 cmd += ',reg1=%s' % index
647 cmd += ',actions='
648 cmd += ',set_field:%s->eth_dst' % target_mac
649 cmd += ',set_field:%s->ip_dst' % target_ip
650 cmd += ',output:%s' % switch_outport_nr
651
652 # reverse route
653 cmd_back = 'in_port=%s' % switch_outport_nr
654 cmd_back += ',cookie=%s' % cookie
655 cmd_back += ',ip'
656 cmd_back += ',actions='
657 cmd_back += ',set_field:%s->eth_src' % lb_mac
658 cmd_back += ',set_field:%s->ip_src' % plus_one
659 cmd_back += ',output:%s' % src_sw_inport_nr
660
661 self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
662
663 # excecute the command on the target switch
664 logging.debug(cmd)
665 cmd = "\"%s\"" % cmd
666 cmd_back = "\"%s\"" % cmd_back
667 net[current_hop].dpctl(main_cmd, cmd)
668 net[current_hop].dpctl(main_cmd, cmd_back)
669
670 # set next hop for the next iteration step
671 if isinstance(next_node, OVSSwitch):
672 switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
673 current_hop = next_hop
674
675 # advance to next destination
676 index += 1
677
678 # set up the actual load balancing rule as a multipath on the very first switch
679 cmd = '"in_port=%s' % src_sw_inport_nr
680 cmd += ',cookie=%s' % (cookie)
681 cmd += ',ip'
682 cmd += ',actions='
683 # push 0x01 into the first register
684 cmd += 'load:0x1->NXM_NX_REG0[]'
685 # load balance modulo n over all dest interfaces
686 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
687 # to balance any kind of traffic
688 cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping)
689 # reuse the cookie as table entry as it will be unique
690 cmd += ',resubmit(, %s)"' % cookie
691
692 # actually add the flow
693 logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd))
694 net[src_sw].dpctl(main_cmd, cmd)
695
696 # finally add all flow data to the internal data storage
697 self.full_lb_data[(src_vnf_name, src_vnf_interface)] = data
698
699 def add_floating_lb(self, datacenter, lb_data):
700 """
701 This function will set up a loadbalancer at the given datacenter.
702 This function returns the floating ip assigned to the loadbalancer as multiple ones are possible.
703
704 :param datacenter: The datacenter entrypoint
705 :type datacenter: ``str``
706 :param lb_data: A dictionary containing the destination data as well as custom path settings
707 :type lb_data: ``dict``
708
709 :Example:
710 lbdata = {"dst_vnf_interfaces": {"dc2_man_web0": "port-man-2",
711 "dc3_man_web0": "port-man-4","dc4_man_web0": "port-man-6"}, "path": {"dc2_man_web0": {"port-man-2": [ "dc1.s1",\
712 "s1", "dc2.s1"]}}}
713 """
714 net = self.net
715 src_sw_inport_nr = 1
716 src_sw = self.floating_switch.name
717 dest_intfs_mapping = lb_data.get('dst_vnf_interfaces', dict())
718 # a custom path can be specified as a list of switches
719 custom_paths = lb_data.get('path', dict())
720 dest_vnf_outport_nrs = list()
721
722 if datacenter not in self.net.dcs:
723 raise Exception(u"Source datacenter can not be found.")
724
725 # get all target interface outport numbers
726 for vnf_name in dest_intfs_mapping:
727 if vnf_name not in net.DCNetwork_graph:
728 raise Exception(u"Target VNF %s is not known." % vnf_name)
729 for connected_sw in net.DCNetwork_graph.neighbors(vnf_name):
730 link_dict = net.DCNetwork_graph[vnf_name][connected_sw]
731 for link in link_dict:
732 if link_dict[link]['src_port_name'] == dest_intfs_mapping[vnf_name]:
733 dest_vnf_outport_nrs.append(int(link_dict[link]['dst_port_nr']))
734
735 if len(dest_vnf_outport_nrs) == 0:
736 raise Exception("There are no paths specified for the loadbalancer")
737 src_ip = self.floating_intf.IP()
738 src_mac = self.floating_intf.MAC()
739
740 # set up paths for each destination vnf individually
741 index = 0
742 cookie = self.get_cookie()
743 main_cmd = "add-flow -OOpenFlow13"
744 floating_ip = self.floating_network.get_new_ip_address("floating-ip").split("/")[0]
745
746 for dst_vnf_name, dst_vnf_interface in dest_intfs_mapping.items():
747 path = None
748 # use custom path if one is supplied
749 # json does not support hashing on tuples so we use nested dicts
750 if custom_paths is not None and dst_vnf_name in custom_paths:
751 if dst_vnf_interface in custom_paths[dst_vnf_name]:
752 path = custom_paths[dst_vnf_name][dst_vnf_interface]
753 logging.debug("Taking custom path to %s: %s" % (dst_vnf_name, path))
754 else:
755 if datacenter not in self.floating_links:
756 self.floating_links[datacenter] = \
757 net.addLink(self.floating_switch, datacenter)
758 path = \
759 self._get_path(self.floating_root.name, dst_vnf_name, self.floating_intf.name, dst_vnf_interface)[0]
760
761 if isinstance(path, dict):
762 self.delete_flow_by_cookie(cookie)
763 raise Exception(u"Can not find a valid path. Are you specifying the right interfaces?.")
764
765 intf = net[dst_vnf_name].nameToIntf[dst_vnf_interface]
766 target_mac = str(intf.MAC())
767 target_ip = str(intf.IP())
768 dst_sw_outport_nr = dest_vnf_outport_nrs[index]
769 current_hop = src_sw
770 switch_inport_nr = src_sw_inport_nr
771 vlan = net.vlans.pop()
772
773 # iterate all switches on the path
774 for i in range(0, len(path)):
775 if i < len(path) - 1:
776 next_hop = path[i + 1]
777 else:
778 # last switch reached
779 next_hop = dst_vnf_name
780 next_node = net.getNodeByName(next_hop)
781
782 # sanity checks
783 if next_hop == dst_vnf_name:
784 switch_outport_nr = dst_sw_outport_nr
785 logging.info("end node reached: {0}".format(dst_vnf_name))
786 elif not isinstance(next_node, OVSSwitch):
787 logging.info("Next node: {0} is not a switch".format(next_hop))
788 return "Next node: {0} is not a switch".format(next_hop)
789 else:
790 # take first link between switches by default
791 index_edge_out = 0
792 switch_outport_nr = net.DCNetwork_graph[current_hop][next_hop][index_edge_out]['src_port_nr']
793
794 # default filters, just overwritten on the first node and last node
795 cmd = 'priority=1,in_port=%s,cookie=%s' % (switch_inport_nr, cookie)
796 cmd_back = 'priority=1,in_port=%s,cookie=%s' % (switch_outport_nr, cookie)
797 if i == 0: # first node
798 cmd = 'in_port=%s' % src_sw_inport_nr
799 cmd += ',cookie=%s' % cookie
800 cmd += ',table=%s' % cookie
801 cmd += ',ip'
802 cmd += ',ip_dst=%s' % floating_ip
803 cmd += ',reg1=%s' % index
804 cmd += ',actions='
805 # set vlan id
806 cmd += ',push_vlan:0x8100'
807 masked_vlan = vlan | 0x1000
808 cmd += ',set_field:%s->vlan_vid' % masked_vlan
809 cmd += ',set_field:%s->eth_dst' % target_mac
810 cmd += ',set_field:%s->ip_dst' % target_ip
811 cmd += ',output:%s' % switch_outport_nr
812
813 # last switch for reverse route
814 # remove any vlan tags
815 cmd_back += ',dl_vlan=%s' % vlan
816 cmd_back += ',actions=pop_vlan,output:%s' % switch_inport_nr
817 self.setup_arp_reply_at(current_hop, src_sw_inport_nr, floating_ip, target_mac, cookie=cookie)
818 elif next_hop == dst_vnf_name: # last switch
819 # remove any vlan tags
820 cmd += ',dl_vlan=%s' % vlan
821 cmd += ',actions=pop_vlan,output:%s' % switch_outport_nr
822 # set up arp replys at the port so the dst nodes know the src
823 self.setup_arp_reply_at(current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie)
824
825 # reverse route
826 cmd_back = 'in_port=%s' % switch_outport_nr
827 cmd_back += ',cookie=%s' % cookie
828 cmd_back += ',ip'
829 cmd_back += ',actions='
830 cmd_back += 'push_vlan:0x8100'
831 masked_vlan = vlan | 0x1000
832 cmd_back += ',set_field:%s->vlan_vid' % masked_vlan
833 cmd_back += ',set_field:%s->eth_src' % src_mac
834 cmd_back += ',set_field:%s->ip_src' % floating_ip
835 cmd_back += ',output:%s' % switch_inport_nr
836 net.getNodeByName(dst_vnf_name).setHostRoute(src_ip, dst_vnf_interface)
837 else: # middle node
838 # if we have a circle in the path we need to specify this, as openflow will ignore the packet
839 # if we just output it on the same port as it came in
840 if switch_inport_nr == switch_outport_nr:
841 cmd += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
842 cmd_back += ',dl_vlan=%s,actions=IN_PORT' % (vlan)
843 else:
844 cmd += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_outport_nr)
845 cmd_back += ',dl_vlan=%s,actions=output:%s' % (vlan, switch_inport_nr)
846
847 # excecute the command on the target switch
848 logging.debug(cmd)
849 cmd = "\"%s\"" % cmd
850 cmd_back = "\"%s\"" % cmd_back
851 net[current_hop].dpctl(main_cmd, cmd)
852 net[current_hop].dpctl(main_cmd, cmd_back)
853
854 # set next hop for the next iteration step
855 if isinstance(next_node, OVSSwitch):
856 switch_inport_nr = net.DCNetwork_graph[current_hop][next_hop][0]['dst_port_nr']
857 current_hop = next_hop
858
859 # advance to next destination
860 index += 1
861
862 # set up the actual load balancing rule as a multipath on the very first switch
863 cmd = '"in_port=%s' % src_sw_inport_nr
864 cmd += ',cookie=%s' % (cookie)
865 cmd += ',ip'
866 cmd += ',actions='
867 # push 0x01 into the first register
868 cmd += 'load:0x1->NXM_NX_REG0[]'
869 # load balance modulo n over all dest interfaces
870 # TODO: in newer openvswitch implementations this should be changed to symmetric_l3l4+udp
871 # to balance any kind of traffic
872 cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len(dest_intfs_mapping)
873 # reuse the cookie as table entry as it will be unique
874 cmd += ',resubmit(, %s)"' % cookie
875
876 # actually add the flow
877 logging.debug("Switch: %s, CMD: %s" % (src_sw, cmd))
878 net[src_sw].dpctl(main_cmd, cmd)
879
880 self.floating_cookies[cookie] = floating_ip
881
882 return cookie, floating_ip
883
884 def setup_arp_reply_at(self, switch, port_nr, target_ip, target_mac, cookie=None):
885 """
886 Sets up a custom ARP reply at a switch.
887 An ARP request coming in on the `port_nr` for `target_ip` will be answered with target IP/MAC.
888
889 :param switch: The switch belonging to the interface
890 :type switch: ``str``
891 :param port_nr: The port number at the switch that is connected to the interface
892 :type port_nr: ``int``
893 :param target_ip: The IP for which to set up the ARP reply
894 :type target_ip: ``str``
895 :param target_mac: The MAC address of the target interface
896 :type target_mac: ``str``
897 :param cookie: cookie to identify the ARP request, if None a new one will be picked
898 :type cookie: ``int`` or ``None``
899 :return: cookie
900 :rtype: ``int``
901 """
902 if cookie is None:
903 cookie = self.get_cookie()
904 main_cmd = "add-flow -OOpenFlow13"
905
906 # first set up ARP requests for the source node, so it will always 'find' a partner
907 cmd = '"in_port=%s' % port_nr
908 cmd += ',cookie=%s' % cookie
909 cmd += ',arp'
910 # only answer for target ip arp requests
911 cmd += ',arp_tpa=%s' % target_ip
912 cmd += ',actions='
913 # set message type to ARP reply
914 cmd += 'load:0x2->NXM_OF_ARP_OP[]'
915 # set src ip as dst ip
916 cmd += ',move:NXM_OF_ETH_SRC[]->NXM_OF_ETH_DST[]'
917 # set src mac
918 cmd += ',set_field:%s->eth_src' % target_mac
919 # set src as target
920 cmd += ',move:NXM_NX_ARP_SHA[]->NXM_NX_ARP_THA[], move:NXM_OF_ARP_SPA[]->NXM_OF_ARP_TPA[]'
921 # set target mac as hex
922 cmd += ',load:0x%s->NXM_NX_ARP_SHA[]' % "".join(target_mac.split(':'))
923 # set target ip as hex
924 octets = target_ip.split('.')
925 dst_ip_hex = '{:02X}{:02X}{:02X}{:02X}'.format(*map(int, octets))
926 cmd += ',load:0x%s->NXM_OF_ARP_SPA[]' % dst_ip_hex
927 # output to incoming port remember the closing "
928 cmd += ',IN_PORT"'
929 self.net[switch].dpctl(main_cmd, cmd)
930 logging.debug(
931 "Set up ARP reply at %s port %s." % (switch, port_nr))
932
933 def delete_flow_by_cookie(self, cookie):
934 """
935 Removes a flow identified by the cookie
936
937 :param cookie: The cookie for the specified flow
938 :type cookie: ``int``
939 :return: True if successful, else false
940 :rtype: ``bool``
941 """
942 if not cookie:
943 return False
944 logging.debug("Deleting flow by cookie %d" % (cookie))
945 flows = list()
946 # we have to call delete-group for each switch
947 for node in self.net.switches:
948 flow = dict()
949 flow["dpid"] = int(node.dpid, 16)
950 flow["cookie"] = cookie
951 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
952
953 flows.append(flow)
954 for flow in flows:
955 logging.debug("Deleting flowentry with cookie %d" % (
956 flow["cookie"]))
957 if self.net.controller == RemoteController:
958 self.net.ryu_REST('stats/flowentry/delete', data=flow)
959
960 self.cookies.remove(cookie)
961 return True
962
963 def delete_chain_by_intf(self, src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf):
964 """
965 Removes a flow identified by the vnf_name/vnf_intf pairs
966
967 :param src_vnf_name: The vnf name for the specified flow
968 :type src_vnf_name: ``str``
969 :param src_vnf_intf: The interface name for the specified flow
970 :type src_vnf_intf: ``str``
971 :param dst_vnf_name: The vnf name for the specified flow
972 :type dst_vnf_name: ``str``
973 :param dst_vnf_intf: The interface name for the specified flow
974 :type dst_vnf_intf: ``str``
975 :return: True if successful, else false
976 :rtype: ``bool``
977 """
978 logging.debug("Deleting flow for vnf/intf pair %s %s" % (src_vnf_name, src_vnf_intf))
979 if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_intf):
980 return False
981 if not self.check_vnf_intf_pair(dst_vnf_name, dst_vnf_intf):
982 return False
983 target_flow = (src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf)
984 if not target_flow in self.chain_flow_cookies:
985 return False
986
987 success = self.delete_flow_by_cookie(self.chain_flow_cookies[target_flow])
988
989 if success:
990 del self.chain_flow_cookies[target_flow]
991 del self.full_chain_data[target_flow]
992 return True
993 return False
994
995 def delete_loadbalancer(self, vnf_src_name, vnf_src_interface):
996 '''
997 Removes a loadbalancer that is configured for the node and interface
998
999 :param src_vnf_name: Name of the source VNF
1000 :param src_vnf_interface: Name of the destination VNF
1001 '''
1002 flows = list()
1003 # we have to call delete-group for each switch
1004 delete_group = list()
1005 group_id = self.get_flow_group(vnf_src_name, vnf_src_interface)
1006 for node in self.net.switches:
1007 for cookie in self.lb_flow_cookies[(vnf_src_name, vnf_src_interface)]:
1008 flow = dict()
1009 flow["dpid"] = int(node.dpid, 16)
1010 flow["cookie"] = cookie
1011 flow['cookie_mask'] = int('0xffffffffffffffff', 16)
1012
1013 flows.append(flow)
1014 group_del = dict()
1015 group_del["dpid"] = int(node.dpid, 16)
1016 group_del["group_id"] = group_id
1017 delete_group.append(group_del)
1018
1019 for flow in flows:
1020 logging.debug("Deleting flowentry with cookie %d belonging to lb at %s:%s" % (
1021 flow["cookie"], vnf_src_name, vnf_src_interface))
1022 if self.net.controller == RemoteController:
1023 self.net.ryu_REST('stats/flowentry/delete', data=flow)
1024
1025 logging.debug("Deleting group with id %s" % group_id)
1026 for switch_del_group in delete_group:
1027 if self.net.controller == RemoteController:
1028 self.net.ryu_REST("stats/groupentry/delete", data=switch_del_group)
1029
1030 # unmap groupid from the interface
1031 target_pair = (vnf_src_name, vnf_src_interface)
1032 if target_pair in self.flow_groups:
1033 del self.flow_groups[target_pair]
1034 if target_pair in self.full_lb_data:
1035 del self.full_lb_data[target_pair]
1036
1037 def delete_floating_lb(self, cookie):
1038 """
1039 Delete a floating loadbalancer.
1040 Floating loadbalancers are different from normal ones as there are multiple ones on the same interface.
1041 :param cookie: The cookie of the loadbalancer
1042 :type cookie: ``int``
1043 """
1044 cookie = int(cookie)
1045 if cookie not in self.floating_cookies:
1046 raise Exception("Can not delete floating loadbalancer as the flowcookie is not known")
1047
1048 self.delete_flow_by_cookie(cookie)
1049 floating_ip = self.floating_cookies[cookie]
1050 self.floating_network.withdraw_ip_address(floating_ip)
1051
1052 def set_arp_entry(self, vnf_name, vnf_interface, ip, mac):
1053 """
1054 Sets an arp entry on the specified VNF. This is done on the node directly and not by open vswitch!
1055 :param vnf_name: Name of the VNF
1056 :type vnf_name: ``str``
1057 :param vnf_interface: Name of the interface
1058 :type vnf_interface: ``str``
1059 :param ip: IP to reply to
1060 :type ip: ``str``
1061 :param mac: Answer with this MAC
1062 :type mac: ``str``
1063 """
1064 node = self.net.getNodeByName(vnf_name)
1065 node.cmd("arp -i %s -s %s %s" % (vnf_interface, ip, mac))