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