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 datetime import datetime
20 from attrdict import AttrDict
22 from netaddr import IPNetwork
23 # pylint: disable=import-error
24 from trex_stl_lib.api import STLError
25 # pylint: enable=import-error
28 from network import Interface
29 from specs import ChainType
30 from stats_collector import IntervalCollector
31 from stats_collector import IterationCollector
32 import traffic_gen.traffic_utils as utils
33 from utils import cast_integer
36 class TrafficClientException(Exception):
40 class TrafficRunner(object):
41 def __init__(self, client, duration_sec, interval_sec=0):
43 self.start_time = None
44 self.duration_sec = duration_sec
45 self.interval_sec = interval_sec
48 LOG.info('Running traffic generator')
49 self.client.gen.clear_stats()
50 self.client.gen.start_traffic()
51 self.start_time = time.time()
52 return self.poll_stats()
56 self.start_time = None
57 self.client.gen.stop_traffic()
60 return self.start_time is not None
62 def time_elapsed(self):
64 return time.time() - self.start_time
65 return self.duration_sec
68 if not self.is_running():
70 if self.client.skip_sleep:
72 return self.client.get_stats()
73 time_elapsed = self.time_elapsed()
74 if time_elapsed > self.duration_sec:
77 time_left = self.duration_sec - time_elapsed
78 if self.interval_sec > 0.0:
79 if time_left <= self.interval_sec:
83 time.sleep(self.interval_sec)
85 time.sleep(self.duration_sec)
87 return self.client.get_stats()
90 class IpBlock(object):
91 def __init__(self, base_ip, step_ip, count_ip):
92 self.base_ip_int = Device.ip_to_int(base_ip)
93 self.step = Device.ip_to_int(step_ip)
94 self.max_available = count_ip
97 def get_ip(self, index=0):
98 '''Return the IP address at given index
100 if index < 0 or index >= self.max_available:
101 raise IndexError('Index out of bounds')
102 return Device.int_to_ip(self.base_ip_int + index * self.step)
104 def reserve_ip_range(self, count):
105 '''Reserve a range of count consecutive IP addresses spaced by step
107 if self.next_free + count > self.max_available:
108 raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' %
112 first_ip = self.get_ip(self.next_free)
113 last_ip = self.get_ip(self.next_free + count - 1)
114 self.next_free += count
115 return (first_ip, last_ip)
117 def reset_reservation(self):
121 class Device(object):
122 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
123 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
124 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
125 chain_count=1, flow_count=1, vlan_tagging=False):
126 self.chain_count = chain_count
127 self.flow_count = flow_count
130 self.switch_port = switch_port
131 self.vtep_vlan = vtep_vlan
133 self.vlan_tagging = vlan_tagging
136 self.vm_mac_list = None
137 subnet = IPNetwork(ip)
138 self.ip = subnet.ip.format()
139 self.ip_prefixlen = subnet.prefixlen
140 self.ip_addrs_step = ip_addrs_step
141 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
142 self.gateway_ip_addrs_step = gateway_ip_addrs_step
143 self.gateway_ip = gateway_ip
144 self.tg_gateway_ip = tg_gateway_ip
145 self.ip_block = IpBlock(self.ip, ip_addrs_step, flow_count)
146 self.gw_ip_block = IpBlock(gateway_ip,
147 gateway_ip_addrs_step,
149 self.tg_gw_ip_block = IpBlock(tg_gateway_ip,
150 tg_gateway_ip_addrs_step,
152 self.udp_src_port = udp_src_port
153 self.udp_dst_port = udp_dst_port
155 def set_mac(self, mac):
157 raise TrafficClientException('Trying to set traffic generator MAC address as None')
160 def set_destination(self, dst):
163 def set_vm_mac_list(self, vm_mac_list):
164 self.vm_mac_list = map(str, vm_mac_list)
166 def set_vlan_tag(self, vlan_tag):
167 if self.vlan_tagging and vlan_tag is None:
168 raise TrafficClientException('Trying to set VLAN tag as None')
169 self.vlan_tag = vlan_tag
171 def get_gw_ip(self, chain_index):
172 '''Retrieve the IP address assigned for the gateway of a given chain
174 return self.gw_ip_block.get_ip(chain_index)
176 def get_stream_configs(self, service_chain):
178 # exact flow count for each chain is calculated as follows:
179 # - all chains except the first will have the same flow count
180 # calculated as (total_flows + chain_count - 1) / chain_count
181 # - the first chain will have the remainder
182 # example 11 flows and 3 chains => 3, 4, 4
183 flows_per_chain = (self.flow_count + self.chain_count - 1) / self.chain_count
184 cur_chain_flow_count = self.flow_count - flows_per_chain * (self.chain_count - 1)
186 self.ip_block.reset_reservation()
187 self.dst.ip_block.reset_reservation()
189 for chain_idx in xrange(self.chain_count):
190 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range(cur_chain_flow_count)
191 dst_ip_first, dst_ip_last = self.dst.ip_block.reserve_ip_range(cur_chain_flow_count)
193 'count': cur_chain_flow_count,
195 'mac_dst': self.dst.mac if service_chain == ChainType.EXT else self.vm_mac_list[
197 'ip_src_addr': src_ip_first,
198 'ip_src_addr_max': src_ip_last,
199 'ip_src_count': cur_chain_flow_count,
200 'ip_dst_addr': dst_ip_first,
201 'ip_dst_addr_max': dst_ip_last,
202 'ip_dst_count': cur_chain_flow_count,
203 'ip_addrs_step': self.ip_addrs_step,
204 'udp_src_port': self.udp_src_port,
205 'udp_dst_port': self.udp_dst_port,
206 'mac_discovery_gw': self.get_gw_ip(chain_idx),
207 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
208 'ip_dst_tg_gw': self.dst.tg_gw_ip_block.get_ip(chain_idx),
209 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
211 # after first chain, fall back to the flow count for all other chains
212 cur_chain_flow_count = flows_per_chain
216 def ip_range_overlaps(self):
217 '''Check if this device ip range is overlapping with the dst device ip range
219 src_base_ip = Device.ip_to_int(self.ip)
220 dst_base_ip = Device.ip_to_int(self.dst.ip)
221 src_last_ip = src_base_ip + self.flow_count - 1
222 dst_last_ip = dst_base_ip + self.flow_count - 1
223 return dst_last_ip >= src_base_ip and src_last_ip >= dst_base_ip
227 return int(mac.translate(None, ":.- "), 16)
231 mac = format(i, 'x').zfill(12)
232 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
233 return ':'.join(blocks)
237 return struct.unpack("!I", socket.inet_aton(addr))[0]
240 def int_to_ip(nvalue):
241 return socket.inet_ntoa(struct.pack("!I", nvalue))
244 class RunningTrafficProfile(object):
245 """Represents traffic configuration for currently running traffic profile."""
247 DEFAULT_IP_STEP = '0.0.0.1'
248 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
250 def __init__(self, config, generator_profile):
251 generator_config = self.__match_generator_profile(config.traffic_generator,
253 self.generator_config = generator_config
254 self.service_chain = config.service_chain
255 self.service_chain_count = config.service_chain_count
256 self.flow_count = config.flow_count
257 self.host_name = generator_config.host_name
258 self.name = generator_config.name
259 self.tool = generator_config.tool
260 self.cores = generator_config.get('cores', 1)
261 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
262 self.tg_gateway_ip_addrs_step = \
263 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
264 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
265 self.gateway_ips = generator_config.gateway_ip_addrs
266 self.ip = generator_config.ip
267 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
268 self.vlan_tagging = config.vlan_tagging
269 self.no_arp = config.no_arp
270 self.src_device = None
271 self.dst_device = None
272 self.vm_mac_list = None
273 self.__prep_interfaces(generator_config)
276 return dict(self.generator_config)
278 def set_vm_mac_list(self, vm_mac_list):
279 self.src_device.set_vm_mac_list(vm_mac_list[0])
280 self.dst_device.set_vm_mac_list(vm_mac_list[1])
283 def __match_generator_profile(traffic_generator, generator_profile):
284 generator_config = AttrDict(traffic_generator)
285 generator_config.pop('default_profile')
286 generator_config.pop('generator_profile')
287 matching_profile = [profile for profile in traffic_generator.generator_profile if
288 profile.name == generator_profile]
289 if len(matching_profile) != 1:
290 raise Exception('Traffic generator profile not found: ' + generator_profile)
292 generator_config.update(matching_profile[0])
294 return generator_config
296 def __prep_interfaces(self, generator_config):
298 'chain_count': self.service_chain_count,
299 'flow_count': self.flow_count / 2,
300 'ip': generator_config.ip_addrs[0],
301 'ip_addrs_step': self.ip_addrs_step,
302 'gateway_ip': self.gateway_ips[0],
303 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
304 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
305 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
306 'udp_src_port': generator_config.udp_src_port,
307 'udp_dst_port': generator_config.udp_dst_port,
308 'vlan_tagging': self.vlan_tagging
311 'chain_count': self.service_chain_count,
312 'flow_count': self.flow_count / 2,
313 'ip': generator_config.ip_addrs[1],
314 'ip_addrs_step': self.ip_addrs_step,
315 'gateway_ip': self.gateway_ips[1],
316 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
317 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
318 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
319 'udp_src_port': generator_config.udp_src_port,
320 'udp_dst_port': generator_config.udp_dst_port,
321 'vlan_tagging': self.vlan_tagging
324 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
325 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
326 self.src_device.set_destination(self.dst_device)
327 self.dst_device.set_destination(self.src_device)
329 if self.service_chain == ChainType.EXT and not self.no_arp \
330 and self.src_device.ip_range_overlaps():
331 raise Exception('Overlapping IP address ranges src=%s dst=%d flows=%d' %
338 return [self.src_device, self.dst_device]
342 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
346 return [self.src_device.port, self.dst_device.port]
349 def switch_ports(self):
350 return [self.src_device.switch_port, self.dst_device.switch_port]
354 return [self.src_device.pci, self.dst_device.pci]
357 class TrafficGeneratorFactory(object):
358 def __init__(self, config):
362 return self.config.generator_config.tool
364 def get_generator_client(self):
365 tool = self.get_tool().lower()
367 from traffic_gen import trex
368 return trex.TRex(self.config)
369 elif tool == 'dummy':
370 from traffic_gen import dummy
371 return dummy.DummyTG(self.config)
374 def list_generator_profile(self):
375 return [profile.name for profile in self.config.traffic_generator.generator_profile]
377 def get_generator_config(self, generator_profile):
378 return RunningTrafficProfile(self.config, generator_profile)
380 def get_matching_profile(self, traffic_profile_name):
381 matching_profile = [profile for profile in self.config.traffic_profile if
382 profile.name == traffic_profile_name]
384 if len(matching_profile) > 1:
385 raise Exception('Multiple traffic profiles with the same name found.')
386 elif not matching_profile:
387 raise Exception('No traffic profile found.')
389 return matching_profile[0]
391 def get_frame_sizes(self, traffic_profile):
392 matching_profile = self.get_matching_profile(traffic_profile)
393 return matching_profile.l2frame_size
396 class TrafficClient(object):
399 def __init__(self, config, notifier=None, skip_sleep=False):
400 generator_factory = TrafficGeneratorFactory(config)
401 self.gen = generator_factory.get_generator_client()
402 self.tool = generator_factory.get_tool()
404 self.notifier = notifier
405 self.interval_collector = None
406 self.iteration_collector = None
407 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
409 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
412 'l2frame_size': None,
413 'duration_sec': self.config.duration_sec,
414 'bidirectional': True,
415 'rates': [] # to avoid unsbuscriptable-obj warning
417 self.current_total_rate = {'rate_percent': '10'}
418 if self.config.single_run:
419 self.current_total_rate = utils.parse_rate_str(self.config.rate)
420 # UT with dummy TG can bypass all sleeps
421 self.skip_sleep = skip_sleep
424 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
427 def start_traffic_generator(self):
433 self.gen.config_interface()
434 self.gen.clear_stats()
436 def get_version(self):
437 return self.gen.get_version()
439 def ensure_end_to_end(self):
441 Ensure traffic generator receives packets it has transmitted.
442 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
444 At this point all VMs are in active state, but forwarding does not have to work.
445 Small amount of traffic is sent to every chain. Then total of sent and received packets
446 is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
447 N being number of chains, traffic flows through every chain and real measurements can be
451 PVP chain (1 VM per chain)
452 N = 10 (number of chains)
453 threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
454 if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
455 all 10 VMs are in operational state.
457 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
458 rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
459 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
461 # ensures enough traffic is coming back
462 threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
463 retry_count = (self.config.check_traffic_time_sec +
464 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
465 for it in xrange(retry_count):
466 self.gen.clear_stats()
467 self.gen.start_traffic()
468 LOG.info('Waiting for packets to be received back... (%d / %d)', it + 1, retry_count)
469 if not self.skip_sleep:
470 time.sleep(self.config.generic_poll_sec)
471 self.gen.stop_traffic()
472 stats = self.gen.get_stats()
474 # compute total sent and received traffic on both ports
477 for port in self.PORTS:
478 total_rx += float(stats[port]['rx'].get('total_pkts', 0))
479 total_tx += float(stats[port]['tx'].get('total_pkts', 0))
481 # how much of traffic came back
482 ratio = total_rx / total_tx if total_tx else 0
484 if ratio > threshold:
485 self.gen.clear_stats()
486 self.gen.clear_streamblock()
487 LOG.info('End-to-end connectivity ensured')
490 if not self.skip_sleep:
491 time.sleep(self.config.generic_poll_sec)
493 raise TrafficClientException('End-to-end connectivity cannot be ensured')
495 def ensure_arp_successful(self):
496 if not self.gen.resolve_arp():
497 raise TrafficClientException('ARP cannot be resolved')
499 def set_traffic(self, frame_size, bidirectional):
500 self.run_config['bidirectional'] = bidirectional
501 self.run_config['l2frame_size'] = frame_size
502 self.run_config['rates'] = [self.get_per_direction_rate()]
504 self.run_config['rates'].append(self.get_per_direction_rate())
506 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
507 if unidir_reverse_pps > 0:
508 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
510 self.gen.clear_streamblock()
511 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
513 def modify_load(self, load):
514 self.current_total_rate = {'rate_percent': str(load)}
515 rate_per_direction = self.get_per_direction_rate()
517 self.gen.modify_rate(rate_per_direction, False)
518 self.run_config['rates'][0] = rate_per_direction
519 if self.run_config['bidirectional']:
520 self.gen.modify_rate(rate_per_direction, True)
521 self.run_config['rates'][1] = rate_per_direction
523 def get_ndr_and_pdr(self):
524 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
526 if self.config.ndr_run:
527 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
528 targets['ndr'] = self.config.measurement.NDR
529 if self.config.pdr_run:
530 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
531 targets['pdr'] = self.config.measurement.PDR
533 self.run_config['start_time'] = time.time()
534 self.interval_collector = IntervalCollector(self.run_config['start_time'])
535 self.interval_collector.attach_notifier(self.notifier)
536 self.iteration_collector = IterationCollector(self.run_config['start_time'])
538 self.__range_search(0.0, 200.0, targets, results)
540 results['iteration_stats'] = {
541 'ndr_pdr': self.iteration_collector.get()
544 if self.config.ndr_run:
545 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
546 results['ndr']['time_taken_sec'] = \
547 results['ndr']['timestamp_sec'] - self.run_config['start_time']
548 if self.config.pdr_run:
549 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
550 results['pdr']['time_taken_sec'] = \
551 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
553 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
554 results['pdr']['time_taken_sec'] = \
555 results['pdr']['timestamp_sec'] - self.run_config['start_time']
558 def __get_dropped_rate(self, result):
559 dropped_pkts = result['rx']['dropped_pkts']
560 total_pkts = result['tx']['total_pkts']
563 return float(dropped_pkts) / total_pkts * 100
566 stats = self.gen.get_stats()
567 retDict = {'total_tx_rate': stats['total_tx_rate']}
568 for port in self.PORTS:
569 retDict[port] = {'tx': {}, 'rx': {}}
571 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
572 rx_keys = tx_keys + ['dropped_pkts']
574 for port in self.PORTS:
576 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
579 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
581 retDict[port]['rx'][key] = 0
582 retDict[port]['rx']['avg_delay_usec'] = cast_integer(
583 stats[port]['rx']['avg_delay_usec'])
584 retDict[port]['rx']['min_delay_usec'] = cast_integer(
585 stats[port]['rx']['min_delay_usec'])
586 retDict[port]['rx']['max_delay_usec'] = cast_integer(
587 stats[port]['rx']['max_delay_usec'])
588 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
590 ports = sorted(retDict.keys())
591 if self.run_config['bidirectional']:
592 retDict['overall'] = {'tx': {}, 'rx': {}}
594 retDict['overall']['tx'][key] = \
595 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
597 retDict['overall']['rx'][key] = \
598 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
599 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
600 retDict[ports[1]]['rx']['total_pkts']]
601 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
602 retDict[ports[1]]['rx']['avg_delay_usec']]
603 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
604 retDict[ports[1]]['rx']['max_delay_usec']]
605 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
606 retDict[ports[1]]['rx']['min_delay_usec']]
607 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
608 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
609 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
610 for key in ['pkt_bit_rate', 'pkt_rate']:
611 for dirc in ['tx', 'rx']:
612 retDict['overall'][dirc][key] /= 2.0
614 retDict['overall'] = retDict[ports[0]]
615 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
618 def __convert_rates(self, rate):
619 return utils.convert_rates(self.run_config['l2frame_size'],
621 self.config.generator_config.intf_speed)
623 def __ndr_pdr_found(self, tag, load):
624 rates = self.__convert_rates({'rate_percent': load})
625 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
626 last_stats = self.iteration_collector.peek()
627 self.interval_collector.add_ndr_pdr(tag, last_stats)
629 def __format_output_stats(self, stats):
630 for key in self.PORTS + ['overall']:
631 interface = stats[key]
633 'tx_pkts': interface['tx']['total_pkts'],
634 'rx_pkts': interface['rx']['total_pkts'],
635 'drop_percentage': interface['drop_rate_percent'],
636 'drop_pct': interface['rx']['dropped_pkts'],
637 'avg_delay_usec': interface['rx']['avg_delay_usec'],
638 'max_delay_usec': interface['rx']['max_delay_usec'],
639 'min_delay_usec': interface['rx']['min_delay_usec'],
644 def __targets_found(self, rate, targets, results):
645 for tag, target in targets.iteritems():
646 LOG.info('Found %s (%s) load: %s', tag, target, rate)
647 self.__ndr_pdr_found(tag, rate)
648 results[tag]['timestamp_sec'] = time.time()
650 def __range_search(self, left, right, targets, results):
651 '''Perform a binary search for a list of targets inside a [left..right] range or rate
653 left the left side of the range to search as a % the line rate (100 = 100% line rate)
654 indicating the rate to send on each interface
655 right the right side of the range to search as a % of line rate
656 indicating the rate to send on each interface
657 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
659 results a dict to store results
663 LOG.info('Range search [%s .. %s] targets: %s', left, right, targets)
665 # Terminate search when gap is less than load epsilon
666 if right - left < self.config.measurement.load_epsilon:
667 self.__targets_found(left, targets, results)
670 # Obtain the average drop rate in for middle load
671 middle = (left + right) / 2.0
673 stats, rates = self.__run_search_iteration(middle)
675 LOG.exception("Got exception from traffic generator during binary search")
676 self.__targets_found(left, targets, results)
678 # Split target dicts based on the avg drop rate
681 for tag, target in targets.iteritems():
682 if stats['overall']['drop_rate_percent'] <= target:
683 # record the best possible rate found for this target
685 results[tag].update({
686 'load_percent_per_direction': middle,
687 'stats': self.__format_output_stats(dict(stats)),
688 'timestamp_sec': None
690 right_targets[tag] = target
692 # initialize to 0 all fields of result for
693 # the worst case scenario of the binary search (if ndr/pdr is not found)
694 if tag not in results:
695 results[tag] = dict.fromkeys(rates, 0)
696 empty_stats = self.__format_output_stats(dict(stats))
697 for key in empty_stats:
698 if isinstance(empty_stats[key], dict):
699 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
702 results[tag].update({
703 'load_percent_per_direction': 0,
704 'stats': empty_stats,
705 'timestamp_sec': None
707 left_targets[tag] = target
710 self.__range_search(left, middle, left_targets, results)
712 # search upper half only if the upper rate does not exceed
713 # 100%, this only happens when the first search at 100%
714 # yields a DR that is < target DR
716 self.__targets_found(100, right_targets, results)
718 self.__range_search(middle, right, right_targets, results)
720 def __run_search_iteration(self, rate):
722 self.modify_load(rate)
724 # poll interval stats and collect them
725 for stats in self.run_traffic():
726 self.interval_collector.add(stats)
727 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
728 if time_elapsed_ratio >= 1:
729 self.cancel_traffic()
730 self.interval_collector.reset()
732 # get stats from the run
733 stats = self.runner.client.get_stats()
734 current_traffic_config = self.get_traffic_config()
735 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
736 stats['total_tx_rate'])
737 if warning is not None:
738 stats['warning'] = warning
740 # save reliable stats from whole iteration
741 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
742 LOG.info('Average drop rate: %f', stats['overall']['drop_rate_percent'])
744 return stats, current_traffic_config['direction-total']
747 def log_stats(stats):
749 'datetime': str(datetime.now()),
750 'tx_packets': stats['overall']['tx']['total_pkts'],
751 'rx_packets': stats['overall']['rx']['total_pkts'],
752 'drop_packets': stats['overall']['rx']['dropped_pkts'],
753 'drop_rate_percent': stats['overall']['drop_rate_percent']
755 LOG.info('TX: %(tx_packets)d; '
756 'RX: %(rx_packets)d; '
757 'Dropped: %(drop_packets)d; '
758 'Drop rate: %(drop_rate_percent).4f%%',
761 def run_traffic(self):
762 stats = self.runner.run()
763 while self.runner.is_running:
764 self.log_stats(stats)
766 stats = self.runner.poll_stats()
769 self.log_stats(stats)
770 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
773 def cancel_traffic(self):
776 def get_interface(self, port_index):
777 port = self.gen.port_handle[port_index]
779 if not self.config.no_traffic:
780 stats = self.get_stats()
782 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
783 return Interface('traffic-generator', self.tool.lower(), tx, rx)
785 def get_traffic_config(self):
790 for idx, rate in enumerate(self.run_config['rates']):
791 key = 'direction-forward' if idx == 0 else 'direction-reverse'
793 'l2frame_size': self.run_config['l2frame_size'],
794 'duration_sec': self.run_config['duration_sec']
796 config[key].update(rate)
797 config[key].update(self.__convert_rates(rate))
798 load_total += float(config[key]['rate_percent'])
799 bps_total += float(config[key]['rate_bps'])
800 pps_total += float(config[key]['rate_pps'])
801 config['direction-total'] = dict(config['direction-forward'])
802 config['direction-total'].update({
803 'rate_percent': load_total,
804 'rate_pps': cast_integer(pps_total),
805 'rate_bps': bps_total
810 def get_run_config(self, results):
811 """Returns configuration which was used for the last run."""
813 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
814 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
815 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
817 "orig": self.__convert_rates(self.run_config['rates'][idx]),
818 "tx": self.__convert_rates({'rate_pps': tx_rate}),
819 "rx": self.__convert_rates({'rate_pps': rx_rate})
823 for direction in ['orig', 'tx', 'rx']:
824 total[direction] = {}
825 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
827 total[direction][unit] = sum([float(x[direction][unit]) for x in r.values()])
829 r['direction-total'] = total
833 def compare_tx_rates(required, actual):
835 are_different = False
837 if float(actual) / required < threshold:
839 except ZeroDivisionError:
843 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
844 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
845 "to achieve the requested TX rate.".format(r=required, a=actual)
851 def get_per_direction_rate(self):
852 divisor = 2 if self.run_config['bidirectional'] else 1
853 if 'rate_percent' in self.current_total_rate:
854 # don't split rate if it's percentage
857 return utils.divide_rate(self.current_total_rate, divisor)
861 self.gen.stop_traffic()
864 self.gen.clear_stats()