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
30 class TrafficClientException(Exception):
34 class TrafficRunner(object):
36 def __init__(self, client, duration_sec, interval_sec=0):
38 self.start_time = None
39 self.duration_sec = duration_sec
40 self.interval_sec = interval_sec
43 LOG.info('Running traffic generator')
44 self.client.gen.clear_stats()
45 self.client.gen.start_traffic()
46 self.start_time = time.time()
47 return self.poll_stats()
51 self.start_time = None
52 self.client.gen.stop_traffic()
55 return self.start_time is not None
57 def time_elapsed(self):
59 return time.time() - self.start_time
61 return self.duration_sec
64 if not self.is_running():
66 time_elapsed = self.time_elapsed()
67 if time_elapsed > self.duration_sec:
70 time_left = self.duration_sec - time_elapsed
71 if self.interval_sec > 0.0:
72 if time_left <= self.interval_sec:
76 time.sleep(self.interval_sec)
78 time.sleep(self.duration_sec)
80 return self.client.get_stats()
85 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
86 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
87 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
88 chain_count=1, flow_count=1, vlan_tagging=False):
89 self.chain_count = chain_count
90 self.flow_count = flow_count
93 self.switch_port = switch_port
94 self.vtep_vlan = vtep_vlan
96 self.vlan_tagging = vlan_tagging
99 self.vm_mac_list = None
100 subnet = IPNetwork(ip)
101 self.ip = subnet.ip.format()
102 self.ip_prefixlen = subnet.prefixlen
103 self.ip_addrs_step = ip_addrs_step
104 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
105 self.gateway_ip_addrs_step = gateway_ip_addrs_step
106 self.ip_list = self.expand_ip(self.ip, self.ip_addrs_step, self.flow_count)
107 self.gateway_ip = gateway_ip
108 self.gateway_ip_list = self.expand_ip(self.gateway_ip,
109 self.gateway_ip_addrs_step,
111 self.tg_gateway_ip = tg_gateway_ip
112 self.tg_gateway_ip_list = self.expand_ip(self.tg_gateway_ip,
113 self.tg_gateway_ip_addrs_step,
115 self.udp_src_port = udp_src_port
116 self.udp_dst_port = udp_dst_port
118 def set_mac(self, mac):
120 raise TrafficClientException('Trying to set traffic generator MAC address as None')
123 def set_destination(self, dst):
126 def set_vm_mac_list(self, vm_mac_list):
127 self.vm_mac_list = map(str, vm_mac_list)
129 def set_vlan_tag(self, vlan_tag):
130 if self.vlan_tagging and vlan_tag is None:
131 raise TrafficClientException('Trying to set VLAN tag as None')
132 self.vlan_tag = vlan_tag
134 def get_stream_configs(self, service_chain):
137 for chain_idx in xrange(self.chain_count):
138 current_flow_count = (self.flow_count - flow_idx) / (self.chain_count - chain_idx)
139 max_idx = flow_idx + current_flow_count - 1
140 ip_src_count = self.ip_to_int(self.ip_list[max_idx]) - \
141 self.ip_to_int(self.ip_list[flow_idx]) + 1
142 ip_dst_count = self.ip_to_int(self.dst.ip_list[max_idx]) - \
143 self.ip_to_int(self.dst.ip_list[flow_idx]) + 1
146 'count': current_flow_count,
148 'mac_dst': self.dst.mac if service_chain == ChainType.EXT
149 else self.vm_mac_list[chain_idx],
150 'ip_src_addr': self.ip_list[flow_idx],
151 'ip_src_addr_max': self.ip_list[max_idx],
152 'ip_src_count': ip_src_count,
153 'ip_dst_addr': self.dst.ip_list[flow_idx],
154 'ip_dst_addr_max': self.dst.ip_list[max_idx],
155 'ip_dst_count': ip_dst_count,
156 'ip_addrs_step': self.ip_addrs_step,
157 'udp_src_port': self.udp_src_port,
158 'udp_dst_port': self.udp_dst_port,
159 'mac_discovery_gw': self.gateway_ip_list[chain_idx],
160 'ip_src_tg_gw': self.tg_gateway_ip_list[chain_idx],
161 'ip_dst_tg_gw': self.dst.tg_gateway_ip_list[chain_idx],
162 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
165 flow_idx += current_flow_count
169 def expand_ip(cls, ip, step_ip, count):
170 if step_ip == 'random':
171 # Repeatable Random will used in the stream src/dst IP pairs, but we still need
172 # to expand the IP based on the number of chains and flows configured. So we use
173 # "0.0.0.1" as the step to have the exact IP flow ranges for every chain.
176 step_ip_in_int = cls.ip_to_int(step_ip)
177 subnet = IPNetwork(ip)
179 for _ in xrange(count):
180 ip_list.append(subnet.ip.format())
181 subnet = subnet.next(step_ip_in_int)
186 return int(mac.translate(None, ":.- "), 16)
190 mac = format(i, 'x').zfill(12)
191 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
192 return ':'.join(blocks)
196 return struct.unpack("!I", socket.inet_aton(addr))[0]
199 class RunningTrafficProfile(object):
200 """Represents traffic configuration for currently running traffic profile."""
202 DEFAULT_IP_STEP = '0.0.0.1'
203 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
205 def __init__(self, config, generator_profile):
206 generator_config = self.__match_generator_profile(config.traffic_generator,
208 self.generator_config = generator_config
209 self.service_chain = config.service_chain
210 self.service_chain_count = config.service_chain_count
211 self.flow_count = config.flow_count
212 self.host_name = generator_config.host_name
213 self.name = generator_config.name
214 self.tool = generator_config.tool
215 self.cores = generator_config.get('cores', 1)
216 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
217 self.tg_gateway_ip_addrs_step = \
218 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
219 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
220 self.gateway_ips = generator_config.gateway_ip_addrs
221 self.ip = generator_config.ip
222 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
223 self.vlan_tagging = config.vlan_tagging
224 self.no_arp = config.no_arp
225 self.src_device = None
226 self.dst_device = None
227 self.vm_mac_list = None
228 self.__prep_interfaces(generator_config)
231 return dict(self.generator_config)
233 def set_vm_mac_list(self, vm_mac_list):
234 self.src_device.set_vm_mac_list(vm_mac_list[0])
235 self.dst_device.set_vm_mac_list(vm_mac_list[1])
238 def __match_generator_profile(traffic_generator, generator_profile):
239 generator_config = AttrDict(traffic_generator)
240 generator_config.pop('default_profile')
241 generator_config.pop('generator_profile')
242 matching_profile = filter(lambda profile: profile.name == generator_profile,
243 traffic_generator.generator_profile)
244 if len(matching_profile) != 1:
245 raise Exception('Traffic generator profile not found: ' + generator_profile)
247 generator_config.update(matching_profile[0])
249 return generator_config
251 def __prep_interfaces(self, generator_config):
253 'chain_count': self.service_chain_count,
254 'flow_count': self.flow_count / 2,
255 'ip': generator_config.ip_addrs[0],
256 'ip_addrs_step': self.ip_addrs_step,
257 'gateway_ip': self.gateway_ips[0],
258 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
259 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
260 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
261 'udp_src_port': generator_config.udp_src_port,
262 'udp_dst_port': generator_config.udp_dst_port,
263 'vlan_tagging': self.vlan_tagging
266 'chain_count': self.service_chain_count,
267 'flow_count': self.flow_count / 2,
268 'ip': generator_config.ip_addrs[1],
269 'ip_addrs_step': self.ip_addrs_step,
270 'gateway_ip': self.gateway_ips[1],
271 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
272 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
273 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
274 'udp_src_port': generator_config.udp_src_port,
275 'udp_dst_port': generator_config.udp_dst_port,
276 'vlan_tagging': self.vlan_tagging
279 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
280 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
281 self.src_device.set_destination(self.dst_device)
282 self.dst_device.set_destination(self.src_device)
284 if self.service_chain == ChainType.EXT and not self.no_arp \
285 and not self.__are_unique(self.src_device.ip_list, self.dst_device.ip_list):
286 raise Exception('Computed IP addresses are not unique, choose different base. '
287 'Start IPs: {start}. End IPs: {end}'
288 .format(start=self.src_device.ip_list,
289 end=self.dst_device.ip_list))
291 def __are_unique(self, list1, list2):
292 return set(list1).isdisjoint(set(list2))
296 return [self.src_device, self.dst_device]
300 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
304 return [self.src_device.port, self.dst_device.port]
307 def switch_ports(self):
308 return [self.src_device.switch_port, self.dst_device.switch_port]
312 return [self.src_device.pci, self.dst_device.pci]
315 class TrafficGeneratorFactory(object):
317 def __init__(self, config):
321 return self.config.generator_config.tool
323 def get_generator_client(self):
324 tool = self.get_tool().lower()
326 from traffic_gen import trex
327 return trex.TRex(self.config)
328 elif tool == 'dummy':
329 from traffic_gen import dummy
330 return dummy.DummyTG(self.config)
334 def list_generator_profile(self):
335 return [profile.name for profile in self.config.traffic_generator.generator_profile]
337 def get_generator_config(self, generator_profile):
338 return RunningTrafficProfile(self.config, generator_profile)
340 def get_matching_profile(self, traffic_profile_name):
341 matching_profile = filter(lambda profile: profile.name == traffic_profile_name,
342 self.config.traffic_profile)
344 if len(matching_profile) > 1:
345 raise Exception('Multiple traffic profiles with the same name found.')
346 elif len(matching_profile) == 0:
347 raise Exception('No traffic profile found.')
349 return matching_profile[0]
351 def get_frame_sizes(self, traffic_profile):
352 matching_profile = self.get_matching_profile(traffic_profile)
353 return matching_profile.l2frame_size
356 class TrafficClient(object):
360 def __init__(self, config, notifier=None):
361 generator_factory = TrafficGeneratorFactory(config)
362 self.gen = generator_factory.get_generator_client()
363 self.tool = generator_factory.get_tool()
365 self.notifier = notifier
366 self.interval_collector = None
367 self.iteration_collector = None
368 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
370 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
373 'l2frame_size': None,
374 'duration_sec': self.config.duration_sec,
375 'bidirectional': True,
378 self.current_total_rate = {'rate_percent': '10'}
379 if self.config.single_run:
380 self.current_total_rate = utils.parse_rate_str(self.config.rate)
383 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
386 def start_traffic_generator(self):
392 self.gen.config_interface()
393 self.gen.clear_stats()
395 def get_version(self):
396 return self.gen.get_version()
398 def ensure_end_to_end(self):
400 Ensure traffic generator receives packets it has transmitted.
401 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
403 At this point all VMs are in active state, but forwarding does not have to work.
404 Small amount of traffic is sent to every chain. Then total of sent and received packets
405 is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
406 N being number of chains, traffic flows through every chain and real measurements can be
410 PVP chain (1 VM per chain)
411 N = 10 (number of chains)
412 threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
413 if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
414 all 10 VMs are in operational state.
416 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
417 rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
418 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
420 # ensures enough traffic is coming back
421 threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
423 for it in xrange(self.config.generic_retry_count):
424 self.gen.clear_stats()
425 self.gen.start_traffic()
426 LOG.info('Waiting for packets to be received back... ({} / {})'.format(it + 1,
427 self.config.generic_retry_count))
428 time.sleep(self.config.generic_poll_sec)
429 self.gen.stop_traffic()
430 stats = self.gen.get_stats()
432 # compute total sent and received traffic on both ports
435 for port in self.PORTS:
436 total_rx += float(stats[port]['rx'].get('total_pkts', 0))
437 total_tx += float(stats[port]['tx'].get('total_pkts', 0))
439 # how much of traffic came back
440 ratio = total_rx / total_tx if total_tx else 0
442 if ratio > threshold:
443 self.gen.clear_stats()
444 self.gen.clear_streamblock()
445 LOG.info('End-to-end connectivity ensured')
448 time.sleep(self.config.generic_poll_sec)
450 raise TrafficClientException('End-to-end connectivity cannot be ensured')
452 def ensure_arp_successful(self):
453 if not self.gen.resolve_arp():
454 raise TrafficClientException('ARP cannot be resolved')
456 def set_traffic(self, frame_size, bidirectional):
457 self.run_config['bidirectional'] = bidirectional
458 self.run_config['l2frame_size'] = frame_size
459 self.run_config['rates'] = [self.get_per_direction_rate()]
461 self.run_config['rates'].append(self.get_per_direction_rate())
463 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
464 if unidir_reverse_pps > 0:
465 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
467 self.gen.clear_streamblock()
468 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
470 def modify_load(self, load):
471 self.current_total_rate = {'rate_percent': str(load)}
472 rate_per_direction = self.get_per_direction_rate()
474 self.gen.modify_rate(rate_per_direction, False)
475 self.run_config['rates'][0] = rate_per_direction
476 if self.run_config['bidirectional']:
477 self.gen.modify_rate(rate_per_direction, True)
478 self.run_config['rates'][1] = rate_per_direction
480 def get_ndr_and_pdr(self):
481 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
483 if self.config.ndr_run:
484 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
485 targets['ndr'] = self.config.measurement.NDR
486 if self.config.pdr_run:
487 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
488 targets['pdr'] = self.config.measurement.PDR
490 self.run_config['start_time'] = time.time()
491 self.interval_collector = IntervalCollector(self.run_config['start_time'])
492 self.interval_collector.attach_notifier(self.notifier)
493 self.iteration_collector = IterationCollector(self.run_config['start_time'])
495 self.__range_search(0.0, 200.0, targets, results)
497 results['iteration_stats'] = {
498 'ndr_pdr': self.iteration_collector.get()
501 if self.config.ndr_run:
502 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
503 results['ndr']['time_taken_sec'] = \
504 results['ndr']['timestamp_sec'] - self.run_config['start_time']
505 if self.config.pdr_run:
506 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
507 results['pdr']['time_taken_sec'] = \
508 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
510 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
511 results['pdr']['time_taken_sec'] = \
512 results['pdr']['timestamp_sec'] - self.run_config['start_time']
515 def __get_dropped_rate(self, result):
516 dropped_pkts = result['rx']['dropped_pkts']
517 total_pkts = result['tx']['total_pkts']
521 return float(dropped_pkts) / total_pkts * 100
524 stats = self.gen.get_stats()
525 retDict = {'total_tx_rate': stats['total_tx_rate']}
526 for port in self.PORTS:
527 retDict[port] = {'tx': {}, 'rx': {}}
529 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
530 rx_keys = tx_keys + ['dropped_pkts']
532 for port in self.PORTS:
534 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
537 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
539 retDict[port]['rx'][key] = 0
540 retDict[port]['rx']['avg_delay_usec'] = int(stats[port]['rx']['avg_delay_usec'])
541 retDict[port]['rx']['min_delay_usec'] = int(stats[port]['rx']['min_delay_usec'])
542 retDict[port]['rx']['max_delay_usec'] = int(stats[port]['rx']['max_delay_usec'])
543 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
545 ports = sorted(retDict.keys())
546 if self.run_config['bidirectional']:
547 retDict['overall'] = {'tx': {}, 'rx': {}}
549 retDict['overall']['tx'][key] = \
550 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
552 retDict['overall']['rx'][key] = \
553 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
554 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
555 retDict[ports[1]]['rx']['total_pkts']]
556 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
557 retDict[ports[1]]['rx']['avg_delay_usec']]
558 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
559 retDict[ports[1]]['rx']['max_delay_usec']]
560 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
561 retDict[ports[1]]['rx']['min_delay_usec']]
562 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
563 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
564 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
565 for key in ['pkt_bit_rate', 'pkt_rate']:
566 for dirc in ['tx', 'rx']:
567 retDict['overall'][dirc][key] /= 2.0
569 retDict['overall'] = retDict[ports[0]]
570 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
573 def __convert_rates(self, rate):
574 return utils.convert_rates(self.run_config['l2frame_size'],
576 self.config.generator_config.intf_speed)
578 def __ndr_pdr_found(self, tag, load):
579 rates = self.__convert_rates({'rate_percent': load})
580 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
581 last_stats = self.iteration_collector.peek()
582 self.interval_collector.add_ndr_pdr(tag, last_stats)
584 def __format_output_stats(self, stats):
585 for key in (self.PORTS + ['overall']):
586 interface = stats[key]
588 'tx_pkts': interface['tx']['total_pkts'],
589 'rx_pkts': interface['rx']['total_pkts'],
590 'drop_percentage': interface['drop_rate_percent'],
591 'drop_pct': interface['rx']['dropped_pkts'],
592 'avg_delay_usec': interface['rx']['avg_delay_usec'],
593 'max_delay_usec': interface['rx']['max_delay_usec'],
594 'min_delay_usec': interface['rx']['min_delay_usec'],
599 def __targets_found(self, rate, targets, results):
600 for tag, target in targets.iteritems():
601 LOG.info('Found {} ({}) load: {}'.format(tag, target, rate))
602 self.__ndr_pdr_found(tag, rate)
603 results[tag]['timestamp_sec'] = time.time()
605 def __range_search(self, left, right, targets, results):
606 '''Perform a binary search for a list of targets inside a [left..right] range or rate
608 left the left side of the range to search as a % the line rate (100 = 100% line rate)
609 indicating the rate to send on each interface
610 right the right side of the range to search as a % of line rate
611 indicating the rate to send on each interface
612 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
614 results a dict to store results
616 if len(targets) == 0:
618 LOG.info('Range search [{} .. {}] targets: {}'.format(left, right, targets))
620 # Terminate search when gap is less than load epsilon
621 if right - left < self.config.measurement.load_epsilon:
622 self.__targets_found(left, targets, results)
625 # Obtain the average drop rate in for middle load
626 middle = (left + right) / 2.0
627 stats, rates = self.__run_search_iteration(middle)
629 # Split target dicts based on the avg drop rate
632 for tag, target in targets.iteritems():
633 if stats['overall']['drop_rate_percent'] <= target:
634 # record the best possible rate found for this target
636 results[tag].update({
637 'load_percent_per_direction': middle,
638 'stats': self.__format_output_stats(dict(stats)),
639 'timestamp_sec': None
641 right_targets[tag] = target
643 # initialize to 0 all fields of result for
644 # the worst case scenario of the binary search (if ndr/pdr is not found)
645 if tag not in results:
646 results[tag] = dict.fromkeys(rates, 0)
647 empty_stats = self.__format_output_stats(dict(stats))
648 for key in empty_stats:
649 if isinstance(empty_stats[key], dict):
650 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
653 results[tag].update({
654 'load_percent_per_direction': 0,
655 'stats': empty_stats,
656 'timestamp_sec': None
658 left_targets[tag] = target
661 self.__range_search(left, middle, left_targets, results)
663 # search upper half only if the upper rate does not exceed
664 # 100%, this only happens when the first search at 100%
665 # yields a DR that is < target DR
667 self.__targets_found(100, right_targets, results)
669 self.__range_search(middle, right, right_targets, results)
671 def __run_search_iteration(self, rate):
673 self.modify_load(rate)
675 # poll interval stats and collect them
676 for stats in self.run_traffic():
677 self.interval_collector.add(stats)
678 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
679 if time_elapsed_ratio >= 1:
680 self.cancel_traffic()
681 self.interval_collector.reset()
683 # get stats from the run
684 stats = self.runner.client.get_stats()
685 current_traffic_config = self.get_traffic_config()
686 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
687 stats['total_tx_rate'])
688 if warning is not None:
689 stats['warning'] = warning
691 # save reliable stats from whole iteration
692 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
693 LOG.info('Average drop rate: {}'.format(stats['overall']['drop_rate_percent']))
695 return stats, current_traffic_config['direction-total']
698 def log_stats(stats):
700 'datetime': str(datetime.now()),
701 'tx_packets': stats['overall']['tx']['total_pkts'],
702 'rx_packets': stats['overall']['rx']['total_pkts'],
703 'drop_packets': stats['overall']['rx']['dropped_pkts'],
704 'drop_rate_percent': stats['overall']['drop_rate_percent']
706 LOG.info('TX: %(tx_packets)d; '
707 'RX: %(rx_packets)d; '
708 'Dropped: %(drop_packets)d; '
709 'Drop rate: %(drop_rate_percent).4f%%',
712 def run_traffic(self):
713 stats = self.runner.run()
714 while self.runner.is_running:
715 self.log_stats(stats)
717 stats = self.runner.poll_stats()
720 self.log_stats(stats)
721 LOG.info('Drop rate: {}'.format(stats['overall']['drop_rate_percent']))
724 def cancel_traffic(self):
727 def get_interface(self, port_index):
728 port = self.gen.port_handle[port_index]
730 if not self.config.no_traffic:
731 stats = self.get_stats()
733 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
734 return Interface('traffic-generator', self.tool.lower(), tx, rx)
736 def get_traffic_config(self):
741 for idx, rate in enumerate(self.run_config['rates']):
742 key = 'direction-forward' if idx == 0 else 'direction-reverse'
744 'l2frame_size': self.run_config['l2frame_size'],
745 'duration_sec': self.run_config['duration_sec']
747 config[key].update(rate)
748 config[key].update(self.__convert_rates(rate))
749 load_total += float(config[key]['rate_percent'])
750 bps_total += float(config[key]['rate_bps'])
751 pps_total += float(config[key]['rate_pps'])
752 config['direction-total'] = dict(config['direction-forward'])
753 config['direction-total'].update({
754 'rate_percent': load_total,
755 'rate_pps': pps_total,
756 'rate_bps': bps_total
761 def get_run_config(self, results):
762 """Returns configuration which was used for the last run."""
764 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
765 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
766 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
768 "orig": self.__convert_rates(self.run_config['rates'][idx]),
769 "tx": self.__convert_rates({'rate_pps': tx_rate}),
770 "rx": self.__convert_rates({'rate_pps': rx_rate})
774 for direction in ['orig', 'tx', 'rx']:
775 total[direction] = {}
776 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
777 total[direction][unit] = sum(map(lambda x: float(x[direction][unit]), r.values()))
779 r['direction-total'] = total
783 def compare_tx_rates(required, actual):
785 are_different = False
787 if float(actual) / required < threshold:
789 except ZeroDivisionError:
793 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
794 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
795 "to achieve the requested TX rate.".format(r=required, a=actual)
801 def get_per_direction_rate(self):
802 divisor = 2 if self.run_config['bidirectional'] else 1
803 if 'rate_percent' in self.current_total_rate:
804 # don't split rate if it's percentage
807 return utils.divide_rate(self.current_total_rate, divisor)
811 self.gen.stop_traffic()
814 self.gen.clear_stats()