add yardstick iruya 9.0.0 release notes
[yardstick.git] / yardstick / benchmark / scenarios / networking / vnf_generic.py
1 # Copyright (c) 2016-2019 Intel Corporation
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 import copy
16 import ipaddress
17 from itertools import chain
18 import logging
19 import os
20 import sys
21 import time
22
23 import six
24 import yaml
25
26 from yardstick.benchmark.contexts import base as context_base
27 from yardstick.benchmark.scenarios import base as scenario_base
28 from yardstick.common.constants import LOG_DIR
29 from yardstick.common import exceptions
30 from yardstick.common.process import terminate_children
31 from yardstick.common import utils
32 from yardstick.network_services.collector.subscriber import Collector
33 from yardstick.network_services.vnf_generic import vnfdgen
34 from yardstick.network_services.vnf_generic.vnf.base import GenericVNF
35 from yardstick.network_services import traffic_profile
36 from yardstick.network_services.traffic_profile import base as tprofile_base
37 from yardstick.network_services.utils import get_nsb_option
38 from yardstick import ssh
39
40
41 traffic_profile.register_modules()
42
43
44 LOG = logging.getLogger(__name__)
45
46
47 class NetworkServiceBase(scenario_base.Scenario):
48     """Base class for Network service testing scenarios"""
49
50     __scenario_type__ = ""
51
52     def __init__(self, scenario_cfg, context_cfg):  # pragma: no cover
53         super(NetworkServiceBase, self).__init__()
54         self.scenario_cfg = scenario_cfg
55         self.context_cfg = context_cfg
56
57         self._render_topology()
58         self.vnfs = []
59         self.collector = None
60         self.traffic_profile = None
61         self.node_netdevs = {}
62         self.bin_path = get_nsb_option('bin_path', '')
63
64     def run(self, *args):
65         pass
66
67     def teardown(self):
68         """ Stop the collector and terminate VNF & TG instance
69
70         :return
71         """
72
73         try:
74             try:
75                 self.collector.stop()
76                 for vnf in self.vnfs:
77                     LOG.info("Stopping %s", vnf.name)
78                     vnf.terminate()
79                 LOG.debug("all VNFs terminated: %s", ", ".join(vnf.name for vnf in self.vnfs))
80             finally:
81                 terminate_children()
82         except Exception:
83             # catch any exception in teardown and convert to simple exception
84             # never pass exceptions back to multiprocessing, because some exceptions can
85             # be unpicklable
86             # https://bugs.python.org/issue9400
87             LOG.exception("")
88             raise RuntimeError("Error in teardown")
89
90     def is_ended(self):
91         return self.traffic_profile is not None and self.traffic_profile.is_ended()
92
93     def _get_ip_flow_range(self, ip_start_range):
94         """Retrieve a CIDR first and last viable IPs
95
96         :param ip_start_range: could be the IP range itself or a dictionary
97                with the host name and the port.
98         :return: (str) IP range (min, max) with this format "x.x.x.x-y.y.y.y"
99         """
100         if isinstance(ip_start_range, six.string_types):
101             return ip_start_range
102
103         node_name, range_or_interface = next(iter(ip_start_range.items()),
104                                              (None, '0.0.0.0'))
105         if node_name is None:
106             return range_or_interface
107
108         node = self.context_cfg['nodes'].get(node_name, {})
109         interface = node.get('interfaces', {}).get(range_or_interface)
110         if interface:
111             ip = interface['local_ip']
112             mask = interface['netmask']
113         else:
114             ip = '0.0.0.0'
115             mask = '255.255.255.0'
116
117         ipaddr = ipaddress.ip_network(
118             six.text_type('{}/{}'.format(ip, mask)), strict=False)
119         if ipaddr.prefixlen + 2 < ipaddr.max_prefixlen:
120             ip_addr_range = '{}-{}'.format(ipaddr[2], ipaddr[-2])
121         else:
122             LOG.warning('Only single IP in range %s', ipaddr)
123             ip_addr_range = ip
124         return ip_addr_range
125
126     def _get_traffic_flow(self):
127         flow = {}
128         try:
129             # TODO: should be .0  or .1 so we can use list
130             # but this also roughly matches uplink_0, downlink_0
131             fflow = self.scenario_cfg["options"]["flow"]
132             for index, src in enumerate(fflow.get("src_ip", [])):
133                 flow["src_ip_{}".format(index)] = self._get_ip_flow_range(src)
134
135             for index, dst in enumerate(fflow.get("dst_ip", [])):
136                 flow["dst_ip_{}".format(index)] = self._get_ip_flow_range(dst)
137
138             for index, publicip in enumerate(fflow.get("public_ip", [])):
139                 flow["public_ip_{}".format(index)] = publicip
140
141             for index, src_port in enumerate(fflow.get("src_port", [])):
142                 flow["src_port_{}".format(index)] = src_port
143
144             for index, dst_port in enumerate(fflow.get("dst_port", [])):
145                 flow["dst_port_{}".format(index)] = dst_port
146
147             if "count" in fflow:
148                 flow["count"] = fflow["count"]
149
150             if "srcseed" in fflow:
151                 flow["srcseed"] = fflow["srcseed"]
152
153             if "dstseed" in fflow:
154                 flow["dstseed"] = fflow["dstseed"]
155
156         except KeyError:
157             flow = {}
158         return {"flow": flow}
159
160     def _get_traffic_imix(self):
161         try:
162             imix = {"imix": self.scenario_cfg['options']['framesize']}
163         except KeyError:
164             imix = {}
165         return imix
166
167     def _get_ip_priority(self):
168         try:
169             priority = self.scenario_cfg['options']['priority']
170         except KeyError:
171             priority = {}
172         return priority
173
174     def _get_traffic_profile(self):
175         profile = self.scenario_cfg["traffic_profile"]
176         path = self.scenario_cfg["task_path"]
177         with utils.open_relative_file(profile, path) as infile:
178             return infile.read()
179
180     def _get_duration(self):
181         options = self.scenario_cfg.get('options', {})
182         return options.get('duration',
183                            tprofile_base.TrafficProfileConfig.DEFAULT_DURATION)
184
185     def _key_list_to_dict(self, key, value_list):
186         value_dict = {}
187         try:
188             for index, count in enumerate(value_list[key]):
189                 value_dict["{}_{}".format(key, index)] = count
190         except KeyError:
191             value_dict = {}
192
193         return value_dict
194
195     def _get_simulated_users(self):
196         users = self.scenario_cfg.get("options", {}).get("simulated_users", {})
197         simulated_users = self._key_list_to_dict("uplink", users)
198         return {"simulated_users": simulated_users}
199
200     def _get_page_object(self):
201         objects = self.scenario_cfg.get("options", {}).get("page_object", {})
202         page_object = self._key_list_to_dict("uplink", objects)
203         return {"page_object": page_object}
204
205     def _fill_traffic_profile(self):
206         tprofile = self._get_traffic_profile()
207         extra_args = self.scenario_cfg.get('extra_args', {})
208         tprofile_data = {
209             'flow': self._get_traffic_flow(),
210             'imix': self._get_traffic_imix(),
211             'priority': self._get_ip_priority(),
212             tprofile_base.TrafficProfile.UPLINK: {},
213             tprofile_base.TrafficProfile.DOWNLINK: {},
214             'extra_args': extra_args,
215             'duration': self._get_duration(),
216             'page_object': self._get_page_object(),
217             'simulated_users': self._get_simulated_users()}
218         traffic_vnfd = vnfdgen.generate_vnfd(tprofile, tprofile_data)
219
220         traffic_config = \
221             self.scenario_cfg.get("options", {}).get("traffic_config", {})
222
223         traffic_vnfd.setdefault("traffic_profile", {})
224         traffic_vnfd["traffic_profile"].update(traffic_config)
225
226         self.traffic_profile = \
227             tprofile_base.TrafficProfile.get(traffic_vnfd)
228
229     def _get_topology(self):
230         topology = self.scenario_cfg["topology"]
231         path = self.scenario_cfg["task_path"]
232         with utils.open_relative_file(topology, path) as infile:
233             return infile.read()
234
235     def _render_topology(self):
236         topology = self._get_topology()
237         topology_args = self.scenario_cfg.get('extra_args', {})
238         topolgy_data = {
239             'extra_args': topology_args
240         }
241         topology_yaml = vnfdgen.generate_vnfd(topology, topolgy_data)
242         self.topology = topology_yaml["nsd:nsd-catalog"]["nsd"][0]
243
244     def _find_vnf_name_from_id(self, vnf_id):  # pragma: no cover
245         return next((vnfd["vnfd-id-ref"]
246                      for vnfd in self.topology["constituent-vnfd"]
247                      if vnf_id == vnfd["member-vnf-index"]), None)
248
249     def _find_vnfd_from_vnf_idx(self, vnf_id):  # pragma: no cover
250         return next((vnfd
251                      for vnfd in self.topology["constituent-vnfd"]
252                      if vnf_id == vnfd["member-vnf-index"]), None)
253
254     @staticmethod
255     def find_node_if(nodes, name, if_name, vld_id):  # pragma: no cover
256         try:
257             # check for xe0, xe1
258             intf = nodes[name]["interfaces"][if_name]
259         except KeyError:
260             # if not xe0, then maybe vld_id,  uplink_0, downlink_0
261             # pop it and re-insert with the correct name from topology
262             intf = nodes[name]["interfaces"].pop(vld_id)
263             nodes[name]["interfaces"][if_name] = intf
264         return intf
265
266     def _resolve_topology(self):
267         for vld in self.topology["vld"]:
268             try:
269                 node0_data, node1_data = vld["vnfd-connection-point-ref"]
270             except (ValueError, TypeError):
271                 raise exceptions.IncorrectConfig(
272                     error_msg='Topology file corrupted, wrong endpoint count '
273                               'for connection')
274
275             node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
276             node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
277
278             node0_if_name = node0_data["vnfd-connection-point-ref"]
279             node1_if_name = node1_data["vnfd-connection-point-ref"]
280
281             try:
282                 nodes = self.context_cfg["nodes"]
283                 node0_if = self.find_node_if(nodes, node0_name, node0_if_name, vld["id"])
284                 node1_if = self.find_node_if(nodes, node1_name, node1_if_name, vld["id"])
285
286                 # names so we can do reverse lookups
287                 node0_if["ifname"] = node0_if_name
288                 node1_if["ifname"] = node1_if_name
289
290                 node0_if["node_name"] = node0_name
291                 node1_if["node_name"] = node1_name
292
293                 node0_if["vld_id"] = vld["id"]
294                 node1_if["vld_id"] = vld["id"]
295
296                 # set peer name
297                 node0_if["peer_name"] = node1_name
298                 node1_if["peer_name"] = node0_name
299
300                 # set peer interface name
301                 node0_if["peer_ifname"] = node1_if_name
302                 node1_if["peer_ifname"] = node0_if_name
303
304                 # just load the network
305                 vld_networks = {n.get('vld_id', name): n for name, n in
306                                 self.context_cfg["networks"].items()}
307
308                 node0_if["network"] = vld_networks.get(vld["id"], {})
309                 node1_if["network"] = vld_networks.get(vld["id"], {})
310
311                 node0_if["dst_mac"] = node1_if["local_mac"]
312                 node0_if["dst_ip"] = node1_if["local_ip"]
313
314                 node1_if["dst_mac"] = node0_if["local_mac"]
315                 node1_if["dst_ip"] = node0_if["local_ip"]
316
317             except KeyError:
318                 LOG.exception("")
319                 raise exceptions.IncorrectConfig(
320                     error_msg='Required interface not found, topology file '
321                               'corrupted')
322
323         for vld in self.topology['vld']:
324             try:
325                 node0_data, node1_data = vld["vnfd-connection-point-ref"]
326             except (ValueError, TypeError):
327                 raise exceptions.IncorrectConfig(
328                     error_msg='Topology file corrupted, wrong endpoint count '
329                               'for connection')
330
331             node0_name = self._find_vnf_name_from_id(node0_data["member-vnf-index-ref"])
332             node1_name = self._find_vnf_name_from_id(node1_data["member-vnf-index-ref"])
333
334             node0_if_name = node0_data["vnfd-connection-point-ref"]
335             node1_if_name = node1_data["vnfd-connection-point-ref"]
336
337             nodes = self.context_cfg["nodes"]
338             node0_if = self.find_node_if(nodes, node0_name, node0_if_name, vld["id"])
339             node1_if = self.find_node_if(nodes, node1_name, node1_if_name, vld["id"])
340
341             # add peer interface dict, but remove circular link
342             # TODO: don't waste memory
343             node0_copy = node0_if.copy()
344             node1_copy = node1_if.copy()
345             node0_if["peer_intf"] = node1_copy
346             node1_if["peer_intf"] = node0_copy
347
348     def _update_context_with_topology(self):  # pragma: no cover
349         for vnfd in self.topology["constituent-vnfd"]:
350             vnf_idx = vnfd["member-vnf-index"]
351             vnf_name = self._find_vnf_name_from_id(vnf_idx)
352             vnfd = self._find_vnfd_from_vnf_idx(vnf_idx)
353             self.context_cfg["nodes"][vnf_name].update(vnfd)
354
355     def _generate_pod_yaml(self):  # pragma: no cover
356         context_yaml = os.path.join(LOG_DIR, "pod-{}.yaml".format(self.scenario_cfg['task_id']))
357         # convert OrderedDict to a list
358         # pod.yaml nodes is a list
359         nodes = [self._serialize_node(node) for node in self.context_cfg["nodes"].values()]
360         pod_dict = {
361             "nodes": nodes,
362             "networks": self.context_cfg["networks"]
363         }
364         with open(context_yaml, "w") as context_out:
365             yaml.safe_dump(pod_dict, context_out, default_flow_style=False,
366                            explicit_start=True)
367
368     @staticmethod
369     def _serialize_node(node):  # pragma: no cover
370         new_node = copy.deepcopy(node)
371         # name field is required
372         # remove context suffix
373         new_node["name"] = node['name'].split('.')[0]
374         try:
375             new_node["pkey"] = ssh.convert_key_to_str(node["pkey"])
376         except KeyError:
377             pass
378         return new_node
379
380     def map_topology_to_infrastructure(self):
381         """ This method should verify if the available resources defined in pod.yaml
382         match the topology.yaml file.
383
384         :return: None. Side effect: context_cfg is updated
385         """
386         # 3. Use topology file to find connections & resolve dest address
387         self._resolve_topology()
388         self._update_context_with_topology()
389
390     @classmethod
391     def get_vnf_impl(cls, vnf_model_id):  # pragma: no cover
392         """ Find the implementing class from vnf_model["vnf"]["name"] field
393
394         :param vnf_model_id: parsed vnfd model ID field
395         :return: subclass of GenericVNF
396         """
397         utils.import_modules_from_package(
398             "yardstick.network_services.vnf_generic.vnf")
399         expected_name = vnf_model_id
400         classes_found = []
401
402         def impl():
403             for name, class_ in ((c.__name__, c) for c in
404                                  utils.itersubclasses(GenericVNF)):
405                 if name == expected_name:
406                     yield class_
407                 classes_found.append(name)
408
409         try:
410             return next(impl())
411         except StopIteration:
412             pass
413
414         message = ('No implementation for %s found in %s'
415                    % (expected_name, classes_found))
416         raise exceptions.IncorrectConfig(error_msg=message)
417
418     @staticmethod
419     def create_interfaces_from_node(vnfd, node):  # pragma: no cover
420         ext_intfs = vnfd["vdu"][0]["external-interface"] = []
421         # have to sort so xe0 goes first
422         for intf_name, intf in sorted(node['interfaces'].items()):
423             # only interfaces with vld_id are added.
424             # Thus there are two layers of filters, only intefaces with vld_id
425             # show up in interfaces, and only interfaces with traffic profiles
426             # are used by the generators
427             if intf.get('vld_id'):
428                 # force dpkd_port_num to int so we can do reverse lookup
429                 try:
430                     intf['dpdk_port_num'] = int(intf['dpdk_port_num'])
431                 except KeyError:
432                     pass
433                 ext_intf = {
434                     "name": intf_name,
435                     "virtual-interface": intf,
436                     "vnfd-connection-point-ref": intf_name,
437                 }
438                 ext_intfs.append(ext_intf)
439
440     def load_vnf_models(self, scenario_cfg=None, context_cfg=None):
441         """ Create VNF objects based on YAML descriptors
442
443         :param scenario_cfg:
444         :type scenario_cfg:
445         :param context_cfg:
446         :return:
447         """
448         trex_lib_path = get_nsb_option('trex_client_lib')
449         sys.path[:] = list(chain([trex_lib_path], (x for x in sys.path if x != trex_lib_path)))
450
451         if scenario_cfg is None:
452             scenario_cfg = self.scenario_cfg
453
454         if context_cfg is None:
455             context_cfg = self.context_cfg
456
457         vnfs = []
458         # we assume OrderedDict for consistency in instantiation
459         for node_name, node in context_cfg["nodes"].items():
460             LOG.debug(node)
461             try:
462                 file_name = node["VNF model"]
463             except KeyError:
464                 LOG.debug("no model for %s, skipping", node_name)
465                 continue
466             file_path = scenario_cfg['task_path']
467             with utils.open_relative_file(file_name, file_path) as stream:
468                 vnf_model = stream.read()
469             vnfd = vnfdgen.generate_vnfd(vnf_model, node)
470             # TODO: here add extra context_cfg["nodes"] regardless of template
471             vnfd = vnfd["vnfd:vnfd-catalog"]["vnfd"][0]
472             # force inject pkey if it exists
473             # we want to standardize Heat using pkey as a string so we don't rely
474             # on the filesystem
475             try:
476                 vnfd['mgmt-interface']['pkey'] = node['pkey']
477             except KeyError:
478                 pass
479             self.create_interfaces_from_node(vnfd, node)
480             vnf_impl = self.get_vnf_impl(vnfd['id'])
481             vnf_instance = vnf_impl(node_name, vnfd)
482             vnfs.append(vnf_instance)
483
484         self.vnfs = vnfs
485         return vnfs
486
487     def pre_run_wait_time(self, time_seconds):  # pragma: no cover
488         """Time waited before executing the run method"""
489         time.sleep(time_seconds)
490
491     def post_run_wait_time(self, time_seconds):  # pragma: no cover
492         """Time waited after executing the run method"""
493         pass
494
495
496 class NetworkServiceTestCase(NetworkServiceBase):
497     """Class handles Generic framework to do pre-deployment VNF &
498        Network service testing  """
499
500     __scenario_type__ = "NSPerf"
501
502     def __init__(self, scenario_cfg, context_cfg):  # pragma: no cover
503         super(NetworkServiceTestCase, self).__init__(scenario_cfg, context_cfg)
504
505     def setup(self):
506         """Setup infrastructure, provission VNFs & start traffic"""
507         # 1. Verify if infrastructure mapping can meet topology
508         self.map_topology_to_infrastructure()
509         # 1a. Load VNF models
510         self.load_vnf_models()
511         # 1b. Fill traffic profile with information from topology
512         self._fill_traffic_profile()
513
514         # 2. Provision VNFs
515
516         # link events will cause VNF application to exit
517         # so we should start traffic runners before VNFs
518         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
519         non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
520         try:
521             for vnf in chain(traffic_runners, non_traffic_runners):
522                 LOG.info("Instantiating %s", vnf.name)
523                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
524                 LOG.info("Waiting for %s to instantiate", vnf.name)
525                 vnf.wait_for_instantiate()
526         except:
527             LOG.exception("")
528             for vnf in self.vnfs:
529                 vnf.terminate()
530             raise
531
532         # we have to generate pod.yaml here after VNF has probed so we know vpci and driver
533         self._generate_pod_yaml()
534
535         # 3. Run experiment
536         # Start listeners first to avoid losing packets
537         for traffic_gen in traffic_runners:
538             traffic_gen.listen_traffic(self.traffic_profile)
539
540         # register collector with yardstick for KPI collection.
541         self.collector = Collector(self.vnfs, context_base.Context.get_physical_nodes())
542         self.collector.start()
543
544         # Start the actual traffic
545         for traffic_gen in traffic_runners:
546             LOG.info("Starting traffic on %s", traffic_gen.name)
547             traffic_gen.run_traffic(self.traffic_profile)
548
549     def run(self, result):  # yardstick API
550         """ Yardstick calls run() at intervals defined in the yaml and
551             produces timestamped samples
552
553         :param result: dictionary with results to update
554         :return: None
555         """
556
557         # this is the only method that is check from the runner
558         # so if we have any fatal error it must be raised via these methods
559         # otherwise we will not terminate
560
561         result.update(self.collector.get_kpi())
562
563
564 class NetworkServiceRFC2544(NetworkServiceBase):
565     """Class handles RFC2544 Network service testing"""
566
567     __scenario_type__ = "NSPerf-RFC2544"
568
569     def __init__(self, scenario_cfg, context_cfg):  # pragma: no cover
570         super(NetworkServiceRFC2544, self).__init__(scenario_cfg, context_cfg)
571
572     def setup(self):
573         """Setup infrastructure, provision VNFs"""
574         self.map_topology_to_infrastructure()
575         self.load_vnf_models()
576
577         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
578         non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
579         try:
580             for vnf in chain(traffic_runners, non_traffic_runners):
581                 LOG.info("Instantiating %s", vnf.name)
582                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
583                 LOG.info("Waiting for %s to instantiate", vnf.name)
584                 vnf.wait_for_instantiate()
585         except:
586             LOG.exception("")
587             for vnf in self.vnfs:
588                 vnf.terminate()
589             raise
590
591         self._generate_pod_yaml()
592
593     def run(self, output):
594         """ Run experiment
595
596         :param output: scenario output to push results
597         :return: None
598         """
599
600         self._fill_traffic_profile()
601
602         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
603
604         for traffic_gen in traffic_runners:
605             traffic_gen.listen_traffic(self.traffic_profile)
606
607         self.collector = Collector(self.vnfs,
608                                    context_base.Context.get_physical_nodes())
609         self.collector.start()
610
611         test_completed = False
612         while not test_completed:
613             for traffic_gen in traffic_runners:
614                 LOG.info("Run traffic on %s", traffic_gen.name)
615                 traffic_gen.run_traffic_once(self.traffic_profile)
616
617             test_completed = True
618             for traffic_gen in traffic_runners:
619                 # wait for all tg to complete running traffic
620                 status = traffic_gen.wait_on_traffic()
621                 LOG.info("Run traffic on %s complete status=%s",
622                          traffic_gen.name, status)
623                 if status == 'CONTINUE':
624                     # continue running if at least one tg is running
625                     test_completed = False
626
627             output.push(self.collector.get_kpi())
628
629         self.collector.stop()
630
631 class NetworkServiceRFC3511(NetworkServiceBase):
632     """Class handles RFC3511 Network service testing"""
633
634     __scenario_type__ = "NSPerf-RFC3511"
635
636     def __init__(self, scenario_cfg, context_cfg):  # pragma: no cover
637         super(NetworkServiceRFC3511, self).__init__(scenario_cfg, context_cfg)
638
639     def setup(self):
640         """Setup infrastructure, provision VNFs"""
641         self.map_topology_to_infrastructure()
642         self.load_vnf_models()
643
644         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
645         non_traffic_runners = [vnf for vnf in self.vnfs if not vnf.runs_traffic]
646         try:
647             for vnf in chain(traffic_runners, non_traffic_runners):
648                 LOG.info("Instantiating %s", vnf.name)
649                 vnf.instantiate(self.scenario_cfg, self.context_cfg)
650                 LOG.info("Waiting for %s to instantiate", vnf.name)
651                 vnf.wait_for_instantiate()
652         except:
653             LOG.exception("")
654             for vnf in self.vnfs:
655                 vnf.terminate()
656             raise
657
658         self._generate_pod_yaml()
659
660     def run(self, output):
661         """ Run experiment
662
663         :param output: scenario output to push results
664         :return: None
665         """
666
667         self._fill_traffic_profile()
668
669         traffic_runners = [vnf for vnf in self.vnfs if vnf.runs_traffic]
670
671         for traffic_gen in traffic_runners:
672             traffic_gen.listen_traffic(self.traffic_profile)
673
674         self.collector = Collector(self.vnfs,
675                                    context_base.Context.get_physical_nodes())
676         self.collector.start()
677
678         for traffic_gen in traffic_runners:
679             LOG.info("Run traffic on %s", traffic_gen.name)
680             traffic_gen.run_traffic(self.traffic_profile)
681
682         output.push(self.collector.get_kpi())
683
684         self.collector.stop()