1 # Copyright 2015-2017 Intel Corporation.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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 """TestCase base class
17 from collections import OrderedDict
27 from datetime import datetime as dt
28 from conf import settings as S
29 from conf import get_test_param, merge_spec
30 import core.component_factory as component_factory
31 from core.loader import Loader
32 from core.results.results_constants import ResultsConstants
33 from tools import tasks
34 from tools import hugepages
35 from tools import functions
36 from tools import namespace
37 from tools import veth
38 from tools.teststepstools import TestStepsTools
40 CHECK_PREFIX = 'validate_'
42 # pylint: disable=too-many-instance-attributes
43 class TestCase(object):
44 """TestCase base class
46 In this basic form runs RFC2544 throughput test
48 # pylint: disable=too-many-statements
49 def __init__(self, test_cfg):
50 """Pull out fields from test config
52 :param test_cfg: A dictionary of string-value pairs describing the test
53 configuration. Both the key and values strings use well-known
55 :param results_dir: Where the csv formatted results are written.
57 # make a local copy of test configuration to avoid modification of
58 # original content used in vsperf main script
59 cfg = copy.deepcopy(test_cfg)
61 self._testcase_start_time = time.time()
62 self._testcase_stop_time = self._testcase_start_time
63 self._hugepages_mounted = False
64 self._traffic_ctl = None
66 self._vswitch_ctl = None
67 self._collector = None
69 self._output_file = None
70 self._tc_results = None
71 self._settings_paths_modified = False
72 self._testcast_run_time = None
74 # initialization of step driven specific members
75 self._step_check = False # by default don't check result for step driven testcases
76 self._step_vnf_list = {}
77 self._step_result = []
78 self._step_result_mapping = {}
79 self._step_status = None
80 self._step_send_traffic = False # indication if send_traffic was called within test steps
81 self._testcase_run_time = None
83 S.setValue('VSWITCH', cfg.get('vSwitch', S.getValue('VSWITCH')))
84 S.setValue('VNF', cfg.get('VNF', S.getValue('VNF')))
85 S.setValue('TRAFFICGEN', cfg.get('Trafficgen', S.getValue('TRAFFICGEN')))
86 test_params = copy.deepcopy(S.getValue('TEST_PARAMS'))
87 tc_test_params = cfg.get('Parameters', S.getValue('TEST_PARAMS'))
88 test_params = merge_spec(test_params, tc_test_params)
89 S.setValue('TEST_PARAMS', test_params)
92 # override all redefined GUEST_ values to have them expanded correctly
93 tmp_test_params = copy.deepcopy(S.getValue('TEST_PARAMS'))
94 for key in tmp_test_params:
95 if key.startswith('GUEST_'):
96 S.setValue(key, S.getValue(key))
97 S.getValue('TEST_PARAMS').pop(key)
99 # update global settings
100 functions.settings_update_paths()
102 # set test parameters; CLI options take precedence to testcase settings
103 self._logger = logging.getLogger(__name__)
104 self.name = cfg['Name']
105 self.desc = cfg.get('Description', 'No description given.')
106 self.test = cfg.get('TestSteps', None)
108 # log testcase name and details
109 tmp_desc = functions.format_description(self.desc, 50)
110 self._logger.info('############################################################')
111 self._logger.info('# Test: %s', self.name)
112 self._logger.info('# Details: %s', tmp_desc[0])
113 for i in range(1, len(tmp_desc)):
114 self._logger.info('# %s', tmp_desc[i])
115 self._logger.info('############################################################')
117 bidirectional = S.getValue('TRAFFIC')['bidir']
118 if not isinstance(S.getValue('TRAFFIC')['bidir'], str):
120 'Bi-dir value must be of type string')
121 bidirectional = bidirectional.title() # Keep things consistent
123 self.deployment = cfg['Deployment']
124 self._frame_mod = cfg.get('Frame Modification', None)
126 self._tunnel_type = None
127 self._tunnel_operation = None
129 if self.deployment == 'op2p':
130 self._tunnel_operation = cfg['Tunnel Operation']
132 if 'Tunnel Type' in cfg:
133 self._tunnel_type = cfg['Tunnel Type']
134 self._tunnel_type = get_test_param('TUNNEL_TYPE',
137 # check if test requires background load and which generator it uses
138 self._load_cfg = cfg.get('Load', None)
141 self._frame_mod = self._frame_mod.lower()
142 self._results_dir = S.getValue('RESULTS_PATH')
144 # set traffic details, so they can be passed to vswitch and traffic ctls
145 self._traffic = copy.deepcopy(S.getValue('TRAFFIC'))
146 self._traffic.update({'bidir': bidirectional,
147 'tunnel_type': self._tunnel_type,})
149 self._traffic = functions.check_traffic(self._traffic)
151 # Packet Forwarding mode
152 self._vswitch_none = str(S.getValue('VSWITCH')).strip().lower() == 'none'
154 # trafficgen configuration required for tests of tunneling protocols
155 if self.deployment == "op2p":
156 self._traffic['l2'].update({'srcmac':
157 S.getValue('TRAFFICGEN_PORT1_MAC'),
159 S.getValue('TRAFFICGEN_PORT2_MAC')})
161 self._traffic['l3'].update({'srcip':
162 S.getValue('TRAFFICGEN_PORT1_IP'),
164 S.getValue('TRAFFICGEN_PORT2_IP')})
166 if self._tunnel_operation == "decapsulation":
167 self._traffic['l2'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L2')
168 self._traffic['l3'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L3')
169 self._traffic['l4'] = S.getValue(self._tunnel_type.upper() + '_FRAME_L4')
170 self._traffic['l2']['dstmac'] = S.getValue('NICS')[1]['mac']
171 elif len(S.getValue('NICS')) and \
172 (S.getValue('NICS')[0]['type'] == 'vf' or
173 S.getValue('NICS')[1]['type'] == 'vf'):
174 mac1 = S.getValue('NICS')[0]['mac']
175 mac2 = S.getValue('NICS')[1]['mac']
177 self._traffic['l2'].update({'srcmac': mac2, 'dstmac': mac1})
179 self._logger.debug("MAC addresses can not be read")
181 # count how many VNFs are involved in TestSteps
183 for step in self.test:
184 if step[0].startswith('vnf'):
185 self._step_vnf_list[step[0]] = None
187 def run_initialize(self):
188 """ Prepare test execution environment
190 # mount hugepages if needed
191 self._mount_hugepages()
193 self._logger.debug("Controllers:")
195 self._traffic_ctl = component_factory.create_traffic(
196 self._traffic['traffic_type'],
197 loader.get_trafficgen_class())
199 self._vnf_ctl = component_factory.create_vnf(
201 loader.get_vnf_class(),
202 len(self._step_vnf_list))
204 # verify enough hugepages are free to run the testcase
205 if not self._check_for_enough_hugepages():
206 raise RuntimeError('Not enough hugepages free to run test.')
208 # perform guest related handling
209 tmp_vm_count = self._vnf_ctl.get_vnfs_number() + len(self._step_vnf_list)
211 # copy sources of l2 forwarding tools into VM shared dir if needed
212 self._copy_fwd_tools_for_all_guests(tmp_vm_count)
214 # in case of multi VM in parallel, set the number of streams to the number of VMs
215 if self.deployment.startswith('pvpv'):
216 # for each VM NIC pair we need an unique stream
218 for vm_nic in S.getValue('GUEST_NICS_NR')[:tmp_vm_count]:
219 streams += int(vm_nic / 2) if vm_nic > 1 else 1
220 self._logger.debug("VMs with parallel connection were detected. "
221 "Thus Number of streams was set to %s", streams)
222 # update streams if needed; In case of additional VNFs deployed by TestSteps
223 # user can define a proper stream count manually
224 if 'multistream' not in self._traffic or self._traffic['multistream'] < streams:
225 self._traffic.update({'multistream': streams})
227 # OVS Vanilla requires guest VM MAC address and IPs to work
228 if 'linux_bridge' in S.getValue('GUEST_LOOPBACK'):
229 self._traffic['l2'].update({'srcmac': S.getValue('VANILLA_TGEN_PORT1_MAC'),
230 'dstmac': S.getValue('VANILLA_TGEN_PORT2_MAC')})
231 self._traffic['l3'].update({'srcip': S.getValue('VANILLA_TGEN_PORT1_IP'),
232 'dstip': S.getValue('VANILLA_TGEN_PORT2_IP')})
234 if self._vswitch_none:
235 self._vswitch_ctl = component_factory.create_pktfwd(
237 loader.get_pktfwd_class())
239 self._vswitch_ctl = component_factory.create_vswitch(
241 loader.get_vswitch_class(),
243 self._tunnel_operation)
245 self._collector = component_factory.create_collector(
246 loader.get_collector_class(),
247 self._results_dir, self.name)
248 self._loadgen = component_factory.create_loadgen(
249 loader.get_loadgen_class(),
252 self._output_file = os.path.join(self._results_dir, "result_" + self.name +
253 "_" + self.deployment + ".csv")
255 self._step_status = {'status' : True, 'details' : ''}
257 self._logger.debug("Setup:")
259 def run_finalize(self):
260 """ Tear down test execution environment and record test results
262 # Stop all VNFs started by TestSteps in case that something went wrong
263 self.step_stop_vnfs()
265 # umount hugepages if mounted
266 self._umount_hugepages()
268 # cleanup any namespaces created
269 if os.path.isdir('/tmp/namespaces'):
270 namespace_list = os.listdir('/tmp/namespaces')
271 if len(namespace_list):
272 self._logger.info('Cleaning up namespaces')
273 for name in namespace_list:
274 namespace.delete_namespace(name)
275 os.rmdir('/tmp/namespaces')
276 # cleanup any veth ports created
277 if os.path.isdir('/tmp/veth'):
278 veth_list = os.listdir('/tmp/veth')
280 self._logger.info('Cleaning up veth ports')
281 for eth in veth_list:
282 port1, port2 = eth.split('-')
283 veth.del_veth_port(port1, port2)
284 os.rmdir('/tmp/veth')
286 def run_report(self):
287 """ Report test results
289 self._logger.debug("self._collector Results:")
290 self._collector.print_results()
292 results = self._traffic_ctl.get_results()
294 self._logger.debug("Traffic Results:")
295 self._traffic_ctl.print_results()
297 if self._tc_results is None:
298 self._tc_results = self._append_results(results)
300 # integration step driven tests have their status and possible
301 # failure details stored inside self._tc_results
302 results = self._append_results(results)
303 if len(self._tc_results) < len(results):
304 if len(self._tc_results) > 1:
305 raise RuntimeError('Testcase results do not match:'
307 'trafficgen results: {}\n',
311 tmp_results = copy.deepcopy(self._tc_results[0])
312 self._tc_results = []
314 tmp_res = copy.deepcopy(tmp_results)
316 self._tc_results.append(tmp_res)
318 for i, result in enumerate(results):
319 self._tc_results[i].update(result)
321 TestCase.write_result_to_file(self._tc_results, self._output_file)
326 All setup and teardown through controllers is included.
328 # prepare test execution environment
329 self.run_initialize()
332 with self._vswitch_ctl, self._loadgen:
333 with self._vnf_ctl, self._collector:
334 if not self._vswitch_none:
337 self._versions += self._vswitch_ctl.get_vswitch().get_version()
339 with self._traffic_ctl:
340 # execute test based on TestSteps definition if needed...
342 # ...and continue with traffic generation, but keep
343 # in mind, that clean deployment does not configure
344 # OVS nor executes the traffic
345 if self.deployment != 'clean' and not self._step_send_traffic:
346 self._traffic_ctl.send_traffic(self._traffic)
348 # dump vswitch flows before they are affected by VNF termination
349 if not self._vswitch_none:
350 self._vswitch_ctl.dump_vswitch_flows()
352 # garbage collection for case that TestSteps modify existing deployment
353 self.step_stop_vnfs()
356 # tear down test execution environment and log results
359 self._testcase_stop_time = time.time()
360 self._testcase_run_time = time.strftime("%H:%M:%S",
361 time.gmtime(self._testcase_stop_time -
362 self._testcase_start_time))
363 logging.info("Testcase execution time: " + self._testcase_run_time)
364 # report test results
367 def _append_results(self, results):
369 Method appends mandatory Test Case results to list of dictionaries.
371 :param results: list of dictionaries which contains results from
374 :returns: modified list of dictionaries.
377 item[ResultsConstants.ID] = self.name
378 item[ResultsConstants.DEPLOYMENT] = self.deployment
379 item[ResultsConstants.VSWITCH] = S.getValue('VSWITCH')
380 item[ResultsConstants.TRAFFIC_TYPE] = self._traffic['l3']['proto']
381 item[ResultsConstants.TEST_RUN_TIME] = self._testcase_run_time
382 # convert timestamps to human readable format
383 item[ResultsConstants.TEST_START_TIME] = dt.fromtimestamp(
384 self._testcase_start_time).strftime('%Y-%m-%d %H:%M:%S')
385 item[ResultsConstants.TEST_STOP_TIME] = dt.fromtimestamp(
386 self._testcase_stop_time).strftime('%Y-%m-%d %H:%M:%S')
387 if self._traffic['multistream']:
388 item[ResultsConstants.SCAL_STREAM_COUNT] = self._traffic['multistream']
389 item[ResultsConstants.SCAL_STREAM_TYPE] = self._traffic['stream_type']
390 item[ResultsConstants.SCAL_PRE_INSTALLED_FLOWS] = self._traffic['pre_installed_flows']
391 if self._vnf_ctl.get_vnfs_number():
392 item[ResultsConstants.GUEST_LOOPBACK] = ' '.join(S.getValue('GUEST_LOOPBACK'))
393 if self._tunnel_type:
394 item[ResultsConstants.TUNNEL_TYPE] = self._tunnel_type
397 def _copy_fwd_tools_for_all_guests(self, vm_count):
398 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR[s] based on selected deployment.
400 # consider only VNFs involved in the test
401 for guest_dir in set(S.getValue('GUEST_SHARE_DIR')[:vm_count]):
402 self._copy_fwd_tools_for_guest(guest_dir)
404 def _copy_fwd_tools_for_guest(self, guest_dir):
405 """Copy dpdk and l2fwd code to GUEST_SHARE_DIR of VM
407 :param index: Index of VM starting from 1 (i.e. 1st VM has index 1)
409 # remove shared dir if it exists to avoid issues with file consistency
410 if os.path.exists(guest_dir):
411 tasks.run_task(['rm', '-f', '-r', guest_dir], self._logger,
412 'Removing content of shared directory...', True)
414 # directory to share files between host and guest
415 os.makedirs(guest_dir)
417 # copy sources into shared dir only if neccessary
418 guest_loopback = set(S.getValue('GUEST_LOOPBACK'))
419 if 'testpmd' in guest_loopback:
421 # exclude whole .git/ subdirectory and all o-files;
422 # It is assumed, that the same RTE_TARGET is used in both host
423 # and VMs; This simplification significantly speeds up testpmd
424 # build. If we will need a different RTE_TARGET in VM,
425 # then we have to build whole DPDK from the scratch in VM.
426 # In that case we can copy just DPDK sources (e.g. by excluding
427 # all items obtained by git status -unormal --porcelain).
428 # NOTE: Excluding RTE_TARGET directory won't help on systems,
429 # where DPDK is built for multiple targets (e.g. for gcc & icc)
431 exclude.append(r'--exclude=.git/')
432 exclude.append(r'--exclude=*.o')
433 tasks.run_task(['rsync', '-a', '-r', '-l'] + exclude +
434 [os.path.join(S.getValue('TOOLS')['dpdk_src'], ''),
435 os.path.join(guest_dir, 'DPDK')],
437 'Copying DPDK to shared directory...',
439 except subprocess.CalledProcessError:
440 self._logger.error('Unable to copy DPDK to shared directory')
442 if 'l2fwd' in guest_loopback:
444 tasks.run_task(['rsync', '-a', '-r', '-l',
445 os.path.join(S.getValue('ROOT_DIR'), 'src/l2fwd/'),
446 os.path.join(guest_dir, 'l2fwd')],
448 'Copying l2fwd to shared directory...',
450 except subprocess.CalledProcessError:
451 self._logger.error('Unable to copy l2fwd to shared directory')
454 def _mount_hugepages(self):
455 """Mount hugepages if usage of DPDK or Qemu is detected
457 # pylint: disable=too-many-boolean-expressions
458 # hugepages are needed by DPDK and Qemu
459 if not self._hugepages_mounted and \
460 (self.deployment.count('v') or \
461 str(S.getValue('VSWITCH')).lower().count('dpdk') or \
462 self._vswitch_none or \
463 self.test and 'vnf' in [step[0][0:3] for step in self.test]):
464 hugepages.mount_hugepages()
465 self._hugepages_mounted = True
467 def _umount_hugepages(self):
468 """Umount hugepages if they were mounted before
470 if self._hugepages_mounted:
471 hugepages.umount_hugepages()
472 self._hugepages_mounted = False
474 def _check_for_enough_hugepages(self):
475 """Check to make sure enough hugepages are free to satisfy the
479 hugepage_size = hugepages.get_hugepage_size()
480 # get hugepage amounts per guest involved in the test
481 for guest in range(self._vnf_ctl.get_vnfs_number()):
482 hugepages_needed += math.ceil((int(S.getValue(
483 'GUEST_MEMORY')[guest]) * 1000) / hugepage_size)
485 # get hugepage amounts for each socket on dpdk
486 sock0_mem, sock1_mem = 0, 0
488 if str(S.getValue('VSWITCH')).lower().count('dpdk'):
489 sock_mem = S.getValue('DPDK_SOCKET_MEM')
490 sock0_mem, sock1_mem = (int(sock_mem[0]) * 1024 / hugepage_size,
491 int(sock_mem[1]) * 1024 / hugepage_size)
493 # If hugepages needed, verify the amounts are free
494 if any([hugepages_needed, sock0_mem, sock1_mem]):
495 free_hugepages = hugepages.get_free_hugepages()
497 logging.info('Need %s hugepages free for guests',
499 result1 = free_hugepages >= hugepages_needed
500 free_hugepages -= hugepages_needed
505 logging.info('Need %s hugepages free for dpdk socket 0',
507 result2 = hugepages.get_free_hugepages('0') >= sock0_mem
508 free_hugepages -= sock0_mem
513 logging.info('Need %s hugepages free for dpdk socket 1',
515 result3 = hugepages.get_free_hugepages('1') >= sock1_mem
516 free_hugepages -= sock1_mem
520 logging.info('Need a total of %s total hugepages',
521 hugepages_needed + sock1_mem + sock0_mem)
523 # The only drawback here is sometimes dpdk doesn't release
524 # its hugepages on a test failure. This could cause a test
525 # to fail when dpdk would be OK to start because it will just
526 # use the previously allocated hugepages.
527 result4 = True if free_hugepages >= 0 else False
529 return all([result1, result2, result3, result4])
534 def write_result_to_file(results, output):
535 """Write list of dictionaries to a CSV file.
537 Each element on list will create separate row in output file.
538 If output file already exists, data will be appended at the end,
539 otherwise it will be created.
541 :param results: list of dictionaries.
542 :param output: path to output file.
544 with open(output, 'a') as csvfile:
546 logging.info("Write results to file: " + output)
547 fieldnames = TestCase._get_unique_keys(results)
549 writer = csv.DictWriter(csvfile, fieldnames)
551 if not csvfile.tell(): # file is now empty
554 for result in results:
555 writer.writerow(result)
558 def _get_unique_keys(list_of_dicts):
559 """Gets unique key values as ordered list of strings in given dicts
561 :param list_of_dicts: list of dictionaries.
563 :returns: list of unique keys(strings).
565 result = OrderedDict()
566 for item in list_of_dicts:
567 for key in item.keys():
570 return list(result.keys())
572 def _add_flows(self):
573 """Add flows to the vswitch
575 vswitch = self._vswitch_ctl.get_vswitch()
576 # NOTE BOM 15-08-07 the frame mod code assumes that the
577 # physical ports are ports 1 & 2. The actual numbers
578 # need to be retrived from the vSwitch and the metadata value
579 # updated accordingly.
580 bridge = S.getValue('VSWITCH_BRIDGE_NAME')
581 if self._frame_mod == "vlan":
582 # 0x8100 => VLAN ethertype
583 self._logger.debug(" **** VLAN ***** ")
584 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
585 'actions': ['push_vlan:0x8100', 'goto_table:3']}
586 vswitch.add_flow(bridge, flow)
587 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
588 'actions': ['push_vlan:0x8100', 'goto_table:3']}
589 vswitch.add_flow(bridge, flow)
590 elif self._frame_mod == "mpls":
591 # 0x8847 => MPLS unicast ethertype
592 self._logger.debug(" **** MPLS ***** ")
593 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
594 'actions': ['push_mpls:0x8847', 'goto_table:3']}
595 vswitch.add_flow(bridge, flow)
596 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
597 'actions': ['push_mpls:0x8847', 'goto_table:3']}
598 vswitch.add_flow(bridge, flow)
599 elif self._frame_mod == "mac":
600 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
601 'actions': ['mod_dl_src:22:22:22:22:22:22',
603 vswitch.add_flow(bridge, flow)
604 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
605 'actions': ['mod_dl_src:11:11:11:11:11:11',
607 vswitch.add_flow(bridge, flow)
608 elif self._frame_mod == "dscp":
609 # DSCP 184d == 0x4E<<2 => 'Expedited Forwarding'
610 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
612 'actions': ['mod_nw_tos:184', 'goto_table:3']}
613 vswitch.add_flow(bridge, flow)
614 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
616 'actions': ['mod_nw_tos:184', 'goto_table:3']}
617 vswitch.add_flow(bridge, flow)
618 elif self._frame_mod == "ttl":
619 # 251 and 241 are the highest prime numbers < 255
620 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
622 'actions': ['mod_nw_ttl:251', 'goto_table:3']}
623 vswitch.add_flow(bridge, flow)
624 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
626 'actions': ['mod_nw_ttl:241', 'goto_table:3']}
627 vswitch.add_flow(bridge, flow)
628 elif self._frame_mod == "ip_addr":
629 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
631 'actions': ['mod_nw_src:10.10.10.10',
632 'mod_nw_dst:20.20.20.20',
634 vswitch.add_flow(bridge, flow)
635 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
637 'actions': ['mod_nw_src:20.20.20.20',
638 'mod_nw_dst:10.10.10.10',
640 vswitch.add_flow(bridge, flow)
641 elif self._frame_mod == "ip_port":
642 # NOTE BOM 15-08-27 The traffic generated is assumed
643 # to be UDP (nw_proto 17d) which is the default case but
644 # we will need to pick up the actual traffic params in use.
645 flow = {'table':'2', 'priority':'1000', 'metadata':'2',
646 'dl_type':'0x0800', 'nw_proto':'17',
647 'actions': ['mod_tp_src:44444',
648 'mod_tp_dst:44444', 'goto_table:3']}
649 vswitch.add_flow(bridge, flow)
650 flow = {'table':'2', 'priority':'1000', 'metadata':'1',
651 'dl_type':'0x0800', 'nw_proto':'17',
652 'actions': ['mod_tp_src:44444',
653 'mod_tp_dst:44444', 'goto_table:3']}
654 vswitch.add_flow(bridge, flow)
660 # TestSteps realted methods
662 def step_report_status(self, label, status):
663 """ Log status of test step
665 self._logger.info("%s ... %s", label, 'OK' if status else 'FAILED')
667 def step_stop_vnfs(self):
668 """ Stop all VNFs started by TestSteps
670 for vnf in self._step_vnf_list:
671 if self._step_vnf_list[vnf]:
672 self._step_vnf_list[vnf].stop()
674 def step_eval_param(self, param, step_result):
675 """ Helper function for #STEP macro evaluation
677 if isinstance(param, str):
678 # evaluate every #STEP reference inside parameter itself
679 macros = re.findall(r'(#STEP\[([\w\-:]+)\]((\[[\w\-\'\"]+\])*))', param)
683 if macro[1] in self._step_result_mapping:
684 key = self._step_result_mapping[macro[1]]
687 # pylint: disable=eval-used
688 tmp_val = str(eval('step_result[{}]{}'.format(key, macro[2])))
689 param = param.replace(macro[0], tmp_val)
691 # evaluate references to vsperf configuration options
692 macros = re.findall(r'\$(([\w\-]+)(\[[\w\[\]\-\'\"]+\])*)', param)
695 # pylint: disable=eval-used
697 tmp_val = str(eval("S.getValue('{}'){}".format(macro[1], macro[2])))
698 param = param.replace('${}'.format(macro[0]), tmp_val)
699 # ignore that required option can't be evaluated
700 except (IndexError, KeyError, AttributeError):
701 self._logger.debug("Skipping %s as it isn't a configuration "
702 "parameter.", '${}'.format(macro[0]))
704 elif isinstance(param, list) or isinstance(param, tuple):
707 tmp_list.append(self.step_eval_param(item, step_result))
709 elif isinstance(param, dict):
711 for (key, value) in param.items():
712 tmp_dict[key] = self.step_eval_param(value, step_result)
717 def step_eval_params(self, params, step_result):
718 """ Evaluates referrences to results from previous steps
721 # evaluate all parameters if needed
723 eval_params.append(self.step_eval_param(param, step_result))
726 # pylint: disable=too-many-locals, too-many-branches, too-many-statements
728 """ Execute actions specified by TestSteps list
730 :return: False if any error was detected
737 # required for VNFs initialization
739 # initialize list with results
740 self._step_result = [None] * len(self.test)
742 # We have to suppress pylint report, because test_object has to be set according
743 # to the test step definition
744 # pylint: disable=redefined-variable-type
745 # run test step by step...
746 for i, step in enumerate(self.test):
747 step_ok = not self._step_check
748 step_check = self._step_check
750 # configure step result mapping if step alias/label is detected
751 if step[0].startswith('#'):
754 raise RuntimeError('Step alias can\'t be an integer value {}'.format(key))
755 if key in self._step_result_mapping:
756 raise RuntimeError('Step alias {} has been used already for step '
757 '{}'.format(key, self._step_result_mapping[key]))
758 self._step_result_mapping[step[0][1:]] = i
761 # store regex filter if it is specified
762 if isinstance(step[-1], str) and step[-1].startswith('|'):
763 # evalute macros and variables used in regex
764 regex = self.step_eval_params([step[-1][1:]], self._step_result[:i])[0]
767 # check if step verification should be suppressed
768 if step[0].startswith('!'):
771 step[0] = step[0][1:]
772 if step[0] == 'vswitch':
773 test_object = self._vswitch_ctl.get_vswitch()
774 elif step[0] == 'namespace':
775 test_object = namespace
776 elif step[0] == 'veth':
778 elif step[0] == 'settings':
780 elif step[0] == 'tools':
781 test_object = TestStepsTools()
782 step[1] = step[1].title()
783 elif step[0] == 'trafficgen':
784 test_object = self._traffic_ctl
785 # in case of send_traffic or send_traffic_async methods, ensure
786 # that specified traffic values are merged with existing self._traffic
787 if step[1].startswith('send_traffic'):
788 tmp_traffic = copy.deepcopy(self._traffic)
789 tmp_traffic.update(step[2])
790 step[2] = tmp_traffic
791 # store indication that traffic has been sent
792 # so it is not sent again after the execution of teststeps
793 self._step_send_traffic = True
794 elif step[0].startswith('vnf'):
795 if not self._step_vnf_list[step[0]]:
797 self._step_vnf_list[step[0]] = loader.get_vnf_class()()
798 test_object = self._step_vnf_list[step[0]]
799 elif step[0] == 'wait':
800 input(os.linesep + "Step {}: Press Enter to continue with "
801 "the next step...".format(i) + os.linesep + os.linesep)
803 elif step[0] == 'sleep':
804 self._logger.debug("Sleep %s seconds", step[1])
805 time.sleep(int(step[1]))
807 elif step[0] == 'log':
808 test_object = self._logger
809 # there isn't a need for validation of log entry
812 elif step[0] == 'pdb':
817 self._logger.error("Unsupported test object %s", step[0])
818 self._step_status = {'status' : False, 'details' : ' '.join(step)}
819 self.step_report_status("Step '{}'".format(' '.join(step)),
820 self._step_status['status'])
823 test_method = getattr(test_object, step[1])
825 test_method_check = getattr(test_object, CHECK_PREFIX + step[1])
827 test_method_check = None
831 # eval parameters, but use only valid step_results
832 # to support negative indexes
833 step_params = self.step_eval_params(step[2:], self._step_result[:i])
834 step_log = '{} {}'.format(' '.join(step[:2]), step_params)
835 step_log += ' filter "{}"'.format(regex) if regex else ''
836 self._logger.debug("Step %s '%s' start", i, step_log)
837 self._step_result[i] = test_method(*step_params)
839 # apply regex to step output
840 self._step_result[i] = functions.filter_output(
841 self._step_result[i], regex)
843 self._logger.debug("Step %s '%s' results '%s'", i,
844 step_log, self._step_result[i])
845 time.sleep(S.getValue('TEST_STEP_DELAY'))
847 step_ok = test_method_check(self._step_result[i], *step_params)
848 except (AssertionError, AttributeError, IndexError) as ex:
850 self._logger.error("Step %s raised %s", i, type(ex).__name__)
853 self.step_report_status("Step {} - '{}'".format(i, step_log), step_ok)
856 self._step_status = {'status' : False, 'details' : step_log}
857 # Stop all VNFs started by TestSteps
858 self.step_stop_vnfs()
861 # all steps processed without any issue
865 # get methods for TestCase members, which needs to be publicly available
867 def get_output_file(self):
868 """Return content of self._output_file member
870 return self._output_file
873 """Return content of self.desc member
877 def get_versions(self):
878 """Return content of self.versions member
880 return self._versions
882 def get_traffic(self):
883 """Return content of self._traffic member
887 def get_tc_results(self):
888 """Return content of self._tc_results member
890 return self._tc_results
892 def get_collector(self):
893 """Return content of self._collector member
895 return self._collector