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