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
21 from attrdict import AttrDict
23 from netaddr import IPNetwork
24 # pylint: disable=import-error
25 from trex_stl_lib.api import STLError
26 # pylint: enable=import-error
29 from network import Interface
30 from specs import ChainType
31 from stats_collector import IntervalCollector
32 from stats_collector import IterationCollector
33 import traffic_gen.traffic_utils as utils
34 from utils import cast_integer
37 class TrafficClientException(Exception):
41 class TrafficRunner(object):
42 def __init__(self, client, duration_sec, interval_sec=0):
44 self.start_time = None
45 self.duration_sec = duration_sec
46 self.interval_sec = interval_sec
49 LOG.info('Running traffic generator')
50 self.client.gen.clear_stats()
51 self.client.gen.start_traffic()
52 self.start_time = time.time()
53 return self.poll_stats()
57 self.start_time = None
58 self.client.gen.stop_traffic()
61 return self.start_time is not None
63 def time_elapsed(self):
65 return time.time() - self.start_time
66 return self.duration_sec
69 if not self.is_running():
71 if self.client.skip_sleep:
73 return self.client.get_stats()
74 time_elapsed = self.time_elapsed()
75 if time_elapsed > self.duration_sec:
78 time_left = self.duration_sec - time_elapsed
79 if self.interval_sec > 0.0:
80 if time_left <= self.interval_sec:
84 time.sleep(self.interval_sec)
86 time.sleep(self.duration_sec)
88 return self.client.get_stats()
91 class IpBlock(object):
92 def __init__(self, base_ip, step_ip, count_ip):
93 self.base_ip_int = Device.ip_to_int(base_ip)
94 self.step = Device.ip_to_int(step_ip)
95 self.max_available = count_ip
98 def get_ip(self, index=0):
99 '''Return the IP address at given index
101 if index < 0 or index >= self.max_available:
102 raise IndexError('Index out of bounds')
103 return Device.int_to_ip(self.base_ip_int + index * self.step)
105 def reserve_ip_range(self, count):
106 '''Reserve a range of count consecutive IP addresses spaced by step
108 if self.next_free + count > self.max_available:
109 raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' %
113 first_ip = self.get_ip(self.next_free)
114 last_ip = self.get_ip(self.next_free + count - 1)
115 self.next_free += count
116 return (first_ip, last_ip)
118 def reset_reservation(self):
122 class Device(object):
123 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
124 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
125 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
126 dst_mac=None, chain_count=1, flow_count=1, vlan_tagging=False):
127 self.chain_count = chain_count
128 self.flow_count = flow_count
131 self.switch_port = switch_port
132 self.vtep_vlan = vtep_vlan
134 self.vlan_tagging = vlan_tagging
137 self.dst_mac = dst_mac
138 self.vm_mac_list = None
139 subnet = IPNetwork(ip)
140 self.ip = subnet.ip.format()
141 self.ip_prefixlen = subnet.prefixlen
142 self.ip_addrs_step = ip_addrs_step
143 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
144 self.gateway_ip_addrs_step = gateway_ip_addrs_step
145 self.gateway_ip = gateway_ip
146 self.tg_gateway_ip = tg_gateway_ip
147 self.ip_block = IpBlock(self.ip, ip_addrs_step, flow_count)
148 self.gw_ip_block = IpBlock(gateway_ip,
149 gateway_ip_addrs_step,
151 self.tg_gw_ip_block = IpBlock(tg_gateway_ip,
152 tg_gateway_ip_addrs_step,
154 self.udp_src_port = udp_src_port
155 self.udp_dst_port = udp_dst_port
157 def set_mac(self, mac):
159 raise TrafficClientException('Trying to set traffic generator MAC address as None')
162 def set_destination(self, dst):
165 def set_vm_mac_list(self, vm_mac_list):
166 self.vm_mac_list = map(str, vm_mac_list)
168 def set_vlan_tag(self, vlan_tag):
169 if self.vlan_tagging and vlan_tag is None:
170 raise TrafficClientException('Trying to set VLAN tag as None')
171 self.vlan_tag = vlan_tag
173 def get_gw_ip(self, chain_index):
174 '''Retrieve the IP address assigned for the gateway of a given chain
176 return self.gw_ip_block.get_ip(chain_index)
178 def get_stream_configs(self, service_chain):
180 # exact flow count for each chain is calculated as follows:
181 # - all chains except the first will have the same flow count
182 # calculated as (total_flows + chain_count - 1) / chain_count
183 # - the first chain will have the remainder
184 # example 11 flows and 3 chains => 3, 4, 4
185 flows_per_chain = (self.flow_count + self.chain_count - 1) / self.chain_count
186 cur_chain_flow_count = self.flow_count - flows_per_chain * (self.chain_count - 1)
188 self.ip_block.reset_reservation()
189 self.dst.ip_block.reset_reservation()
191 for chain_idx in xrange(self.chain_count):
192 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range(cur_chain_flow_count)
193 dst_ip_first, dst_ip_last = self.dst.ip_block.reserve_ip_range(cur_chain_flow_count)
195 dst_mac = self.dst_mac[chain_idx] if self.dst_mac is not None else self.dst.mac
196 if not re.match("[0-9a-f]{2}([-:])[0-9a-f]{2}(\\1[0-9a-f]{2}){4}$", dst_mac.lower()):
197 raise TrafficClientException("Invalid MAC address '{mac}' specified in "
198 "mac_addrs_left/right".format(mac=dst_mac))
201 'count': cur_chain_flow_count,
203 'mac_dst': dst_mac if service_chain == ChainType.EXT else self.vm_mac_list[
205 'ip_src_addr': src_ip_first,
206 'ip_src_addr_max': src_ip_last,
207 'ip_src_count': cur_chain_flow_count,
208 'ip_dst_addr': dst_ip_first,
209 'ip_dst_addr_max': dst_ip_last,
210 'ip_dst_count': cur_chain_flow_count,
211 'ip_addrs_step': self.ip_addrs_step,
212 'udp_src_port': self.udp_src_port,
213 'udp_dst_port': self.udp_dst_port,
214 'mac_discovery_gw': self.get_gw_ip(chain_idx),
215 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
216 'ip_dst_tg_gw': self.dst.tg_gw_ip_block.get_ip(chain_idx),
217 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
219 # after first chain, fall back to the flow count for all other chains
220 cur_chain_flow_count = flows_per_chain
224 def ip_range_overlaps(self):
225 '''Check if this device ip range is overlapping with the dst device ip range
227 src_base_ip = Device.ip_to_int(self.ip)
228 dst_base_ip = Device.ip_to_int(self.dst.ip)
229 src_last_ip = src_base_ip + self.flow_count - 1
230 dst_last_ip = dst_base_ip + self.flow_count - 1
231 return dst_last_ip >= src_base_ip and src_last_ip >= dst_base_ip
235 return int(mac.translate(None, ":.- "), 16)
239 mac = format(i, 'x').zfill(12)
240 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
241 return ':'.join(blocks)
245 return struct.unpack("!I", socket.inet_aton(addr))[0]
248 def int_to_ip(nvalue):
249 return socket.inet_ntoa(struct.pack("!I", nvalue))
252 class RunningTrafficProfile(object):
253 """Represents traffic configuration for currently running traffic profile."""
255 DEFAULT_IP_STEP = '0.0.0.1'
256 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
258 def __init__(self, config, generator_profile):
259 generator_config = self.__match_generator_profile(config.traffic_generator,
261 self.generator_config = generator_config
262 self.service_chain = config.service_chain
263 self.service_chain_count = config.service_chain_count
264 self.flow_count = config.flow_count
265 self.host_name = generator_config.host_name
266 self.name = generator_config.name
267 self.tool = generator_config.tool
268 self.cores = generator_config.get('cores', 1)
269 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
270 self.tg_gateway_ip_addrs_step = \
271 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
272 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
273 self.gateway_ips = generator_config.gateway_ip_addrs
274 self.ip = generator_config.ip
275 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
276 self.vlan_tagging = config.vlan_tagging
277 self.no_arp = config.no_arp
278 self.src_device = None
279 self.dst_device = None
280 self.vm_mac_list = None
281 self.mac_addrs_left = generator_config.mac_addrs_left
282 self.mac_addrs_right = generator_config.mac_addrs_right
283 self.__prep_interfaces(generator_config)
286 return dict(self.generator_config)
288 def set_vm_mac_list(self, vm_mac_list):
289 self.src_device.set_vm_mac_list(vm_mac_list[0])
290 self.dst_device.set_vm_mac_list(vm_mac_list[1])
293 def __match_generator_profile(traffic_generator, generator_profile):
294 generator_config = AttrDict(traffic_generator)
295 generator_config.pop('default_profile')
296 generator_config.pop('generator_profile')
297 matching_profile = [profile for profile in traffic_generator.generator_profile if
298 profile.name == generator_profile]
299 if len(matching_profile) != 1:
300 raise Exception('Traffic generator profile not found: ' + generator_profile)
302 generator_config.update(matching_profile[0])
304 return generator_config
306 def __prep_interfaces(self, generator_config):
308 'chain_count': self.service_chain_count,
309 'flow_count': self.flow_count / 2,
310 'ip': generator_config.ip_addrs[0],
311 'ip_addrs_step': self.ip_addrs_step,
312 'gateway_ip': self.gateway_ips[0],
313 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
314 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
315 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
316 'udp_src_port': generator_config.udp_src_port,
317 'udp_dst_port': generator_config.udp_dst_port,
318 'vlan_tagging': self.vlan_tagging,
319 'dst_mac': generator_config.mac_addrs_left
322 'chain_count': self.service_chain_count,
323 'flow_count': self.flow_count / 2,
324 'ip': generator_config.ip_addrs[1],
325 'ip_addrs_step': self.ip_addrs_step,
326 'gateway_ip': self.gateway_ips[1],
327 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
328 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
329 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
330 'udp_src_port': generator_config.udp_src_port,
331 'udp_dst_port': generator_config.udp_dst_port,
332 'vlan_tagging': self.vlan_tagging,
333 'dst_mac': generator_config.mac_addrs_right
336 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
337 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
338 self.src_device.set_destination(self.dst_device)
339 self.dst_device.set_destination(self.src_device)
341 if self.service_chain == ChainType.EXT and not self.no_arp \
342 and self.src_device.ip_range_overlaps():
343 raise Exception('Overlapping IP address ranges src=%s dst=%d flows=%d' %
350 return [self.src_device, self.dst_device]
354 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
358 return [self.src_device.port, self.dst_device.port]
361 def switch_ports(self):
362 return [self.src_device.switch_port, self.dst_device.switch_port]
366 return [self.src_device.pci, self.dst_device.pci]
369 class TrafficGeneratorFactory(object):
370 def __init__(self, config):
374 return self.config.generator_config.tool
376 def get_generator_client(self):
377 tool = self.get_tool().lower()
379 from traffic_gen import trex
380 return trex.TRex(self.config)
381 elif tool == 'dummy':
382 from traffic_gen import dummy
383 return dummy.DummyTG(self.config)
386 def list_generator_profile(self):
387 return [profile.name for profile in self.config.traffic_generator.generator_profile]
389 def get_generator_config(self, generator_profile):
390 return RunningTrafficProfile(self.config, generator_profile)
392 def get_matching_profile(self, traffic_profile_name):
393 matching_profile = [profile for profile in self.config.traffic_profile if
394 profile.name == traffic_profile_name]
396 if len(matching_profile) > 1:
397 raise Exception('Multiple traffic profiles with the same name found.')
398 elif not matching_profile:
399 raise Exception('No traffic profile found.')
401 return matching_profile[0]
403 def get_frame_sizes(self, traffic_profile):
404 matching_profile = self.get_matching_profile(traffic_profile)
405 return matching_profile.l2frame_size
408 class TrafficClient(object):
411 def __init__(self, config, notifier=None, skip_sleep=False):
412 generator_factory = TrafficGeneratorFactory(config)
413 self.gen = generator_factory.get_generator_client()
414 self.tool = generator_factory.get_tool()
416 self.notifier = notifier
417 self.interval_collector = None
418 self.iteration_collector = None
419 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
421 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
424 'l2frame_size': None,
425 'duration_sec': self.config.duration_sec,
426 'bidirectional': True,
427 'rates': [] # to avoid unsbuscriptable-obj warning
429 self.current_total_rate = {'rate_percent': '10'}
430 if self.config.single_run:
431 self.current_total_rate = utils.parse_rate_str(self.config.rate)
432 # UT with dummy TG can bypass all sleeps
433 self.skip_sleep = skip_sleep
436 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
439 def start_traffic_generator(self):
445 self.gen.config_interface()
446 self.gen.clear_stats()
448 def get_version(self):
449 return self.gen.get_version()
451 def ensure_end_to_end(self):
453 Ensure traffic generator receives packets it has transmitted.
454 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
456 VMs that are started and in active state may not pass traffic yet. It is imperative to make
457 sure that all VMs are passing traffic in both directions before starting any benchmarking.
458 To verify this, we need to send at a low frequency bi-directional packets and make sure
459 that we receive all packets back from all VMs. The number of flows is equal to 2 times
460 the number of chains (1 per direction) and we need to make sure we receive packets coming
461 from exactly 2 x chain count different source MAC addresses.
464 PVP chain (1 VM per chain)
465 N = 10 (number of chains)
466 Flow count = 20 (number of flows)
467 If the number of unique source MAC addresses from received packets is 20 then
468 all 10 VMs 10 VMs are in operational state.
470 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
471 rate_pps = {'rate_pps': str(self.config.service_chain_count * 1)}
472 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
474 # ensures enough traffic is coming back
475 retry_count = (self.config.check_traffic_time_sec +
476 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
477 mac_addresses = set()
479 for it in xrange(retry_count):
480 self.gen.clear_stats()
481 self.gen.start_traffic()
482 self.gen.start_capture()
483 LOG.info('Waiting for packets to be received back... (%d / %d)', it + 1, retry_count)
484 if not self.skip_sleep:
485 time.sleep(self.config.generic_poll_sec)
486 self.gen.stop_traffic()
487 self.gen.fetch_capture_packets()
488 self.gen.stop_capture()
490 for packet in self.gen.packet_list:
491 mac_addresses.add(packet['binary'][6:12])
492 if ln != len(mac_addresses):
493 ln = len(mac_addresses)
494 LOG.info('Flows passing traffic %d / %d', ln,
495 self.config.service_chain_count * 2)
496 if len(mac_addresses) == self.config.service_chain_count * 2:
497 LOG.info('End-to-end connectivity ensured')
500 if not self.skip_sleep:
501 time.sleep(self.config.generic_poll_sec)
503 raise TrafficClientException('End-to-end connectivity cannot be ensured')
505 def ensure_arp_successful(self):
506 if not self.gen.resolve_arp():
507 raise TrafficClientException('ARP cannot be resolved')
509 def set_traffic(self, frame_size, bidirectional):
510 self.run_config['bidirectional'] = bidirectional
511 self.run_config['l2frame_size'] = frame_size
512 self.run_config['rates'] = [self.get_per_direction_rate()]
514 self.run_config['rates'].append(self.get_per_direction_rate())
516 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
517 if unidir_reverse_pps > 0:
518 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
519 # Fix for [NFVBENCH-67], convert the rate string to PPS
520 for idx, rate in enumerate(self.run_config['rates']):
521 if 'rate_pps' not in rate:
522 self.run_config['rates'][idx] = {'rate_pps': self.__convert_rates(rate)['rate_pps']}
524 self.gen.clear_streamblock()
525 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
527 def modify_load(self, load):
528 self.current_total_rate = {'rate_percent': str(load)}
529 rate_per_direction = self.get_per_direction_rate()
531 self.gen.modify_rate(rate_per_direction, False)
532 self.run_config['rates'][0] = rate_per_direction
533 if self.run_config['bidirectional']:
534 self.gen.modify_rate(rate_per_direction, True)
535 self.run_config['rates'][1] = rate_per_direction
537 def get_ndr_and_pdr(self):
538 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
540 if self.config.ndr_run:
541 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
542 targets['ndr'] = self.config.measurement.NDR
543 if self.config.pdr_run:
544 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
545 targets['pdr'] = self.config.measurement.PDR
547 self.run_config['start_time'] = time.time()
548 self.interval_collector = IntervalCollector(self.run_config['start_time'])
549 self.interval_collector.attach_notifier(self.notifier)
550 self.iteration_collector = IterationCollector(self.run_config['start_time'])
552 self.__range_search(0.0, 200.0, targets, results)
554 results['iteration_stats'] = {
555 'ndr_pdr': self.iteration_collector.get()
558 if self.config.ndr_run:
559 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
560 results['ndr']['time_taken_sec'] = \
561 results['ndr']['timestamp_sec'] - self.run_config['start_time']
562 if self.config.pdr_run:
563 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
564 results['pdr']['time_taken_sec'] = \
565 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
567 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
568 results['pdr']['time_taken_sec'] = \
569 results['pdr']['timestamp_sec'] - self.run_config['start_time']
572 def __get_dropped_rate(self, result):
573 dropped_pkts = result['rx']['dropped_pkts']
574 total_pkts = result['tx']['total_pkts']
577 return float(dropped_pkts) / total_pkts * 100
580 stats = self.gen.get_stats()
581 retDict = {'total_tx_rate': stats['total_tx_rate']}
582 for port in self.PORTS:
583 retDict[port] = {'tx': {}, 'rx': {}}
585 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
586 rx_keys = tx_keys + ['dropped_pkts']
588 for port in self.PORTS:
590 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
593 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
595 retDict[port]['rx'][key] = 0
596 retDict[port]['rx']['avg_delay_usec'] = cast_integer(
597 stats[port]['rx']['avg_delay_usec'])
598 retDict[port]['rx']['min_delay_usec'] = cast_integer(
599 stats[port]['rx']['min_delay_usec'])
600 retDict[port]['rx']['max_delay_usec'] = cast_integer(
601 stats[port]['rx']['max_delay_usec'])
602 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
604 ports = sorted(retDict.keys())
605 if self.run_config['bidirectional']:
606 retDict['overall'] = {'tx': {}, 'rx': {}}
608 retDict['overall']['tx'][key] = \
609 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
611 retDict['overall']['rx'][key] = \
612 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
613 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
614 retDict[ports[1]]['rx']['total_pkts']]
615 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
616 retDict[ports[1]]['rx']['avg_delay_usec']]
617 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
618 retDict[ports[1]]['rx']['max_delay_usec']]
619 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
620 retDict[ports[1]]['rx']['min_delay_usec']]
621 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
622 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
623 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
624 for key in ['pkt_bit_rate', 'pkt_rate']:
625 for dirc in ['tx', 'rx']:
626 retDict['overall'][dirc][key] /= 2.0
628 retDict['overall'] = retDict[ports[0]]
629 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
632 def __convert_rates(self, rate):
633 return utils.convert_rates(self.run_config['l2frame_size'],
635 self.config.generator_config.intf_speed)
637 def __ndr_pdr_found(self, tag, load):
638 rates = self.__convert_rates({'rate_percent': load})
639 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
640 last_stats = self.iteration_collector.peek()
641 self.interval_collector.add_ndr_pdr(tag, last_stats)
643 def __format_output_stats(self, stats):
644 for key in self.PORTS + ['overall']:
645 interface = stats[key]
647 'tx_pkts': interface['tx']['total_pkts'],
648 'rx_pkts': interface['rx']['total_pkts'],
649 'drop_percentage': interface['drop_rate_percent'],
650 'drop_pct': interface['rx']['dropped_pkts'],
651 'avg_delay_usec': interface['rx']['avg_delay_usec'],
652 'max_delay_usec': interface['rx']['max_delay_usec'],
653 'min_delay_usec': interface['rx']['min_delay_usec'],
658 def __targets_found(self, rate, targets, results):
659 for tag, target in targets.iteritems():
660 LOG.info('Found %s (%s) load: %s', tag, target, rate)
661 self.__ndr_pdr_found(tag, rate)
662 results[tag]['timestamp_sec'] = time.time()
664 def __range_search(self, left, right, targets, results):
665 '''Perform a binary search for a list of targets inside a [left..right] range or rate
667 left the left side of the range to search as a % the line rate (100 = 100% line rate)
668 indicating the rate to send on each interface
669 right the right side of the range to search as a % of line rate
670 indicating the rate to send on each interface
671 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
673 results a dict to store results
677 LOG.info('Range search [%s .. %s] targets: %s', left, right, targets)
679 # Terminate search when gap is less than load epsilon
680 if right - left < self.config.measurement.load_epsilon:
681 self.__targets_found(left, targets, results)
684 # Obtain the average drop rate in for middle load
685 middle = (left + right) / 2.0
687 stats, rates = self.__run_search_iteration(middle)
689 LOG.exception("Got exception from traffic generator during binary search")
690 self.__targets_found(left, targets, results)
692 # Split target dicts based on the avg drop rate
695 for tag, target in targets.iteritems():
696 if stats['overall']['drop_rate_percent'] <= target:
697 # record the best possible rate found for this target
699 results[tag].update({
700 'load_percent_per_direction': middle,
701 'stats': self.__format_output_stats(dict(stats)),
702 'timestamp_sec': None
704 right_targets[tag] = target
706 # initialize to 0 all fields of result for
707 # the worst case scenario of the binary search (if ndr/pdr is not found)
708 if tag not in results:
709 results[tag] = dict.fromkeys(rates, 0)
710 empty_stats = self.__format_output_stats(dict(stats))
711 for key in empty_stats:
712 if isinstance(empty_stats[key], dict):
713 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
716 results[tag].update({
717 'load_percent_per_direction': 0,
718 'stats': empty_stats,
719 'timestamp_sec': None
721 left_targets[tag] = target
724 self.__range_search(left, middle, left_targets, results)
726 # search upper half only if the upper rate does not exceed
727 # 100%, this only happens when the first search at 100%
728 # yields a DR that is < target DR
730 self.__targets_found(100, right_targets, results)
732 self.__range_search(middle, right, right_targets, results)
734 def __run_search_iteration(self, rate):
736 self.modify_load(rate)
738 # poll interval stats and collect them
739 for stats in self.run_traffic():
740 self.interval_collector.add(stats)
741 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
742 if time_elapsed_ratio >= 1:
743 self.cancel_traffic()
744 self.interval_collector.reset()
746 # get stats from the run
747 stats = self.runner.client.get_stats()
748 current_traffic_config = self.get_traffic_config()
749 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
750 stats['total_tx_rate'])
751 if warning is not None:
752 stats['warning'] = warning
754 # save reliable stats from whole iteration
755 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
756 LOG.info('Average drop rate: %f', stats['overall']['drop_rate_percent'])
758 return stats, current_traffic_config['direction-total']
761 def log_stats(stats):
763 'datetime': str(datetime.now()),
764 'tx_packets': stats['overall']['tx']['total_pkts'],
765 'rx_packets': stats['overall']['rx']['total_pkts'],
766 'drop_packets': stats['overall']['rx']['dropped_pkts'],
767 'drop_rate_percent': stats['overall']['drop_rate_percent']
769 LOG.info('TX: %(tx_packets)d; '
770 'RX: %(rx_packets)d; '
771 'Dropped: %(drop_packets)d; '
772 'Drop rate: %(drop_rate_percent).4f%%',
775 def run_traffic(self):
776 stats = self.runner.run()
777 while self.runner.is_running:
778 self.log_stats(stats)
780 stats = self.runner.poll_stats()
783 self.log_stats(stats)
784 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
787 def cancel_traffic(self):
790 def get_interface(self, port_index):
791 port = self.gen.port_handle[port_index]
793 if not self.config.no_traffic:
794 stats = self.get_stats()
796 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
797 return Interface('traffic-generator', self.tool.lower(), tx, rx)
799 def get_traffic_config(self):
804 for idx, rate in enumerate(self.run_config['rates']):
805 key = 'direction-forward' if idx == 0 else 'direction-reverse'
807 'l2frame_size': self.run_config['l2frame_size'],
808 'duration_sec': self.run_config['duration_sec']
810 config[key].update(rate)
811 config[key].update(self.__convert_rates(rate))
812 load_total += float(config[key]['rate_percent'])
813 bps_total += float(config[key]['rate_bps'])
814 pps_total += float(config[key]['rate_pps'])
815 config['direction-total'] = dict(config['direction-forward'])
816 config['direction-total'].update({
817 'rate_percent': load_total,
818 'rate_pps': cast_integer(pps_total),
819 'rate_bps': bps_total
824 def get_run_config(self, results):
825 """Returns configuration which was used for the last run."""
827 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
828 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
829 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
831 "orig": self.__convert_rates(self.run_config['rates'][idx]),
832 "tx": self.__convert_rates({'rate_pps': tx_rate}),
833 "rx": self.__convert_rates({'rate_pps': rx_rate})
837 for direction in ['orig', 'tx', 'rx']:
838 total[direction] = {}
839 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
840 total[direction][unit] = sum([float(x[direction][unit]) for x in r.values()])
842 r['direction-total'] = total
846 def compare_tx_rates(required, actual):
848 are_different = False
850 if float(actual) / required < threshold:
852 except ZeroDivisionError:
856 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
857 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
858 "to achieve the requested TX rate.".format(r=required, a=actual)
864 def get_per_direction_rate(self):
865 divisor = 2 if self.run_config['bidirectional'] else 1
866 if 'rate_percent' in self.current_total_rate:
867 # don't split rate if it's percentage
870 return utils.divide_rate(self.current_total_rate, divisor)
874 self.gen.stop_traffic()
877 self.gen.clear_stats()