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 time_elapsed = self.time_elapsed()
71 if time_elapsed > self.duration_sec:
74 time_left = self.duration_sec - time_elapsed
75 if self.interval_sec > 0.0:
76 if time_left <= self.interval_sec:
80 time.sleep(self.interval_sec)
82 time.sleep(self.duration_sec)
84 return self.client.get_stats()
87 class IpBlock(object):
88 def __init__(self, base_ip, step_ip, count_ip):
89 self.base_ip_int = Device.ip_to_int(base_ip)
90 self.step = Device.ip_to_int(step_ip)
91 self.max_available = count_ip
94 def get_ip(self, index=0):
95 '''Return the IP address at given index
97 if index < 0 or index >= self.max_available:
98 raise IndexError('Index out of bounds')
99 return Device.int_to_ip(self.base_ip_int + index * self.step)
101 def reserve_ip_range(self, count):
102 '''Reserve a range of count consecutive IP addresses spaced by step
104 if self.next_free + count > self.max_available:
105 raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d',
109 first_ip = self.get_ip(self.next_free)
110 last_ip = self.get_ip(self.next_free + count - 1)
111 self.next_free += count
112 return (first_ip, last_ip)
114 def reset_reservation(self):
118 class Device(object):
119 def __init__(self, port, pci, switch_port=None, vtep_vlan=None, ip=None, tg_gateway_ip=None,
120 gateway_ip=None, ip_addrs_step=None, tg_gateway_ip_addrs_step=None,
121 gateway_ip_addrs_step=None, udp_src_port=None, udp_dst_port=None,
122 chain_count=1, flow_count=1, vlan_tagging=False):
123 self.chain_count = chain_count
124 self.flow_count = flow_count
127 self.switch_port = switch_port
128 self.vtep_vlan = vtep_vlan
130 self.vlan_tagging = vlan_tagging
133 self.vm_mac_list = None
134 subnet = IPNetwork(ip)
135 self.ip = subnet.ip.format()
136 self.ip_prefixlen = subnet.prefixlen
137 self.ip_addrs_step = ip_addrs_step
138 self.tg_gateway_ip_addrs_step = tg_gateway_ip_addrs_step
139 self.gateway_ip_addrs_step = gateway_ip_addrs_step
140 self.gateway_ip = gateway_ip
141 self.tg_gateway_ip = tg_gateway_ip
142 self.ip_block = IpBlock(self.ip, ip_addrs_step, flow_count)
143 self.gw_ip_block = IpBlock(gateway_ip,
144 gateway_ip_addrs_step,
146 self.tg_gw_ip_block = IpBlock(tg_gateway_ip,
147 tg_gateway_ip_addrs_step,
149 self.udp_src_port = udp_src_port
150 self.udp_dst_port = udp_dst_port
152 def set_mac(self, mac):
154 raise TrafficClientException('Trying to set traffic generator MAC address as None')
157 def set_destination(self, dst):
160 def set_vm_mac_list(self, vm_mac_list):
161 self.vm_mac_list = map(str, vm_mac_list)
163 def set_vlan_tag(self, vlan_tag):
164 if self.vlan_tagging and vlan_tag is None:
165 raise TrafficClientException('Trying to set VLAN tag as None')
166 self.vlan_tag = vlan_tag
168 def get_gw_ip(self, chain_index):
169 '''Retrieve the IP address assigned for the gateway of a given chain
171 return self.gw_ip_block.get_ip(chain_index)
173 def get_stream_configs(self, service_chain):
175 # exact flow count for each chain is calculated as follows:
176 # - all chains except the first will have the same flow count
177 # calculated as (total_flows + chain_count - 1) / chain_count
178 # - the first chain will have the remainder
179 # example 11 flows and 3 chains => 3, 4, 4
180 flows_per_chain = (self.flow_count + self.chain_count - 1) / self.chain_count
181 cur_chain_flow_count = self.flow_count - flows_per_chain * (self.chain_count - 1)
183 self.ip_block.reset_reservation()
184 self.dst.ip_block.reset_reservation()
186 for chain_idx in xrange(self.chain_count):
187 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range(cur_chain_flow_count)
188 dst_ip_first, dst_ip_last = self.dst.ip_block.reserve_ip_range(cur_chain_flow_count)
190 'count': cur_chain_flow_count,
192 'mac_dst': self.dst.mac if service_chain == ChainType.EXT else self.vm_mac_list[
194 'ip_src_addr': src_ip_first,
195 'ip_src_addr_max': src_ip_last,
196 'ip_src_count': cur_chain_flow_count,
197 'ip_dst_addr': dst_ip_first,
198 'ip_dst_addr_max': dst_ip_last,
199 'ip_dst_count': cur_chain_flow_count,
200 'ip_addrs_step': self.ip_addrs_step,
201 'udp_src_port': self.udp_src_port,
202 'udp_dst_port': self.udp_dst_port,
203 'mac_discovery_gw': self.get_gw_ip(chain_idx),
204 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
205 'ip_dst_tg_gw': self.dst.tg_gw_ip_block.get_ip(chain_idx),
206 'vlan_tag': self.vlan_tag if self.vlan_tagging else None
208 # after first chain, fall back to the flow count for all other chains
209 cur_chain_flow_count = flows_per_chain
213 def ip_range_overlaps(self):
214 '''Check if this device ip range is overlapping with the dst device ip range
216 src_base_ip = Device.ip_to_int(self.ip)
217 dst_base_ip = Device.ip_to_int(self.dst.ip)
218 src_last_ip = src_base_ip + self.flow_count - 1
219 dst_last_ip = dst_base_ip + self.flow_count - 1
220 return dst_last_ip >= src_base_ip and src_last_ip >= dst_base_ip
224 return int(mac.translate(None, ":.- "), 16)
228 mac = format(i, 'x').zfill(12)
229 blocks = [mac[x:x + 2] for x in xrange(0, len(mac), 2)]
230 return ':'.join(blocks)
234 return struct.unpack("!I", socket.inet_aton(addr))[0]
237 def int_to_ip(nvalue):
238 return socket.inet_ntoa(struct.pack("!I", nvalue))
241 class RunningTrafficProfile(object):
242 """Represents traffic configuration for currently running traffic profile."""
244 DEFAULT_IP_STEP = '0.0.0.1'
245 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
247 def __init__(self, config, generator_profile):
248 generator_config = self.__match_generator_profile(config.traffic_generator,
250 self.generator_config = generator_config
251 self.service_chain = config.service_chain
252 self.service_chain_count = config.service_chain_count
253 self.flow_count = config.flow_count
254 self.host_name = generator_config.host_name
255 self.name = generator_config.name
256 self.tool = generator_config.tool
257 self.cores = generator_config.get('cores', 1)
258 self.ip_addrs_step = generator_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
259 self.tg_gateway_ip_addrs_step = \
260 generator_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
261 self.gateway_ip_addrs_step = generator_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
262 self.gateway_ips = generator_config.gateway_ip_addrs
263 self.ip = generator_config.ip
264 self.intf_speed = bitmath.parse_string(generator_config.intf_speed.replace('ps', '')).bits
265 self.vlan_tagging = config.vlan_tagging
266 self.no_arp = config.no_arp
267 self.src_device = None
268 self.dst_device = None
269 self.vm_mac_list = None
270 self.__prep_interfaces(generator_config)
273 return dict(self.generator_config)
275 def set_vm_mac_list(self, vm_mac_list):
276 self.src_device.set_vm_mac_list(vm_mac_list[0])
277 self.dst_device.set_vm_mac_list(vm_mac_list[1])
280 def __match_generator_profile(traffic_generator, generator_profile):
281 generator_config = AttrDict(traffic_generator)
282 generator_config.pop('default_profile')
283 generator_config.pop('generator_profile')
284 matching_profile = [profile for profile in traffic_generator.generator_profile if
285 profile.name == generator_profile]
286 if len(matching_profile) != 1:
287 raise Exception('Traffic generator profile not found: ' + generator_profile)
289 generator_config.update(matching_profile[0])
291 return generator_config
293 def __prep_interfaces(self, generator_config):
295 'chain_count': self.service_chain_count,
296 'flow_count': self.flow_count / 2,
297 'ip': generator_config.ip_addrs[0],
298 'ip_addrs_step': self.ip_addrs_step,
299 'gateway_ip': self.gateway_ips[0],
300 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
301 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[0],
302 'tg_gateway_ip_addrs_step': self.tg_gateway_ip_addrs_step,
303 'udp_src_port': generator_config.udp_src_port,
304 'udp_dst_port': generator_config.udp_dst_port,
305 'vlan_tagging': self.vlan_tagging
308 'chain_count': self.service_chain_count,
309 'flow_count': self.flow_count / 2,
310 'ip': generator_config.ip_addrs[1],
311 'ip_addrs_step': self.ip_addrs_step,
312 'gateway_ip': self.gateway_ips[1],
313 'gateway_ip_addrs_step': self.gateway_ip_addrs_step,
314 'tg_gateway_ip': generator_config.tg_gateway_ip_addrs[1],
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
321 self.src_device = Device(**dict(src_config, **generator_config.interfaces[0]))
322 self.dst_device = Device(**dict(dst_config, **generator_config.interfaces[1]))
323 self.src_device.set_destination(self.dst_device)
324 self.dst_device.set_destination(self.src_device)
326 if self.service_chain == ChainType.EXT and not self.no_arp \
327 and self.src_device.ip_range_overlaps():
328 raise Exception('Overlapping IP address ranges src=%s dst=%d flows=%d' %
335 return [self.src_device, self.dst_device]
339 return [self.src_device.vtep_vlan, self.dst_device.vtep_vlan]
343 return [self.src_device.port, self.dst_device.port]
346 def switch_ports(self):
347 return [self.src_device.switch_port, self.dst_device.switch_port]
351 return [self.src_device.pci, self.dst_device.pci]
354 class TrafficGeneratorFactory(object):
355 def __init__(self, config):
359 return self.config.generator_config.tool
361 def get_generator_client(self):
362 tool = self.get_tool().lower()
364 from traffic_gen import trex
365 return trex.TRex(self.config)
366 elif tool == 'dummy':
367 from traffic_gen import dummy
368 return dummy.DummyTG(self.config)
371 def list_generator_profile(self):
372 return [profile.name for profile in self.config.traffic_generator.generator_profile]
374 def get_generator_config(self, generator_profile):
375 return RunningTrafficProfile(self.config, generator_profile)
377 def get_matching_profile(self, traffic_profile_name):
378 matching_profile = [profile for profile in self.config.traffic_profile if
379 profile.name == traffic_profile_name]
381 if len(matching_profile) > 1:
382 raise Exception('Multiple traffic profiles with the same name found.')
383 elif not matching_profile:
384 raise Exception('No traffic profile found.')
386 return matching_profile[0]
388 def get_frame_sizes(self, traffic_profile):
389 matching_profile = self.get_matching_profile(traffic_profile)
390 return matching_profile.l2frame_size
393 class TrafficClient(object):
396 def __init__(self, config, notifier=None):
397 generator_factory = TrafficGeneratorFactory(config)
398 self.gen = generator_factory.get_generator_client()
399 self.tool = generator_factory.get_tool()
401 self.notifier = notifier
402 self.interval_collector = None
403 self.iteration_collector = None
404 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec)
406 raise TrafficClientException('%s is not a supported traffic generator' % self.tool)
409 'l2frame_size': None,
410 'duration_sec': self.config.duration_sec,
411 'bidirectional': True,
412 'rates': [] # to avoid unsbuscriptable-obj warning
414 self.current_total_rate = {'rate_percent': '10'}
415 if self.config.single_run:
416 self.current_total_rate = utils.parse_rate_str(self.config.rate)
419 for mac, device in zip(self.gen.get_macs(), self.config.generator_config.devices):
422 def start_traffic_generator(self):
428 self.gen.config_interface()
429 self.gen.clear_stats()
431 def get_version(self):
432 return self.gen.get_version()
434 def ensure_end_to_end(self):
436 Ensure traffic generator receives packets it has transmitted.
437 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
439 At this point all VMs are in active state, but forwarding does not have to work.
440 Small amount of traffic is sent to every chain. Then total of sent and received packets
441 is compared. If ratio between received and transmitted packets is higher than (N-1)/N,
442 N being number of chains, traffic flows through every chain and real measurements can be
446 PVP chain (1 VM per chain)
447 N = 10 (number of chains)
448 threshold = (N-1)/N = 9/10 = 0.9 (acceptable ratio ensuring working conditions)
449 if total_received/total_sent > 0.9, traffic is flowing to more than 9 VMs meaning
450 all 10 VMs are in operational state.
452 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
453 rate_pps = {'rate_pps': str(self.config.service_chain_count * 100)}
454 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False)
456 # ensures enough traffic is coming back
457 threshold = (self.config.service_chain_count - 1) / float(self.config.service_chain_count)
458 retry_count = (self.config.check_traffic_time_sec +
459 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec
460 for it in xrange(retry_count):
461 self.gen.clear_stats()
462 self.gen.start_traffic()
463 LOG.info('Waiting for packets to be received back... (%d / %d)', it + 1, retry_count)
464 time.sleep(self.config.generic_poll_sec)
465 self.gen.stop_traffic()
466 stats = self.gen.get_stats()
468 # compute total sent and received traffic on both ports
471 for port in self.PORTS:
472 total_rx += float(stats[port]['rx'].get('total_pkts', 0))
473 total_tx += float(stats[port]['tx'].get('total_pkts', 0))
475 # how much of traffic came back
476 ratio = total_rx / total_tx if total_tx else 0
478 if ratio > threshold:
479 self.gen.clear_stats()
480 self.gen.clear_streamblock()
481 LOG.info('End-to-end connectivity ensured')
484 time.sleep(self.config.generic_poll_sec)
486 raise TrafficClientException('End-to-end connectivity cannot be ensured')
488 def ensure_arp_successful(self):
489 if not self.gen.resolve_arp():
490 raise TrafficClientException('ARP cannot be resolved')
492 def set_traffic(self, frame_size, bidirectional):
493 self.run_config['bidirectional'] = bidirectional
494 self.run_config['l2frame_size'] = frame_size
495 self.run_config['rates'] = [self.get_per_direction_rate()]
497 self.run_config['rates'].append(self.get_per_direction_rate())
499 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
500 if unidir_reverse_pps > 0:
501 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
503 self.gen.clear_streamblock()
504 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional, latency=True)
506 def modify_load(self, load):
507 self.current_total_rate = {'rate_percent': str(load)}
508 rate_per_direction = self.get_per_direction_rate()
510 self.gen.modify_rate(rate_per_direction, False)
511 self.run_config['rates'][0] = rate_per_direction
512 if self.run_config['bidirectional']:
513 self.gen.modify_rate(rate_per_direction, True)
514 self.run_config['rates'][1] = rate_per_direction
516 def get_ndr_and_pdr(self):
517 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
519 if self.config.ndr_run:
520 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
521 targets['ndr'] = self.config.measurement.NDR
522 if self.config.pdr_run:
523 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
524 targets['pdr'] = self.config.measurement.PDR
526 self.run_config['start_time'] = time.time()
527 self.interval_collector = IntervalCollector(self.run_config['start_time'])
528 self.interval_collector.attach_notifier(self.notifier)
529 self.iteration_collector = IterationCollector(self.run_config['start_time'])
531 self.__range_search(0.0, 200.0, targets, results)
533 results['iteration_stats'] = {
534 'ndr_pdr': self.iteration_collector.get()
537 if self.config.ndr_run:
538 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
539 results['ndr']['time_taken_sec'] = \
540 results['ndr']['timestamp_sec'] - self.run_config['start_time']
541 if self.config.pdr_run:
542 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
543 results['pdr']['time_taken_sec'] = \
544 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
546 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
547 results['pdr']['time_taken_sec'] = \
548 results['pdr']['timestamp_sec'] - self.run_config['start_time']
551 def __get_dropped_rate(self, result):
552 dropped_pkts = result['rx']['dropped_pkts']
553 total_pkts = result['tx']['total_pkts']
556 return float(dropped_pkts) / total_pkts * 100
559 stats = self.gen.get_stats()
560 retDict = {'total_tx_rate': stats['total_tx_rate']}
561 for port in self.PORTS:
562 retDict[port] = {'tx': {}, 'rx': {}}
564 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
565 rx_keys = tx_keys + ['dropped_pkts']
567 for port in self.PORTS:
569 retDict[port]['tx'][key] = int(stats[port]['tx'][key])
572 retDict[port]['rx'][key] = int(stats[port]['rx'][key])
574 retDict[port]['rx'][key] = 0
575 retDict[port]['rx']['avg_delay_usec'] = cast_integer(
576 stats[port]['rx']['avg_delay_usec'])
577 retDict[port]['rx']['min_delay_usec'] = cast_integer(
578 stats[port]['rx']['min_delay_usec'])
579 retDict[port]['rx']['max_delay_usec'] = cast_integer(
580 stats[port]['rx']['max_delay_usec'])
581 retDict[port]['drop_rate_percent'] = self.__get_dropped_rate(retDict[port])
583 ports = sorted(retDict.keys())
584 if self.run_config['bidirectional']:
585 retDict['overall'] = {'tx': {}, 'rx': {}}
587 retDict['overall']['tx'][key] = \
588 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
590 retDict['overall']['rx'][key] = \
591 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
592 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
593 retDict[ports[1]]['rx']['total_pkts']]
594 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
595 retDict[ports[1]]['rx']['avg_delay_usec']]
596 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
597 retDict[ports[1]]['rx']['max_delay_usec']]
598 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
599 retDict[ports[1]]['rx']['min_delay_usec']]
600 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
601 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
602 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
603 for key in ['pkt_bit_rate', 'pkt_rate']:
604 for dirc in ['tx', 'rx']:
605 retDict['overall'][dirc][key] /= 2.0
607 retDict['overall'] = retDict[ports[0]]
608 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
611 def __convert_rates(self, rate):
612 return utils.convert_rates(self.run_config['l2frame_size'],
614 self.config.generator_config.intf_speed)
616 def __ndr_pdr_found(self, tag, load):
617 rates = self.__convert_rates({'rate_percent': load})
618 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
619 last_stats = self.iteration_collector.peek()
620 self.interval_collector.add_ndr_pdr(tag, last_stats)
622 def __format_output_stats(self, stats):
623 for key in self.PORTS + ['overall']:
624 interface = stats[key]
626 'tx_pkts': interface['tx']['total_pkts'],
627 'rx_pkts': interface['rx']['total_pkts'],
628 'drop_percentage': interface['drop_rate_percent'],
629 'drop_pct': interface['rx']['dropped_pkts'],
630 'avg_delay_usec': interface['rx']['avg_delay_usec'],
631 'max_delay_usec': interface['rx']['max_delay_usec'],
632 'min_delay_usec': interface['rx']['min_delay_usec'],
637 def __targets_found(self, rate, targets, results):
638 for tag, target in targets.iteritems():
639 LOG.info('Found %s (%s) load: %s', tag, target, rate)
640 self.__ndr_pdr_found(tag, rate)
641 results[tag]['timestamp_sec'] = time.time()
643 def __range_search(self, left, right, targets, results):
644 '''Perform a binary search for a list of targets inside a [left..right] range or rate
646 left the left side of the range to search as a % the line rate (100 = 100% line rate)
647 indicating the rate to send on each interface
648 right the right side of the range to search as a % of line rate
649 indicating the rate to send on each interface
650 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
652 results a dict to store results
656 LOG.info('Range search [%s .. %s] targets: %s', left, right, targets)
658 # Terminate search when gap is less than load epsilon
659 if right - left < self.config.measurement.load_epsilon:
660 self.__targets_found(left, targets, results)
663 # Obtain the average drop rate in for middle load
664 middle = (left + right) / 2.0
666 stats, rates = self.__run_search_iteration(middle)
668 LOG.exception("Got exception from traffic generator during binary search")
669 self.__targets_found(left, targets, results)
671 # Split target dicts based on the avg drop rate
674 for tag, target in targets.iteritems():
675 if stats['overall']['drop_rate_percent'] <= target:
676 # record the best possible rate found for this target
678 results[tag].update({
679 'load_percent_per_direction': middle,
680 'stats': self.__format_output_stats(dict(stats)),
681 'timestamp_sec': None
683 right_targets[tag] = target
685 # initialize to 0 all fields of result for
686 # the worst case scenario of the binary search (if ndr/pdr is not found)
687 if tag not in results:
688 results[tag] = dict.fromkeys(rates, 0)
689 empty_stats = self.__format_output_stats(dict(stats))
690 for key in empty_stats:
691 if isinstance(empty_stats[key], dict):
692 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
695 results[tag].update({
696 'load_percent_per_direction': 0,
697 'stats': empty_stats,
698 'timestamp_sec': None
700 left_targets[tag] = target
703 self.__range_search(left, middle, left_targets, results)
705 # search upper half only if the upper rate does not exceed
706 # 100%, this only happens when the first search at 100%
707 # yields a DR that is < target DR
709 self.__targets_found(100, right_targets, results)
711 self.__range_search(middle, right, right_targets, results)
713 def __run_search_iteration(self, rate):
715 self.modify_load(rate)
717 # poll interval stats and collect them
718 for stats in self.run_traffic():
719 self.interval_collector.add(stats)
720 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
721 if time_elapsed_ratio >= 1:
722 self.cancel_traffic()
723 self.interval_collector.reset()
725 # get stats from the run
726 stats = self.runner.client.get_stats()
727 current_traffic_config = self.get_traffic_config()
728 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
729 stats['total_tx_rate'])
730 if warning is not None:
731 stats['warning'] = warning
733 # save reliable stats from whole iteration
734 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
735 LOG.info('Average drop rate: %f', stats['overall']['drop_rate_percent'])
737 return stats, current_traffic_config['direction-total']
740 def log_stats(stats):
742 'datetime': str(datetime.now()),
743 'tx_packets': stats['overall']['tx']['total_pkts'],
744 'rx_packets': stats['overall']['rx']['total_pkts'],
745 'drop_packets': stats['overall']['rx']['dropped_pkts'],
746 'drop_rate_percent': stats['overall']['drop_rate_percent']
748 LOG.info('TX: %(tx_packets)d; '
749 'RX: %(rx_packets)d; '
750 'Dropped: %(drop_packets)d; '
751 'Drop rate: %(drop_rate_percent).4f%%',
754 def run_traffic(self):
755 stats = self.runner.run()
756 while self.runner.is_running:
757 self.log_stats(stats)
759 stats = self.runner.poll_stats()
762 self.log_stats(stats)
763 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
766 def cancel_traffic(self):
769 def get_interface(self, port_index):
770 port = self.gen.port_handle[port_index]
772 if not self.config.no_traffic:
773 stats = self.get_stats()
775 tx, rx = int(stats[port]['tx']['total_pkts']), int(stats[port]['rx']['total_pkts'])
776 return Interface('traffic-generator', self.tool.lower(), tx, rx)
778 def get_traffic_config(self):
783 for idx, rate in enumerate(self.run_config['rates']):
784 key = 'direction-forward' if idx == 0 else 'direction-reverse'
786 'l2frame_size': self.run_config['l2frame_size'],
787 'duration_sec': self.run_config['duration_sec']
789 config[key].update(rate)
790 config[key].update(self.__convert_rates(rate))
791 load_total += float(config[key]['rate_percent'])
792 bps_total += float(config[key]['rate_bps'])
793 pps_total += float(config[key]['rate_pps'])
794 config['direction-total'] = dict(config['direction-forward'])
795 config['direction-total'].update({
796 'rate_percent': load_total,
797 'rate_pps': cast_integer(pps_total),
798 'rate_bps': bps_total
803 def get_run_config(self, results):
804 """Returns configuration which was used for the last run."""
806 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
807 tx_rate = results["stats"][idx]["tx"]["total_pkts"] / self.config.duration_sec
808 rx_rate = results["stats"][idx]["rx"]["total_pkts"] / self.config.duration_sec
810 "orig": self.__convert_rates(self.run_config['rates'][idx]),
811 "tx": self.__convert_rates({'rate_pps': tx_rate}),
812 "rx": self.__convert_rates({'rate_pps': rx_rate})
816 for direction in ['orig', 'tx', 'rx']:
817 total[direction] = {}
818 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
820 total[direction][unit] = sum([float(x[direction][unit]) for x in r.values()])
822 r['direction-total'] = total
826 def compare_tx_rates(required, actual):
828 are_different = False
830 if float(actual) / required < threshold:
832 except ZeroDivisionError:
836 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
837 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
838 "to achieve the requested TX rate.".format(r=required, a=actual)
844 def get_per_direction_rate(self):
845 divisor = 2 if self.run_config['bidirectional'] else 1
846 if 'rate_percent' in self.current_total_rate:
847 # don't split rate if it's percentage
850 return utils.divide_rate(self.current_total_rate, divisor)
854 self.gen.stop_traffic()
857 self.gen.clear_stats()