1 # Copyright 2016 Cisco Systems, Inc. All rights reserved.
3 # Licensed under the Apache License, Version 2.0 (the "License"); you may
4 # not use this file except in compliance with the License. You may obtain
5 # 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, WITHOUT
11 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 # License for the specific language governing permissions and limitations
15 from attrdict import AttrDict
17 from datetime import datetime
19 from netaddr import IPNetwork
20 from network import Interface
22 from specs import ChainType
23 from stats_collector import IntervalCollector
24 from stats_collector import IterationCollector
27 import traffic_gen.traffic_utils as utils
28 from trex_stl_lib.api import STLError
29 from utils import cast_integer
32 class TrafficClientException(Exception):
36 class TrafficRunner(object):
38 def __init__(self, client, duration_sec, interval_sec=0):
40 self.start_time = None
41 self.duration_sec = duration_sec
42 self.interval_sec = interval_sec
45 LOG.info('Running traffic generator')
46 self.client.gen.clear_stats()
47 self.client.gen.start_traffic()
48 self.start_time = time.time()
49 return self.poll_stats()
53 self.start_time = None
54 self.client.gen.stop_traffic()
57 return self.start_time is not None
59 def time_elapsed(self):
61 return time.time() - self.start_time
63 return self.duration_sec
66 if not self.is_running():
68 time_elapsed = self.time_elapsed()
69 if time_elapsed > self.duration_sec:
72 time_left = self.duration_sec - time_elapsed
73 if self.interval_sec > 0.0:
74 if time_left <= self.interval_sec:
78 time.sleep(self.interval_sec)
80 time.sleep(self.duration_sec)
82 return self.client.get_stats()
87 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
88 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
89 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
90 chain_count=1, flow_count=1, vlan_tagging=False):
91 self.chain_count = chain_count
92 self.flow_count = flow_count
95 self.switch_port = switch_port
96 self.vtep_vlan = vtep_vlan
98 self.vlan_tagging = vlan_tagging
101 self.vm_mac_list = None
102 subnet = IPNetwork(ip)
103 self.ip = subnet.ip.format()
104 self.ip_prefixlen = subnet.prefixlen
105 self.ip_addrs_step = ip_addrs_step
106 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
107 self.gateway_ip_addrs_step = gateway_ip_addrs_step
108 self.ip_list = self.expand_ip(self.ip, self.ip_addrs_step, self.flow_count)
109 self.gateway_ip = gateway_ip
110 self.gateway_ip_list = self.expand_ip(self.gateway_ip,
111 self.gateway_ip_addrs_step,
113 self.tg_gateway_ip = tg_gateway_ip
114 self.tg_gateway_ip_list = self.expand_ip(self.tg_gateway_ip,
115 self.tg_gateway_ip_addrs_step,
117 self.udp_src_port = udp_src_port
118 self.udp_dst_port = udp_dst_port
120 def set_mac(self, mac):
122 raise TrafficClientException('Trying to set traffic generator MAC address as None')
125 def set_destination(self, dst):
128 def set_vm_mac_list(self, vm_mac_list):
129 self.vm_mac_list = map(str, vm_mac_list)
131 def set_vlan_tag(self, vlan_tag):
132 if self.vlan_tagging and vlan_tag is None:
133 raise TrafficClientException('Trying to set VLAN tag as None')
134 self.vlan_tag = vlan_tag
136 def get_stream_configs(self, service_chain):
139 for chain_idx in xrange(self.chain_count):
140 current_flow_count = (self.flow_count - flow_idx) / (self.chain_count - chain_idx)
141 max_idx = flow_idx + current_flow_count - 1
142 ip_src_count = self.ip_to_int(self.ip_list[max_idx]) - \
143 self.ip_to_int(self.ip_list[flow_idx]) + 1
144 ip_dst_count = self.ip_to_int(self.dst.ip_list[max_idx]) - \
145 self.ip_to_int(self.dst.ip_list[flow_idx]) + 1
148 'count': current_flow_count,
150 'mac_dst': self.dst.mac if service_chain == ChainType.EXT
151 else self.vm_mac_list[chain_idx],
152 'ip_src_addr': self.ip_list[flow_idx],
153 'ip_src_addr_max': self.ip_list[max_idx],
154 'ip_src_count': ip_src_count,
155 'ip_dst_addr': self.dst.ip_list[flow_idx],
156 'ip_dst_addr_max': self.dst.ip_list[max_idx],
157 'ip_dst_count': ip_dst_count,
158 'ip_addrs_step': self.ip_addrs_step,
159 'udp_src_port': self.udp_src_port,
160 'udp_dst_port': self.udp_dst_port,
161 'mac_discovery_gw': self.gateway_ip_list[chain_idx],
162 'ip_src_tg_gw': self.tg_gateway_ip_list[chain_idx],
163 'ip_dst_tg_gw': self.dst.tg_gateway_ip_list[chain_idx],
164 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
167 flow_idx += current_flow_count
171 def expand_ip(cls, ip, step_ip, count):
172 if step_ip == 'random':
173 # Repeatable Random will used in the stream src/dst IP pairs, but we still need
174 # to expand the IP based on the number of chains and flows configured. So we use
175 # "0.0.0.1" as the step to have the exact IP flow ranges for every chain.
178 step_ip_in_int = cls.ip_to_int(step_ip)
179 subnet = IPNetwork(ip)
181 for _ in xrange(count):
182 ip_list.append(subnet.ip.format())
183 subnet = subnet.next(step_ip_in_int)
188 return int(mac.translate(None, ":.- "), 16)
192 mac = format(i, 'x').zfill(12)
193 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
194 return ':'.join(blocks)
198 return struct.unpack("!I", socket.inet_aton(addr))[0]
201 class RunningTrafficProfile(object):
202 """Represents traffic configuration for currently running traffic profile."""
204 DEFAULT_IP_STEP = '0.0.0.1'
205 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
207 def __init__(self, config, generator_profile):
208 generator_config = self.__match_generator_profile(config.traffic_generator,
210 self.generator_config = generator_config
211 self.service_chain = config.service_chain
212 self.service_chain_count = config.service_chain_count
213 self.flow_count = config.flow_count
214 self.host_name = generator_config.host_name
215 self.name = generator_config.name
216 self.tool = generator_config.tool
217 self.cores = generator_config.get('cores', 1)
218 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
219 self.tg_gateway_ip_addrs_step = \
220 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
221 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
222 self.gateway_ips = generator_config.gateway_ip_addrs
223 self.ip = generator_config.ip
224 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
225 self.vlan_tagging = config.vlan_tagging
226 self.no_arp = config.no_arp
227 self.src_device = None
228 self.dst_device = None
229 self.vm_mac_list = None
230 self.__prep_interfaces(generator_config)
233 return dict(self.generator_config)
235 def set_vm_mac_list(self, vm_mac_list):
236 self.src_device.set_vm_mac_list(vm_mac_list[0])
237 self.dst_device.set_vm_mac_list(vm_mac_list[1])
240 def __match_generator_profile(traffic_generator, generator_profile):
241 generator_config = AttrDict(traffic_generator)
242 generator_config.pop('default_profile')
243 generator_config.pop('generator_profile')
244 matching_profile = filter(lambda profile: profile.name == generator_profile,
245 traffic_generator.generator_profile)
246 if len(matching_profile) != 1:
247 raise Exception('Traffic generator profile not found: ' + generator_profile)
249 generator_config.update(matching_profile[0])
251 return generator_config
253 def __prep_interfaces(self, generator_config):
255 'chain_count': self.service_chain_count,
256 'flow_count': self.flow_count / 2,
257 'ip': generator_config.ip_addrs[0],
258 'ip_addrs_step': self.ip_addrs_step,
259 'gateway_ip': self.gateway_ips[0],
260 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
261 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
262 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
263 'udp_src_port': generator_config.udp_src_port,
264 'udp_dst_port': generator_config.udp_dst_port,
265 'vlan_tagging': self.vlan_tagging
268 'chain_count': self.service_chain_count,
269 'flow_count': self.flow_count / 2,
270 'ip': generator_config.ip_addrs[1],
271 'ip_addrs_step': self.ip_addrs_step,
272 'gateway_ip': self.gateway_ips[1],
273 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
274 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
275 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
276 'udp_src_port': generator_config.udp_src_port,
277 'udp_dst_port': generator_config.udp_dst_port,
278 'vlan_tagging': self.vlan_tagging
281 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
282 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
283 self.src_device.set_destination(self.dst_device)
284 self.dst_device.set_destination(self.src_device)
286 if self.service_chain == ChainType.EXT and not self.no_arp \
287 and not self.__are_unique(self.src_device.ip_list, self.dst_device.ip_list):
288 raise Exception('Computed IP addresses are not unique, choose different base. '
289 'Start IPs: {start}. End IPs: {end}'
290 .format(start=self.src_device.ip_list,
291 end=self.dst_device.ip_list))
293 def __are_unique(self, list1, list2):
294 return set(list1).isdisjoint(set(list2))
298 return [self.src_device, self.dst_device]
302 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
306 return [self.src_device.port, self.dst_device.port]
309 def switch_ports(self):
310 return [self.src_device.switch_port, self.dst_device.switch_port]
314 return [self.src_device.pci, self.dst_device.pci]
317 class TrafficGeneratorFactory(object):
319 def __init__(self, config):
323 return self.config.generator_config.tool
325 def get_generator_client(self):
326 tool = self.get_tool().lower()
328 from traffic_gen import trex
329 return trex.TRex(self.config)
330 elif tool == 'dummy':
331 from traffic_gen import dummy
332 return dummy.DummyTG(self.config)
336 def list_generator_profile(self):
337 return [profile.name for profile in self.config.traffic_generator.generator_profile]
339 def get_generator_config(self, generator_profile):
340 return RunningTrafficProfile(self.config, generator_profile)
342 def get_matching_profile(self, traffic_profile_name):
343 matching_profile = filter(lambda profile: profile.name == traffic_profile_name,
344 self.config.traffic_profile)
346 if len(matching_profile) > 1:
347 raise Exception('Multiple traffic profiles with the same name found.')
348 elif len(matching_profile) == 0:
349 raise Exception('No traffic profile found.')
351 return matching_profile[0]
353 def get_frame_sizes(self, traffic_profile):
354 matching_profile = self.get_matching_profile(traffic_profile)
355 return matching_profile.l2frame_size
358 class TrafficClient(object):
362 def __init__(self, config, notifier=None):
363 generator_factory = TrafficGeneratorFactory(config)
364 self.gen = generator_factory.get_generator_client()
365 self.tool = generator_factory.get_tool()
367 self.notifier = notifier
368 self.interval_collector = None
369 self.iteration_collector = None
370 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
372 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
375 'l2frame_size': None,
376 'duration_sec': self.config.duration_sec,
377 'bidirectional': True,
380 self.current_total_rate = {'rate_percent': '10'}
381 if self.config.single_run:
382 self.current_total_rate = utils.parse_rate_str(self.config.rate)
385 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
388 def start_traffic_generator(self):
394 self.gen.config_interface()
395 self.gen.clear_stats()
397 def get_version(self):
398 return self.gen.get_version()
400 def ensure_end_to_end(self):
402 Ensure traffic generator receives packets it has transmitted.
403 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
405 At this point all VMs are in active state, but forwarding does not have to work.
406 Small amount of traffic is sent to every chain. Then total of sent and received packets
407 is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
408 N being number of chains, traffic flows through every chain and real measurements can be
412 PVP chain (1 VM per chain)
413 N = 10 (number of chains)
414 threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
415 if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
416 all 10 VMs are in operational state.
418 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
419 rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
420 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
422 # ensures enough traffic is coming back
423 threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
424 retry_count = (self.config.check_traffic_time_sec +
425 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
426 for it in xrange(retry_count):
427 self.gen.clear_stats()
428 self.gen.start_traffic()
429 LOG.info('Waiting for packets to be received back... ({} / {})'.format(it + 1,
431 time.sleep(self.config.generic_poll_sec)
432 self.gen.stop_traffic()
433 stats = self.gen.get_stats()
435 # compute total sent and received traffic on both ports
438 for port in self.PORTS:
439 total_rx += float(stats[port]['rx'].get('total_pkts', 0))
440 total_tx += float(stats[port]['tx'].get('total_pkts', 0))
442 # how much of traffic came back
443 ratio = total_rx / total_tx if total_tx else 0
445 if ratio > threshold:
446 self.gen.clear_stats()
447 self.gen.clear_streamblock()
448 LOG.info('End-to-end connectivity ensured')
451 time.sleep(self.config.generic_poll_sec)
453 raise TrafficClientException('End-to-end connectivity cannot be ensured')
455 def ensure_arp_successful(self):
456 if not self.gen.resolve_arp():
457 raise TrafficClientException('ARP cannot be resolved')
459 def set_traffic(self, frame_size, bidirectional):
460 self.run_config['bidirectional'] = bidirectional
461 self.run_config['l2frame_size'] = frame_size
462 self.run_config['rates'] = [self.get_per_direction_rate()]
464 self.run_config['rates'].append(self.get_per_direction_rate())
466 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
467 if unidir_reverse_pps > 0:
468 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
470 self.gen.clear_streamblock()
471 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
473 def modify_load(self, load):
474 self.current_total_rate = {'rate_percent': str(load)}
475 rate_per_direction = self.get_per_direction_rate()
477 self.gen.modify_rate(rate_per_direction, False)
478 self.run_config['rates'][0] = rate_per_direction
479 if self.run_config['bidirectional']:
480 self.gen.modify_rate(rate_per_direction, True)
481 self.run_config['rates'][1] = rate_per_direction
483 def get_ndr_and_pdr(self):
484 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
486 if self.config.ndr_run:
487 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
488 targets['ndr'] = self.config.measurement.NDR
489 if self.config.pdr_run:
490 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
491 targets['pdr'] = self.config.measurement.PDR
493 self.run_config['start_time'] = time.time()
494 self.interval_collector = IntervalCollector(self.run_config['start_time'])
495 self.interval_collector.attach_notifier(self.notifier)
496 self.iteration_collector = IterationCollector(self.run_config['start_time'])
498 self.__range_search(0.0, 200.0, targets, results)
500 results['iteration_stats'] = {
501 'ndr_pdr': self.iteration_collector.get()
504 if self.config.ndr_run:
505 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
506 results['ndr']['time_taken_sec'] = \
507 results['ndr']['timestamp_sec'] - self.run_config['start_time']
508 if self.config.pdr_run:
509 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
510 results['pdr']['time_taken_sec'] = \
511 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
513 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
514 results['pdr']['time_taken_sec'] = \
515 results['pdr']['timestamp_sec'] - self.run_config['start_time']
518 def __get_dropped_rate(self, result):
519 dropped_pkts = result['rx']['dropped_pkts']
520 total_pkts = result['tx']['total_pkts']
524 return float(dropped_pkts) / total_pkts * 100
527 stats = self.gen.get_stats()
528 retDict = {'total_tx_rate': stats['total_tx_rate']}
529 for port in self.PORTS:
530 retDict[port] = {'tx': {}, 'rx': {}}
532 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
533 rx_keys = tx_keys + ['dropped_pkts']
535 for port in self.PORTS:
537 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
540 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
542 retDict[port]['rx'][key] = 0
543 retDict[port]['rx']['avg_delay_usec'] = cast_integer(
544 stats[port]['rx']['avg_delay_usec'])
545 retDict[port]['rx']['min_delay_usec'] = cast_integer(
546 stats[port]['rx']['min_delay_usec'])
547 retDict[port]['rx']['max_delay_usec'] = cast_integer(
548 stats[port]['rx']['max_delay_usec'])
549 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
551 ports = sorted(retDict.keys())
552 if self.run_config['bidirectional']:
553 retDict['overall'] = {'tx': {}, 'rx': {}}
555 retDict['overall']['tx'][key] = \
556 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
558 retDict['overall']['rx'][key] = \
559 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
560 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
561 retDict[ports[1]]['rx']['total_pkts']]
562 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
563 retDict[ports[1]]['rx']['avg_delay_usec']]
564 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
565 retDict[ports[1]]['rx']['max_delay_usec']]
566 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
567 retDict[ports[1]]['rx']['min_delay_usec']]
568 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
569 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
570 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
571 for key in ['pkt_bit_rate', 'pkt_rate']:
572 for dirc in ['tx', 'rx']:
573 retDict['overall'][dirc][key] /= 2.0
575 retDict['overall'] = retDict[ports[0]]
576 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
579 def __convert_rates(self, rate):
580 return utils.convert_rates(self.run_config['l2frame_size'],
582 self.config.generator_config.intf_speed)
584 def __ndr_pdr_found(self, tag, load):
585 rates = self.__convert_rates({'rate_percent': load})
586 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
587 last_stats = self.iteration_collector.peek()
588 self.interval_collector.add_ndr_pdr(tag, last_stats)
590 def __format_output_stats(self, stats):
591 for key in (self.PORTS + ['overall']):
592 interface = stats[key]
594 'tx_pkts': interface['tx']['total_pkts'],
595 'rx_pkts': interface['rx']['total_pkts'],
596 'drop_percentage': interface['drop_rate_percent'],
597 'drop_pct': interface['rx']['dropped_pkts'],
598 'avg_delay_usec': interface['rx']['avg_delay_usec'],
599 'max_delay_usec': interface['rx']['max_delay_usec'],
600 'min_delay_usec': interface['rx']['min_delay_usec'],
605 def __targets_found(self, rate, targets, results):
606 for tag, target in targets.iteritems():
607 LOG.info('Found {} ({}) load: {}'.format(tag, target, rate))
608 self.__ndr_pdr_found(tag, rate)
609 results[tag]['timestamp_sec'] = time.time()
611 def __range_search(self, left, right, targets, results):
612 '''Perform a binary search for a list of targets inside a [left..right] range or rate
614 left the left side of the range to search as a % the line rate (100 = 100% line rate)
615 indicating the rate to send on each interface
616 right the right side of the range to search as a % of line rate
617 indicating the rate to send on each interface
618 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
620 results a dict to store results
622 if len(targets) == 0:
624 LOG.info('Range search [{} .. {}] targets: {}'.format(left, right, targets))
626 # Terminate search when gap is less than load epsilon
627 if right - left < self.config.measurement.load_epsilon:
628 self.__targets_found(left, targets, results)
631 # Obtain the average drop rate in for middle load
632 middle = (left + right) / 2.0
634 stats, rates = self.__run_search_iteration(middle)
636 LOG.exception("Got exception from traffic generator during binary search")
637 self.__targets_found(left, targets, results)
639 # Split target dicts based on the avg drop rate
642 for tag, target in targets.iteritems():
643 if stats['overall']['drop_rate_percent'] <= target:
644 # record the best possible rate found for this target
646 results[tag].update({
647 'load_percent_per_direction': middle,
648 'stats': self.__format_output_stats(dict(stats)),
649 'timestamp_sec': None
651 right_targets[tag] = target
653 # initialize to 0 all fields of result for
654 # the worst case scenario of the binary search (if ndr/pdr is not found)
655 if tag not in results:
656 results[tag] = dict.fromkeys(rates, 0)
657 empty_stats = self.__format_output_stats(dict(stats))
658 for key in empty_stats:
659 if isinstance(empty_stats[key], dict):
660 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
663 results[tag].update({
664 'load_percent_per_direction': 0,
665 'stats': empty_stats,
666 'timestamp_sec': None
668 left_targets[tag] = target
671 self.__range_search(left, middle, left_targets, results)
673 # search upper half only if the upper rate does not exceed
674 # 100%, this only happens when the first search at 100%
675 # yields a DR that is < target DR
677 self.__targets_found(100, right_targets, results)
679 self.__range_search(middle, right, right_targets, results)
681 def __run_search_iteration(self, rate):
683 self.modify_load(rate)
685 # poll interval stats and collect them
686 for stats in self.run_traffic():
687 self.interval_collector.add(stats)
688 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
689 if time_elapsed_ratio >= 1:
690 self.cancel_traffic()
691 self.interval_collector.reset()
693 # get stats from the run
694 stats = self.runner.client.get_stats()
695 current_traffic_config = self.get_traffic_config()
696 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
697 stats['total_tx_rate'])
698 if warning is not None:
699 stats['warning'] = warning
701 # save reliable stats from whole iteration
702 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
703 LOG.info('Average drop rate: {}'.format(stats['overall']['drop_rate_percent']))
705 return stats, current_traffic_config['direction-total']
708 def log_stats(stats):
710 'datetime': str(datetime.now()),
711 'tx_packets': stats['overall']['tx']['total_pkts'],
712 'rx_packets': stats['overall']['rx']['total_pkts'],
713 'drop_packets': stats['overall']['rx']['dropped_pkts'],
714 'drop_rate_percent': stats['overall']['drop_rate_percent']
716 LOG.info('TX: %(tx_packets)d; '
717 'RX: %(rx_packets)d; '
718 'Dropped: %(drop_packets)d; '
719 'Drop rate: %(drop_rate_percent).4f%%',
722 def run_traffic(self):
723 stats = self.runner.run()
724 while self.runner.is_running:
725 self.log_stats(stats)
727 stats = self.runner.poll_stats()
730 self.log_stats(stats)
731 LOG.info('Drop rate: {}'.format(stats['overall']['drop_rate_percent']))
734 def cancel_traffic(self):
737 def get_interface(self, port_index):
738 port = self.gen.port_handle[port_index]
740 if not self.config.no_traffic:
741 stats = self.get_stats()
743 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
744 return Interface('traffic-generator', self.tool.lower(), tx, rx)
746 def get_traffic_config(self):
751 for idx, rate in enumerate(self.run_config['rates']):
752 key = 'direction-forward' if idx == 0 else 'direction-reverse'
754 'l2frame_size': self.run_config['l2frame_size'],
755 'duration_sec': self.run_config['duration_sec']
757 config[key].update(rate)
758 config[key].update(self.__convert_rates(rate))
759 load_total += float(config[key]['rate_percent'])
760 bps_total += float(config[key]['rate_bps'])
761 pps_total += float(config[key]['rate_pps'])
762 config['direction-total'] = dict(config['direction-forward'])
763 config['direction-total'].update({
764 'rate_percent': load_total,
765 'rate_pps': cast_integer(pps_total),
766 'rate_bps': bps_total
771 def get_run_config(self, results):
772 """Returns configuration which was used for the last run."""
774 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
775 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
776 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
778 "orig": self.__convert_rates(self.run_config['rates'][idx]),
779 "tx": self.__convert_rates({'rate_pps': tx_rate}),
780 "rx": self.__convert_rates({'rate_pps': rx_rate})
784 for direction in ['orig', 'tx', 'rx']:
785 total[direction] = {}
786 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
787 total[direction][unit] = sum(map(lambda x: float(x[direction][unit]), r.values()))
789 r['direction-total'] = total
793 def compare_tx_rates(required, actual):
795 are_different = False
797 if float(actual) / required < threshold:
799 except ZeroDivisionError:
803 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
804 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
805 "to achieve the requested TX rate.".format(r=required, a=actual)
811 def get_per_direction_rate(self):
812 divisor = 2 if self.run_config['bidirectional'] else 1
813 if 'rate_percent' in self.current_total_rate:
814 # don't split rate if it's percentage
817 return utils.divide_rate(self.current_total_rate, divisor)
821 self.gen.stop_traffic()
824 self.gen.clear_stats()