| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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). |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 26 | import logging |
| 27 | import threading |
| 28 | import uuid |
| 29 | import networkx as nx |
| Rafael Schellenberg | aa8823c | 2019-12-06 15:03:56 +0100 | [diff] [blame^] | 30 | import emuvim.api.openstack.chain_api as chain_api |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 31 | import json |
| 32 | import random |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 33 | from emuvim.api.openstack.resources.net import Net |
| 34 | from emuvim.api.openstack.resources.port import Port |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 35 | from mininet.node import OVSSwitch, RemoteController, Node |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 65 | # to keep track which src_vnf(input port on the switch) handles a load |
| 66 | # balancer |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 78 | # we want one global chain api. this should not be datacenter |
| 79 | # dependent! |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 80 | self.chain = chain_api.ChainApi(ip, port, self) |
| 81 | self.thread = threading.Thread(target=self.chain._start_flask, args=()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 82 | self.thread.name = self.chain.__class__ |
| 83 | self.thread.start() |
| 84 | |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| schillinge | 68d0421 | 2019-03-11 17:57:41 +0100 | [diff] [blame] | 94 | def stop(self): |
| 95 | self.chain.stop() |
| 96 | self.thread.join() |
| 97 | |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 7c16ebc | 2018-04-25 15:36:15 +0200 | [diff] [blame] | 106 | # create default networks |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 107 | self.init_floating_network() |
| 108 | self._net = value |
| 109 | |
| peusterm | 7c16ebc | 2018-04-25 15:36:15 +0200 | [diff] [blame] | 110 | def init_floating_network(self, name="default"): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 7c16ebc | 2018-04-25 15:36:15 +0200 | [diff] [blame] | 117 | fn = self.floating_network = Net(name) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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") |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 127 | # port.id = str(uuid.uuid4()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| Rafael Schellenberg | aa8823c | 2019-12-06 15:03:56 +0100 | [diff] [blame^] | 135 | first_dc = list(self.net.dcs.values())[0] |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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:]) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 144 | self.floating_intf = self.net.addLink( |
| 145 | self.floating_root, self.floating_switch).intf1 |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 146 | self.floating_root.setIP(root_ip, intf=self.floating_intf) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 147 | self.floating_nodes[(self.floating_root.name, |
| 148 | root_ip)] = self.floating_root |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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') |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 237 | flow = (vnf_src_name, vnf_src_interface, |
| 238 | vnf_dst_name, vnf_dst_interface) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 239 | if flow in self.chain_flow_cookies: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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 |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 259 | self.net.setChain( |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 270 | # to keep this logic seperate of the core son-emu do the |
| 271 | # housekeeping here |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 286 | # this might block on containers that are still setting up, so |
| 287 | # start a new thread |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 291 | src_node.setHostRoute(dst_node.intf( |
| 292 | vnf_dst_interface).IP(), vnf_src_interface) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 293 | |
| 294 | try: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 295 | son_emu_data = json.loads( |
| 296 | self.get_son_emu_chain_data(vnf_src_name)) |
| 297 | except BaseException: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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() |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 305 | son_emu_data["son_emu_data"]["interfaces"][vnf_src_interface].append( |
| 306 | dst_intf.IP()) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | """ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 368 | self.net.getNodeByName(vnf_name).cmd( |
| 369 | "echo \'%s\' > /tmp/son_emu_data.json" % json.dumps(data)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 370 | ip_list = [] |
| 371 | for intf in data['son_emu_data']['interfaces'].values(): |
| 372 | ip_list.extend(intf) |
| 373 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 374 | self.net.getNodeByName(vnf_name).cmd( |
| 375 | "echo \'%s\' > /tmp/son_emu_data" % "\n".join(ip_list)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | """ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 386 | return self.net.getNodeByName(vnf_name).cmd( |
| 387 | "cat /tmp/son_emu_data.json") |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 405 | link_dict[link][ |
| 406 | 'src_port_name'] == vnf_interface): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | """ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 430 | # modified version of the _chainAddFlow from |
| 431 | # emuvim.dcemulator.net._chainAddFlow |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 441 | link_dict[link][ |
| 442 | 'src_port_name'] == src_vnf_intf): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 \ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 451 | link_dict[link][ |
| 452 | 'dst_port_name'] == dst_vnf_intf: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 463 | except BaseException: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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]) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 470 | return "No path could be found between {0} and {1}".format( |
| 471 | src_vnf, dst_vnf) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 472 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 473 | logging.info("Shortest path between {0} and {1}: {2}".format( |
| 474 | src_vnf, dst_vnf, path)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 501 | logging.debug("Call to add_loadbalancer at %s intfs:%s" % |
| 502 | (src_vnf_name, src_vnf_interface)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 503 | |
| 504 | if not self.check_vnf_intf_pair(src_vnf_name, src_vnf_interface): |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 505 | raise Exception(u"Source VNF %s or intfs %s does not exist" % ( |
| 506 | src_vnf_name, src_vnf_interface)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 507 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 508 | # find the switch belonging to the source interface, as well as the |
| 509 | # inport nr |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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]: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 529 | dest_vnf_outport_nrs.append( |
| 530 | int(link_dict[link]['dst_port_nr'])) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 556 | lb_mac = "31:33:70:%02x:%02x:%02x" % (random.randint( |
| 557 | 0, 255), random.randint(0, 255), random.randint(0, 255)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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] |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 579 | logging.debug("Taking custom path from %s to %s: %s" % ( |
| 580 | src_vnf_name, dst_vnf_name, path)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 584 | raise Exception(u"VNF %s or intfs %s does not exist" % |
| 585 | (dst_vnf_name, dst_vnf_interface)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 586 | if isinstance(path, dict): |
| 587 | self.delete_loadbalancer(src_vnf_name, src_vnf_interface) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 588 | raise Exception( |
| 589 | u"Can not find a valid path. Are you specifying the right interfaces?.") |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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): |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 634 | logging.info( |
| 635 | "Next node: {0} is not a switch".format(next_hop)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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 |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 721 | self.setup_arp_reply_at( |
| 722 | current_hop, switch_outport_nr, src_ip, src_mac, cookie=cookie) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 739 | # set up the actual load balancing rule as a multipath on the very |
| 740 | # first switch |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 750 | cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len( |
| 751 | dest_intfs_mapping) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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]: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 796 | dest_vnf_outport_nrs.append( |
| 797 | int(link_dict[link]['dst_port_nr'])) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 798 | |
| 799 | if len(dest_vnf_outport_nrs) == 0: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 800 | raise Exception( |
| 801 | "There are no paths specified for the loadbalancer") |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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" |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 809 | floating_ip = self.floating_network.get_new_ip_address( |
| 810 | "floating-ip").split("/")[0] |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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] |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 819 | logging.debug("Taking custom path to %s: %s" % |
| 820 | (dst_vnf_name, path)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 821 | else: |
| 822 | if datacenter not in self.floating_links: |
| 823 | self.floating_links[datacenter] = \ |
| 824 | net.addLink(self.floating_switch, datacenter) |
| 825 | path = \ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 826 | self._get_path(self.floating_root.name, dst_vnf_name, |
| 827 | self.floating_intf.name, dst_vnf_interface)[0] |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 828 | |
| 829 | if isinstance(path, dict): |
| 830 | self.delete_flow_by_cookie(cookie) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 831 | raise Exception( |
| 832 | u"Can not find a valid path. Are you specifying the right interfaces?.") |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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): |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 856 | logging.info( |
| 857 | "Next node: {0} is not a switch".format(next_hop)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 890 | self.setup_arp_reply_at( |
| 891 | current_hop, src_sw_inport_nr, floating_ip, target_mac, cookie=cookie) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 912 | net.getNodeByName(dst_vnf_name).setHostRoute( |
| 913 | src_ip, dst_vnf_interface) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 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) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 941 | # set up the actual load balancing rule as a multipath on the very |
| 942 | # first switch |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 952 | cmd += ',multipath(symmetric_l4,1024,modulo_n,%s,0,NXM_NX_REG1[0..12])' % len( |
| 953 | dest_intfs_mapping) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 965 | def setup_arp_reply_at(self, switch, port_nr, |
| 966 | target_ip, target_mac, cookie=None): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 988 | # first set up ARP requests for the source node, so it will always |
| 989 | # 'find' a partner |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1046 | def delete_chain_by_intf( |
| 1047 | self, src_vnf_name, src_vnf_intf, dst_vnf_name, dst_vnf_intf): |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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 | """ |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1062 | logging.debug("Deleting flow for vnf/intf pair %s %s" % |
| 1063 | (src_vnf_name, src_vnf_intf)) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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) |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1069 | if target_flow not in self.chain_flow_cookies: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 1070 | return False |
| 1071 | |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1072 | success = self.delete_flow_by_cookie( |
| 1073 | self.chain_flow_cookies[target_flow]) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1093 | for cookie in self.lb_flow_cookies[( |
| 1094 | vnf_src_name, vnf_src_interface)]: |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1115 | self.net.ryu_REST("stats/groupentry/delete", |
| 1116 | data=switch_del_group) |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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: |
| peusterm | 72f0988 | 2018-05-15 17:10:27 +0200 | [diff] [blame] | 1134 | raise Exception( |
| 1135 | "Can not delete floating loadbalancer as the flowcookie is not known") |
| peusterm | 0019978 | 2017-05-17 08:48:12 +0200 | [diff] [blame] | 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)) |