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 utils import cast_integer
31 class TrafficClientException(Exception):
35 class TrafficRunner(object):
37 def __init__(self, client, duration_sec, interval_sec=0):
39 self.start_time = None
40 self.duration_sec = duration_sec
41 self.interval_sec = interval_sec
44 LOG.info('Running traffic generator')
45 self.client.gen.clear_stats()
46 self.client.gen.start_traffic()
47 self.start_time = time.time()
48 return self.poll_stats()
52 self.start_time = None
53 self.client.gen.stop_traffic()
56 return self.start_time is not None
58 def time_elapsed(self):
60 return time.time() - self.start_time
62 return self.duration_sec
65 if not self.is_running():
67 time_elapsed = self.time_elapsed()
68 if time_elapsed > self.duration_sec:
71 time_left = self.duration_sec - time_elapsed
72 if self.interval_sec > 0.0:
73 if time_left <= self.interval_sec:
77 time.sleep(self.interval_sec)
79 time.sleep(self.duration_sec)
81 return self.client.get_stats()
86 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
87 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
88 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
89 chain_count=1, flow_count=1, vlan_tagging=False):
90 self.chain_count = chain_count
91 self.flow_count = flow_count
94 self.switch_port = switch_port
95 self.vtep_vlan = vtep_vlan
97 self.vlan_tagging = vlan_tagging
100 self.vm_mac_list = None
101 subnet = IPNetwork(ip)
102 self.ip = subnet.ip.format()
103 self.ip_prefixlen = subnet.prefixlen
104 self.ip_addrs_step = ip_addrs_step
105 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
106 self.gateway_ip_addrs_step = gateway_ip_addrs_step
107 self.ip_list = self.expand_ip(self.ip, self.ip_addrs_step, self.flow_count)
108 self.gateway_ip = gateway_ip
109 self.gateway_ip_list = self.expand_ip(self.gateway_ip,
110 self.gateway_ip_addrs_step,
112 self.tg_gateway_ip = tg_gateway_ip
113 self.tg_gateway_ip_list = self.expand_ip(self.tg_gateway_ip,
114 self.tg_gateway_ip_addrs_step,
116 self.udp_src_port = udp_src_port
117 self.udp_dst_port = udp_dst_port
119 def set_mac(self, mac):
121 raise TrafficClientException('Trying to set traffic generator MAC address as None')
124 def set_destination(self, dst):
127 def set_vm_mac_list(self, vm_mac_list):
128 self.vm_mac_list = map(str, vm_mac_list)
130 def set_vlan_tag(self, vlan_tag):
131 if self.vlan_tagging and vlan_tag is None:
132 raise TrafficClientException('Trying to set VLAN tag as None')
133 self.vlan_tag = vlan_tag
135 def get_stream_configs(self, service_chain):
138 for chain_idx in xrange(self.chain_count):
139 current_flow_count = (self.flow_count - flow_idx) / (self.chain_count - chain_idx)
140 max_idx = flow_idx + current_flow_count - 1
141 ip_src_count = self.ip_to_int(self.ip_list[max_idx]) - \
142 self.ip_to_int(self.ip_list[flow_idx]) + 1
143 ip_dst_count = self.ip_to_int(self.dst.ip_list[max_idx]) - \
144 self.ip_to_int(self.dst.ip_list[flow_idx]) + 1
147 'count': current_flow_count,
149 'mac_dst': self.dst.mac if service_chain == ChainType.EXT
150 else self.vm_mac_list[chain_idx],
151 'ip_src_addr': self.ip_list[flow_idx],
152 'ip_src_addr_max': self.ip_list[max_idx],
153 'ip_src_count': ip_src_count,
154 'ip_dst_addr': self.dst.ip_list[flow_idx],
155 'ip_dst_addr_max': self.dst.ip_list[max_idx],
156 'ip_dst_count': ip_dst_count,
157 'ip_addrs_step': self.ip_addrs_step,
158 'udp_src_port': self.udp_src_port,
159 'udp_dst_port': self.udp_dst_port,
160 'mac_discovery_gw': self.gateway_ip_list[chain_idx],
161 'ip_src_tg_gw': self.tg_gateway_ip_list[chain_idx],
162 'ip_dst_tg_gw': self.dst.tg_gateway_ip_list[chain_idx],
163 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
166 flow_idx += current_flow_count
170 def expand_ip(cls, ip, step_ip, count):
171 if step_ip == 'random':
172 # Repeatable Random will used in the stream src/dst IP pairs, but we still need
173 # to expand the IP based on the number of chains and flows configured. So we use
174 # "0.0.0.1" as the step to have the exact IP flow ranges for every chain.
177 step_ip_in_int = cls.ip_to_int(step_ip)
178 subnet = IPNetwork(ip)
180 for _ in xrange(count):
181 ip_list.append(subnet.ip.format())
182 subnet = subnet.next(step_ip_in_int)
187 return int(mac.translate(None, ":.- "), 16)
191 mac = format(i, 'x').zfill(12)
192 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
193 return ':'.join(blocks)
197 return struct.unpack("!I", socket.inet_aton(addr))[0]
200 class RunningTrafficProfile(object):
201 """Represents traffic configuration for currently running traffic profile."""
203 DEFAULT_IP_STEP = '0.0.0.1'
204 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
206 def __init__(self, config, generator_profile):
207 generator_config = self.__match_generator_profile(config.traffic_generator,
209 self.generator_config = generator_config
210 self.service_chain = config.service_chain
211 self.service_chain_count = config.service_chain_count
212 self.flow_count = config.flow_count
213 self.host_name = generator_config.host_name
214 self.name = generator_config.name
215 self.tool = generator_config.tool
216 self.cores = generator_config.get('cores', 1)
217 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
218 self.tg_gateway_ip_addrs_step = \
219 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
220 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
221 self.gateway_ips = generator_config.gateway_ip_addrs
222 self.ip = generator_config.ip
223 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
224 self.vlan_tagging = config.vlan_tagging
225 self.no_arp = config.no_arp
226 self.src_device = None
227 self.dst_device = None
228 self.vm_mac_list = None
229 self.__prep_interfaces(generator_config)
232 return dict(self.generator_config)
234 def set_vm_mac_list(self, vm_mac_list):
235 self.src_device.set_vm_mac_list(vm_mac_list[0])
236 self.dst_device.set_vm_mac_list(vm_mac_list[1])
239 def __match_generator_profile(traffic_generator, generator_profile):
240 generator_config = AttrDict(traffic_generator)
241 generator_config.pop('default_profile')
242 generator_config.pop('generator_profile')
243 matching_profile = filter(lambda profile: profile.name == generator_profile,
244 traffic_generator.generator_profile)
245 if len(matching_profile) != 1:
246 raise Exception('Traffic generator profile not found: ' + generator_profile)
248 generator_config.update(matching_profile[0])
250 return generator_config
252 def __prep_interfaces(self, generator_config):
254 'chain_count': self.service_chain_count,
255 'flow_count': self.flow_count / 2,
256 'ip': generator_config.ip_addrs[0],
257 'ip_addrs_step': self.ip_addrs_step,
258 'gateway_ip': self.gateway_ips[0],
259 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
260 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
261 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
262 'udp_src_port': generator_config.udp_src_port,
263 'udp_dst_port': generator_config.udp_dst_port,
264 'vlan_tagging': self.vlan_tagging
267 'chain_count': self.service_chain_count,
268 'flow_count': self.flow_count / 2,
269 'ip': generator_config.ip_addrs[1],
270 'ip_addrs_step': self.ip_addrs_step,
271 'gateway_ip': self.gateway_ips[1],
272 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
273 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
274 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
275 'udp_src_port': generator_config.udp_src_port,
276 'udp_dst_port': generator_config.udp_dst_port,
277 'vlan_tagging': self.vlan_tagging
280 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
281 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
282 self.src_device.set_destination(self.dst_device)
283 self.dst_device.set_destination(self.src_device)
285 if self.service_chain == ChainType.EXT and not self.no_arp \
286 and not self.__are_unique(self.src_device.ip_list, self.dst_device.ip_list):
287 raise Exception('Computed IP addresses are not unique, choose different base. '
288 'Start IPs: {start}. End IPs: {end}'
289 .format(start=self.src_device.ip_list,
290 end=self.dst_device.ip_list))
292 def __are_unique(self, list1, list2):
293 return set(list1).isdisjoint(set(list2))
297 return [self.src_device, self.dst_device]
301 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
305 return [self.src_device.port, self.dst_device.port]
308 def switch_ports(self):
309 return [self.src_device.switch_port, self.dst_device.switch_port]
313 return [self.src_device.pci, self.dst_device.pci]
316 class TrafficGeneratorFactory(object):
318 def __init__(self, config):
322 return self.config.generator_config.tool
324 def get_generator_client(self):
325 tool = self.get_tool().lower()
327 from traffic_gen import trex
328 return trex.TRex(self.config)
329 elif tool == 'dummy':
330 from traffic_gen import dummy
331 return dummy.DummyTG(self.config)
335 def list_generator_profile(self):
336 return [profile.name for profile in self.config.traffic_generator.generator_profile]
338 def get_generator_config(self, generator_profile):
339 return RunningTrafficProfile(self.config, generator_profile)
341 def get_matching_profile(self, traffic_profile_name):
342 matching_profile = filter(lambda profile: profile.name == traffic_profile_name,
343 self.config.traffic_profile)
345 if len(matching_profile) > 1:
346 raise Exception('Multiple traffic profiles with the same name found.')
347 elif len(matching_profile) == 0:
348 raise Exception('No traffic profile found.')
350 return matching_profile[0]
352 def get_frame_sizes(self, traffic_profile):
353 matching_profile = self.get_matching_profile(traffic_profile)
354 return matching_profile.l2frame_size
357 class TrafficClient(object):
361 def __init__(self, config, notifier=None):
362 generator_factory = TrafficGeneratorFactory(config)
363 self.gen = generator_factory.get_generator_client()
364 self.tool = generator_factory.get_tool()
366 self.notifier = notifier
367 self.interval_collector = None
368 self.iteration_collector = None
369 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
371 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
374 'l2frame_size': None,
375 'duration_sec': self.config.duration_sec,
376 'bidirectional': True,
379 self.current_total_rate = {'rate_percent': '10'}
380 if self.config.single_run:
381 self.current_total_rate = utils.parse_rate_str(self.config.rate)
384 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
387 def start_traffic_generator(self):
393 self.gen.config_interface()
394 self.gen.clear_stats()
396 def get_version(self):
397 return self.gen.get_version()
399 def ensure_end_to_end(self):
401 Ensure traffic generator receives packets it has transmitted.
402 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
404 At this point all VMs are in active state, but forwarding does not have to work.
405 Small amount of traffic is sent to every chain. Then total of sent and received packets
406 is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
407 N being number of chains, traffic flows through every chain and real measurements can be
411 PVP chain (1 VM per chain)
412 N = 10 (number of chains)
413 threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
414 if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
415 all 10 VMs are in operational state.
417 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
418 rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
419 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
421 # ensures enough traffic is coming back
422 threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
423 retry_count = (self.config.check_traffic_time_sec +
424 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
425 for it in xrange(retry_count):
426 self.gen.clear_stats()
427 self.gen.start_traffic()
428 LOG.info('Waiting for packets to be received back... ({} / {})'.format(it + 1,
430 time.sleep(self.config.generic_poll_sec)
431 self.gen.stop_traffic()
432 stats = self.gen.get_stats()
434 # compute total sent and received traffic on both ports
437 for port in self.PORTS:
438 total_rx += float(stats[port]['rx'].get('total_pkts', 0))
439 total_tx += float(stats[port]['tx'].get('total_pkts', 0))
441 # how much of traffic came back
442 ratio = total_rx / total_tx if total_tx else 0
444 if ratio > threshold:
445 self.gen.clear_stats()
446 self.gen.clear_streamblock()
447 LOG.info('End-to-end connectivity ensured')
450 time.sleep(self.config.generic_poll_sec)
452 raise TrafficClientException('End-to-end connectivity cannot be ensured')
454 def ensure_arp_successful(self):
455 if not self.gen.resolve_arp():
456 raise TrafficClientException('ARP cannot be resolved')
458 def set_traffic(self, frame_size, bidirectional):
459 self.run_config['bidirectional'] = bidirectional
460 self.run_config['l2frame_size'] = frame_size
461 self.run_config['rates'] = [self.get_per_direction_rate()]
463 self.run_config['rates'].append(self.get_per_direction_rate())
465 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
466 if unidir_reverse_pps > 0:
467 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
469 self.gen.clear_streamblock()
470 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
472 def modify_load(self, load):
473 self.current_total_rate = {'rate_percent': str(load)}
474 rate_per_direction = self.get_per_direction_rate()
476 self.gen.modify_rate(rate_per_direction, False)
477 self.run_config['rates'][0] = rate_per_direction
478 if self.run_config['bidirectional']:
479 self.gen.modify_rate(rate_per_direction, True)
480 self.run_config['rates'][1] = rate_per_direction
482 def get_ndr_and_pdr(self):
483 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
485 if self.config.ndr_run:
486 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
487 targets['ndr'] = self.config.measurement.NDR
488 if self.config.pdr_run:
489 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
490 targets['pdr'] = self.config.measurement.PDR
492 self.run_config['start_time'] = time.time()
493 self.interval_collector = IntervalCollector(self.run_config['start_time'])
494 self.interval_collector.attach_notifier(self.notifier)
495 self.iteration_collector = IterationCollector(self.run_config['start_time'])
497 self.__range_search(0.0, 200.0, targets, results)
499 results['iteration_stats'] = {
500 'ndr_pdr': self.iteration_collector.get()
503 if self.config.ndr_run:
504 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
505 results['ndr']['time_taken_sec'] = \
506 results['ndr']['timestamp_sec'] - self.run_config['start_time']
507 if self.config.pdr_run:
508 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
509 results['pdr']['time_taken_sec'] = \
510 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
512 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
513 results['pdr']['time_taken_sec'] = \
514 results['pdr']['timestamp_sec'] - self.run_config['start_time']
517 def __get_dropped_rate(self, result):
518 dropped_pkts = result['rx']['dropped_pkts']
519 total_pkts = result['tx']['total_pkts']
523 return float(dropped_pkts) / total_pkts * 100
526 stats = self.gen.get_stats()
527 retDict = {'total_tx_rate': stats['total_tx_rate']}
528 for port in self.PORTS:
529 retDict[port] = {'tx': {}, 'rx': {}}
531 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
532 rx_keys = tx_keys + ['dropped_pkts']
534 for port in self.PORTS:
536 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
539 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
541 retDict[port]['rx'][key] = 0
542 retDict[port]['rx']['avg_delay_usec'] = cast_integer(
543 stats[port]['rx']['avg_delay_usec'])
544 retDict[port]['rx']['min_delay_usec'] = cast_integer(
545 stats[port]['rx']['min_delay_usec'])
546 retDict[port]['rx']['max_delay_usec'] = cast_integer(
547 stats[port]['rx']['max_delay_usec'])
548 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
550 ports = sorted(retDict.keys())
551 if self.run_config['bidirectional']:
552 retDict['overall'] = {'tx': {}, 'rx': {}}
554 retDict['overall']['tx'][key] = \
555 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
557 retDict['overall']['rx'][key] = \
558 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
559 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
560 retDict[ports[1]]['rx']['total_pkts']]
561 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
562 retDict[ports[1]]['rx']['avg_delay_usec']]
563 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
564 retDict[ports[1]]['rx']['max_delay_usec']]
565 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
566 retDict[ports[1]]['rx']['min_delay_usec']]
567 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
568 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
569 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
570 for key in ['pkt_bit_rate', 'pkt_rate']:
571 for dirc in ['tx', 'rx']:
572 retDict['overall'][dirc][key] /= 2.0
574 retDict['overall'] = retDict[ports[0]]
575 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
578 def __convert_rates(self, rate):
579 return utils.convert_rates(self.run_config['l2frame_size'],
581 self.config.generator_config.intf_speed)
583 def __ndr_pdr_found(self, tag, load):
584 rates = self.__convert_rates({'rate_percent': load})
585 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
586 last_stats = self.iteration_collector.peek()
587 self.interval_collector.add_ndr_pdr(tag, last_stats)
589 def __format_output_stats(self, stats):
590 for key in (self.PORTS + ['overall']):
591 interface = stats[key]
593 'tx_pkts': interface['tx']['total_pkts'],
594 'rx_pkts': interface['rx']['total_pkts'],
595 'drop_percentage': interface['drop_rate_percent'],
596 'drop_pct': interface['rx']['dropped_pkts'],
597 'avg_delay_usec': interface['rx']['avg_delay_usec'],
598 'max_delay_usec': interface['rx']['max_delay_usec'],
599 'min_delay_usec': interface['rx']['min_delay_usec'],
604 def __targets_found(self, rate, targets, results):
605 for tag, target in targets.iteritems():
606 LOG.info('Found {} ({}) load: {}'.format(tag, target, rate))
607 self.__ndr_pdr_found(tag, rate)
608 results[tag]['timestamp_sec'] = time.time()
610 def __range_search(self, left, right, targets, results):
611 '''Perform a binary search for a list of targets inside a [left..right] range or rate
613 left the left side of the range to search as a % the line rate (100 = 100% line rate)
614 indicating the rate to send on each interface
615 right the right side of the range to search as a % of line rate
616 indicating the rate to send on each interface
617 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
619 results a dict to store results
621 if len(targets) == 0:
623 LOG.info('Range search [{} .. {}] targets: {}'.format(left, right, targets))
625 # Terminate search when gap is less than load epsilon
626 if right - left < self.config.measurement.load_epsilon:
627 self.__targets_found(left, targets, results)
630 # Obtain the average drop rate in for middle load
631 middle = (left + right) / 2.0
632 stats, rates = self.__run_search_iteration(middle)
634 # Split target dicts based on the avg drop rate
637 for tag, target in targets.iteritems():
638 if stats['overall']['drop_rate_percent'] <= target:
639 # record the best possible rate found for this target
641 results[tag].update({
642 'load_percent_per_direction': middle,
643 'stats': self.__format_output_stats(dict(stats)),
644 'timestamp_sec': None
646 right_targets[tag] = target
648 # initialize to 0 all fields of result for
649 # the worst case scenario of the binary search (if ndr/pdr is not found)
650 if tag not in results:
651 results[tag] = dict.fromkeys(rates, 0)
652 empty_stats = self.__format_output_stats(dict(stats))
653 for key in empty_stats:
654 if isinstance(empty_stats[key], dict):
655 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
658 results[tag].update({
659 'load_percent_per_direction': 0,
660 'stats': empty_stats,
661 'timestamp_sec': None
663 left_targets[tag] = target
666 self.__range_search(left, middle, left_targets, results)
668 # search upper half only if the upper rate does not exceed
669 # 100%, this only happens when the first search at 100%
670 # yields a DR that is < target DR
672 self.__targets_found(100, right_targets, results)
674 self.__range_search(middle, right, right_targets, results)
676 def __run_search_iteration(self, rate):
678 self.modify_load(rate)
680 # poll interval stats and collect them
681 for stats in self.run_traffic():
682 self.interval_collector.add(stats)
683 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
684 if time_elapsed_ratio >= 1:
685 self.cancel_traffic()
686 self.interval_collector.reset()
688 # get stats from the run
689 stats = self.runner.client.get_stats()
690 current_traffic_config = self.get_traffic_config()
691 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
692 stats['total_tx_rate'])
693 if warning is not None:
694 stats['warning'] = warning
696 # save reliable stats from whole iteration
697 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
698 LOG.info('Average drop rate: {}'.format(stats['overall']['drop_rate_percent']))
700 return stats, current_traffic_config['direction-total']
703 def log_stats(stats):
705 'datetime': str(datetime.now()),
706 'tx_packets': stats['overall']['tx']['total_pkts'],
707 'rx_packets': stats['overall']['rx']['total_pkts'],
708 'drop_packets': stats['overall']['rx']['dropped_pkts'],
709 'drop_rate_percent': stats['overall']['drop_rate_percent']
711 LOG.info('TX: %(tx_packets)d; '
712 'RX: %(rx_packets)d; '
713 'Dropped: %(drop_packets)d; '
714 'Drop rate: %(drop_rate_percent).4f%%',
717 def run_traffic(self):
718 stats = self.runner.run()
719 while self.runner.is_running:
720 self.log_stats(stats)
722 stats = self.runner.poll_stats()
725 self.log_stats(stats)
726 LOG.info('Drop rate: {}'.format(stats['overall']['drop_rate_percent']))
729 def cancel_traffic(self):
732 def get_interface(self, port_index):
733 port = self.gen.port_handle[port_index]
735 if not self.config.no_traffic:
736 stats = self.get_stats()
738 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
739 return Interface('traffic-generator', self.tool.lower(), tx, rx)
741 def get_traffic_config(self):
746 for idx, rate in enumerate(self.run_config['rates']):
747 key = 'direction-forward' if idx == 0 else 'direction-reverse'
749 'l2frame_size': self.run_config['l2frame_size'],
750 'duration_sec': self.run_config['duration_sec']
752 config[key].update(rate)
753 config[key].update(self.__convert_rates(rate))
754 load_total += float(config[key]['rate_percent'])
755 bps_total += float(config[key]['rate_bps'])
756 pps_total += float(config[key]['rate_pps'])
757 config['direction-total'] = dict(config['direction-forward'])
758 config['direction-total'].update({
759 'rate_percent': load_total,
760 'rate_pps': cast_integer(pps_total),
761 'rate_bps': bps_total
766 def get_run_config(self, results):
767 """Returns configuration which was used for the last run."""
769 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
770 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
771 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
773 "orig": self.__convert_rates(self.run_config['rates'][idx]),
774 "tx": self.__convert_rates({'rate_pps': tx_rate}),
775 "rx": self.__convert_rates({'rate_pps': rx_rate})
779 for direction in ['orig', 'tx', 'rx']:
780 total[direction] = {}
781 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
782 total[direction][unit] = sum(map(lambda x: float(x[direction][unit]), r.values()))
784 r['direction-total'] = total
788 def compare_tx_rates(required, actual):
790 are_different = False
792 if float(actual) / required < threshold:
794 except ZeroDivisionError:
798 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
799 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
800 "to achieve the requested TX rate.".format(r=required, a=actual)
806 def get_per_direction_rate(self):
807 divisor = 2 if self.run_config['bidirectional'] else 1
808 if 'rate_percent' in self.current_total_rate:
809 # don't split rate if it's percentage
812 return utils.divide_rate(self.current_total_rate, divisor)
816 self.gen.stop_traffic()
819 self.gen.clear_stats()