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