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