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 """Interface to the traffic generator clients including NDR/PDR binary search."""
22 from attrdict import AttrDict
24 from netaddr import IPNetwork
25 # pylint: disable=import-error
26 from trex.stl.api import Ether
27 from trex.stl.api import STLError
28 from trex.stl.api import UDP
29 # pylint: disable=wrong-import-order
30 from scapy.contrib.mpls import MPLS # flake8: noqa
31 # pylint: enable=wrong-import-order
32 # pylint: enable=import-error
35 from .packet_stats import InterfaceStats
36 from .packet_stats import PacketPathStats
37 from .stats_collector import IntervalCollector
38 from .stats_collector import IterationCollector
39 from .traffic_gen import traffic_utils as utils
40 from .utils import cast_integer
42 class TrafficClientException(Exception):
43 """Generic traffic client exception."""
45 class TrafficRunner(object):
46 """Serialize various steps required to run traffic."""
48 def __init__(self, client, duration_sec, interval_sec=0, service_mode=False):
49 """Create a traffic runner."""
51 self.start_time = None
52 self.duration_sec = duration_sec
53 self.interval_sec = interval_sec
54 self.service_mode = service_mode
57 """Clear stats and instruct the traffic generator to start generating traffic."""
60 LOG.info('Running traffic generator')
61 self.client.gen.clear_stats()
62 # Debug use only : new '--service-mode' option available for the NFVBench command line.
63 # A read-only mode TRex console would be able to capture the generated traffic.
64 self.client.gen.set_service_mode(enabled=self.service_mode)
65 LOG.info('Service mode is %sabled', 'en' if self.service_mode else 'dis')
66 self.client.gen.start_traffic()
67 self.start_time = time.time()
68 return self.poll_stats()
71 """Stop the current run and instruct the traffic generator to stop traffic."""
73 self.start_time = None
74 self.client.gen.stop_traffic()
77 """Check if a run is still pending."""
78 return self.start_time is not None
80 def time_elapsed(self):
81 """Return time elapsed since start of run."""
83 return time.time() - self.start_time
84 return self.duration_sec
87 """Poll latest stats from the traffic generator at fixed interval - sleeps if necessary.
89 return: latest stats or None if traffic is stopped
91 if not self.is_running():
93 if self.client.skip_sleep():
95 return self.client.get_stats()
96 time_elapsed = self.time_elapsed()
97 if time_elapsed > self.duration_sec:
100 time_left = self.duration_sec - time_elapsed
101 if self.interval_sec > 0.0:
102 if time_left <= self.interval_sec:
103 time.sleep(time_left)
106 time.sleep(self.interval_sec)
108 time.sleep(self.duration_sec)
110 return self.client.get_stats()
113 class IpBlock(object):
114 """Manage a block of IP addresses."""
116 def __init__(self, base_ip, step_ip, count_ip):
117 """Create an IP block."""
118 self.base_ip_int = Device.ip_to_int(base_ip)
119 self.step = Device.ip_to_int(step_ip)
120 self.max_available = count_ip
123 def get_ip(self, index=0):
124 """Return the IP address at given index."""
125 if index < 0 or index >= self.max_available:
126 raise IndexError('Index out of bounds: %d (max=%d)' % (index, self.max_available))
127 return Device.int_to_ip(self.base_ip_int + index * self.step)
129 def reserve_ip_range(self, count, force_ip_reservation=False):
130 """Reserve a range of count consecutive IP addresses spaced by step.
131 force_ip_reservation parameter allows to continue the calculation of IPs when
132 the 2 sides (ports) have different size and the flow is greater than
135 if self.next_free + count > self.max_available and force_ip_reservation is False:
136 raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' %
140 if self.next_free + count > self.max_available and force_ip_reservation is True:
141 first_ip = self.get_ip(self.next_free)
142 last_ip = self.get_ip(self.next_free + self.max_available - 1)
143 self.next_free += self.max_available
145 first_ip = self.get_ip(self.next_free)
146 last_ip = self.get_ip(self.next_free + count - 1)
147 self.next_free += count
148 return (first_ip, last_ip)
150 def reset_reservation(self):
151 """Reset all reservations and restart with a completely unused IP block."""
155 class UdpPorts(object):
157 def __init__(self, src_min, src_max, dst_min, dst_max, step):
159 self.src_min = src_min
160 self.src_max = src_max
161 self.dst_min = dst_min
162 self.dst_max = dst_max
166 class Device(object):
167 """Represent a port device and all information associated to it.
169 In the curent version we only support 2 port devices for the traffic generator
170 identified as port 0 or port 1.
173 def __init__(self, port, generator_config):
174 """Create a new device for a given port."""
175 self.generator_config = generator_config
176 self.chain_count = generator_config.service_chain_count
177 if generator_config.bidirectional:
178 self.flow_count = generator_config.flow_count / 2
180 self.flow_count = generator_config.flow_count
183 self.switch_port = generator_config.interfaces[port].get('switch_port', None)
184 self.vtep_vlan = None
185 self.vtep_src_mac = None
188 self.inner_labels = None
189 self.outer_labels = None
190 self.pci = generator_config.interfaces[port].pci
192 self.dest_macs = None
193 self.vtep_dst_mac = None
194 self.vtep_dst_ip = None
195 if generator_config.vteps is None:
196 self.vtep_src_ip = None
198 self.vtep_src_ip = generator_config.vteps[port]
201 self.ip_addrs = generator_config.ip_addrs[port]
202 self.ip_src_static = generator_config.ip_src_static
203 self.ip_addrs_step = generator_config.ip_addrs_step
204 if self.ip_addrs_step == 'random':
205 # Set step to 1 to calculate the IP range size (see check_ip_size below)
208 step = self.ip_addrs_step
209 self.ip_size = self.check_ipsize(IPNetwork(self.ip_addrs).size, Device.ip_to_int(step))
210 self.ip = str(IPNetwork(self.ip_addrs).network)
211 ip_addrs_left = generator_config.ip_addrs[0]
212 ip_addrs_right = generator_config.ip_addrs[1]
213 self.ip_addrs_size = {
214 'left': self.check_ipsize(IPNetwork(ip_addrs_left).size, Device.ip_to_int(step)),
215 'right': self.check_ipsize(IPNetwork(ip_addrs_right).size, Device.ip_to_int(step))}
216 udp_src_port = generator_config.gen_config.udp_src_port
217 if udp_src_port is None:
219 udp_dst_port = generator_config.gen_config.udp_dst_port
220 if udp_dst_port is None:
222 src_max, src_min = self.define_udp_range(udp_src_port, 'udp_src_port')
223 dst_max, dst_min = self.define_udp_range(udp_dst_port, 'udp_dst_port')
224 udp_src_range = int(src_max) - int(src_min) + 1
225 udp_dst_range = int(dst_max) - int(dst_min) + 1
226 lcm_port = self.lcm(udp_src_range, udp_dst_range)
227 if self.ip_src_static is True:
228 lcm_ip = self.lcm(1, min(self.ip_addrs_size['left'], self.ip_addrs_size['right']))
230 lcm_ip = self.lcm(self.ip_addrs_size['left'], self.ip_addrs_size['right'])
231 flow_max = self.lcm(lcm_port, lcm_ip)
232 if self.flow_count > flow_max:
233 raise TrafficClientException('Trying to set unachievable traffic (%d > %d)' %
234 (self.flow_count, flow_max))
236 # manage udp range regarding flow count value
237 # UDP dst range is greater than FC => range will be limited to min + FC
238 if self.flow_count <= udp_dst_range:
239 dst_max = int(dst_min) + self.flow_count - 1
240 # UDP src range is greater than FC => range will be limited to min + FC
241 if self.flow_count <= udp_src_range:
242 src_max = int(src_min) + self.flow_count - 1
243 # Define IP block limit regarding flow count
244 if self.flow_count <= self.ip_size:
245 self.ip_block = IpBlock(self.ip, step, self.flow_count)
247 self.ip_block = IpBlock(self.ip, step, self.ip_size)
249 if generator_config.gen_config.udp_port_step == 'random':
252 step = generator_config.gen_config.udp_port_step
253 self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max, step)
254 self.gw_ip_block = IpBlock(generator_config.gateway_ips[port],
255 generator_config.gateway_ip_addrs_step,
257 self.tg_gateway_ip_addrs = generator_config.tg_gateway_ip_addrs[port]
258 self.tg_gw_ip_block = IpBlock(self.tg_gateway_ip_addrs,
259 generator_config.tg_gateway_ip_addrs_step,
263 def define_udp_range(udp_port, property_name):
264 if isinstance(udp_port, int):
267 elif isinstance(udp_port, tuple):
271 raise TrafficClientException('Invalid %s property value (53 or [\'53\',\'1024\'])'
277 """Calculate the maximum possible value for both IP and ports,
278 eventually for maximum possible flux."""
279 if a != 0 and b != 0:
280 lcm_value = a * b // gcd(a, b)
282 raise TypeError(" IP size or port range can't be zero !")
285 def check_ipsize(ip_size, step):
286 """Check and set the available IPs, considering the step."""
288 if ip_size % step == 0:
289 value = int(ip_size / step)
291 value = int((ip_size / step)) + 1
293 except ZeroDivisionError:
294 raise ZeroDivisionError("step can't be zero !")
296 def set_mac(self, mac):
297 """Set the local MAC for this port device."""
299 raise TrafficClientException('Trying to set traffic generator MAC address as None')
302 def get_peer_device(self):
303 """Get the peer device (device 0 -> device 1, or device 1 -> device 0)."""
304 return self.generator_config.devices[1 - self.port]
306 def set_vtep_dst_mac(self, dest_macs):
307 """Set the list of dest MACs indexed by the chain id.
309 This is only called in 2 cases:
310 - VM macs discovered using openstack API
311 - dest MACs provisioned in config file
313 self.vtep_dst_mac = list(map(str, dest_macs))
315 def set_dest_macs(self, dest_macs):
316 """Set the list of dest MACs indexed by the chain id.
318 This is only called in 2 cases:
319 - VM macs discovered using openstack API
320 - dest MACs provisioned in config file
322 self.dest_macs = list(map(str, dest_macs))
324 def get_dest_macs(self):
325 """Get the list of dest macs for this device.
327 If set_dest_macs was never called, assumes l2-loopback and return
328 a list of peer mac (as many as chains but normally only 1 chain)
331 return self.dest_macs
332 # assume this is l2-loopback
333 return [self.get_peer_device().mac] * self.chain_count
335 def set_vlans(self, vlans):
336 """Set the list of vlans to use indexed by the chain id."""
338 LOG.info("Port %d: VLANs %s", self.port, self.vlans)
340 def set_vtep_vlan(self, vlan):
341 """Set the vtep vlan to use indexed by specific port."""
342 self.vtep_vlan = vlan
344 self.vlan_tagging = None
345 LOG.info("Port %d: VTEP VLANs %s", self.port, self.vtep_vlan)
347 def set_vxlan_endpoints(self, src_ip, dst_ip):
348 self.vtep_dst_ip = dst_ip
349 self.vtep_src_ip = src_ip
350 LOG.info("Port %d: src_vtep %s, dst_vtep %s", self.port,
351 self.vtep_src_ip, self.vtep_dst_ip)
353 def set_mpls_peers(self, src_ip, dst_ip):
355 self.vtep_dst_ip = dst_ip
356 self.vtep_src_ip = src_ip
357 LOG.info("Port %d: src_mpls_vtep %s, mpls_peer_ip %s", self.port,
358 self.vtep_src_ip, self.vtep_dst_ip)
360 def set_vxlans(self, vnis):
362 LOG.info("Port %d: VNIs %s", self.port, self.vnis)
364 def set_mpls_inner_labels(self, labels):
365 self.inner_labels = labels
366 LOG.info("Port %d: MPLS Inner Labels %s", self.port, self.inner_labels)
368 def set_mpls_outer_labels(self, labels):
369 self.outer_labels = labels
370 LOG.info("Port %d: MPLS Outer Labels %s", self.port, self.outer_labels)
372 def set_gw_ip(self, gateway_ip):
373 self.gw_ip_block = IpBlock(gateway_ip,
374 self.generator_config.gateway_ip_addrs_step,
377 def get_gw_ip(self, chain_index):
378 """Retrieve the IP address assigned for the gateway of a given chain."""
379 return self.gw_ip_block.get_ip(chain_index)
381 def get_stream_configs(self):
382 """Get the stream config for a given chain on this device.
384 Called by the traffic generator driver to program the traffic generator properly
385 before generating traffic
388 # exact flow count for each chain is calculated as follows:
389 # - all chains except the first will have the same flow count
390 # calculated as (total_flows + chain_count - 1) / chain_count
391 # - the first chain will have the remainder
392 # example 11 flows and 3 chains => 3, 4, 4
393 flows_per_chain = int((self.flow_count + self.chain_count - 1) / self.chain_count)
394 cur_chain_flow_count = int(self.flow_count - flows_per_chain * (self.chain_count - 1))
395 force_ip_reservation = False
396 # use case example of this parameter:
397 # - static IP addresses (source & destination), netmask = /30
398 # - 4 varying UDP source ports | 1 UDP destination port
400 # --> parameter 'reserve_ip_range' should have flag 'force_ip_reservation'
401 # in order to assign the maximum available IP on each iteration
402 if self.ip_size < cur_chain_flow_count \
403 or self.ip_addrs_size['left'] != self.ip_addrs_size['right']:
404 force_ip_reservation = True
406 peer = self.get_peer_device()
407 self.ip_block.reset_reservation()
408 peer.ip_block.reset_reservation()
409 dest_macs = self.get_dest_macs()
411 for chain_idx in range(self.chain_count):
412 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range\
413 (cur_chain_flow_count, force_ip_reservation)
414 dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range\
415 (cur_chain_flow_count, force_ip_reservation)
418 'count': cur_chain_flow_count,
420 'mac_dst': dest_macs[chain_idx],
421 'ip_src_addr': src_ip_first,
422 'ip_src_addr_max': src_ip_last,
423 'ip_src_count': cur_chain_flow_count,
424 'ip_dst_addr': dst_ip_first,
425 'ip_dst_addr_max': dst_ip_last,
426 'ip_dst_count': cur_chain_flow_count,
427 'ip_addrs_step': self.ip_addrs_step,
428 'ip_src_static': self.ip_src_static,
429 'udp_src_port': self.udp_ports.src_min,
430 'udp_src_port_max': self.udp_ports.src_max,
431 'udp_src_count': int(self.udp_ports.src_max) - int(self.udp_ports.src_min) + 1,
432 'udp_dst_port': self.udp_ports.dst_min,
433 'udp_dst_port_max': self.udp_ports.dst_max,
434 'udp_dst_count': int(self.udp_ports.dst_max) - int(self.udp_ports.dst_min) + 1,
435 'udp_port_step': self.udp_ports.step,
436 'mac_discovery_gw': self.get_gw_ip(chain_idx),
437 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
438 'ip_dst_tg_gw': peer.tg_gw_ip_block.get_ip(chain_idx),
439 'vlan_tag': self.vlans[chain_idx] if self.vlans else None,
441 'vtep_vlan': self.vtep_vlan if self.vtep_vlan else None,
442 'vtep_src_mac': self.mac if (self.vxlan or self.mpls) else None,
443 'vtep_dst_mac': self.vtep_dst_mac if (self.vxlan or self.mpls) else None,
444 'vtep_dst_ip': self.vtep_dst_ip if self.vxlan is True else None,
445 'vtep_src_ip': self.vtep_src_ip if self.vxlan is True else None,
446 'net_vni': self.vnis[chain_idx] if self.vxlan is True else None,
448 'mpls_outer_label': self.outer_labels[chain_idx] if self.mpls is True else None,
449 'mpls_inner_label': self.inner_labels[chain_idx] if self.mpls is True else None
452 # after first chain, fall back to the flow count for all other chains
453 cur_chain_flow_count = flows_per_chain
458 """Convert an IP address from string to numeric."""
459 return struct.unpack("!I", socket.inet_aton(addr))[0]
462 def int_to_ip(nvalue):
463 """Convert an IP address from numeric to string."""
464 return socket.inet_ntoa(struct.pack("!I", int(nvalue)))
467 class GeneratorConfig(object):
468 """Represents traffic configuration for currently running traffic profile."""
470 DEFAULT_IP_STEP = '0.0.0.1'
471 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
473 def __init__(self, config):
474 """Create a generator config."""
476 # name of the generator profile (normally trex or dummy)
477 # pick the default one if not specified explicitly from cli options
478 if not config.generator_profile:
479 config.generator_profile = config.traffic_generator.default_profile
480 # pick up the profile dict based on the name
481 gen_config = self.__match_generator_profile(config.traffic_generator,
482 config.generator_profile)
483 self.gen_config = gen_config
484 # copy over fields from the dict
485 self.tool = gen_config.tool
486 self.ip = gen_config.ip
487 # overrides on config.cores and config.mbuf_factor
489 self.cores = config.cores
491 self.cores = gen_config.get('cores', 1)
492 self.mbuf_factor = config.mbuf_factor
493 self.mbuf_64 = config.mbuf_64
494 self.hdrh = not config.disable_hdrh
495 if gen_config.intf_speed:
496 # interface speed is overriden from config
497 self.intf_speed = bitmath.parse_string(gen_config.intf_speed.replace('ps', '')).bits
499 # interface speed is discovered/provided by the traffic generator
501 self.name = gen_config.name
502 self.zmq_pub_port = gen_config.get('zmq_pub_port', 4500)
503 self.zmq_rpc_port = gen_config.get('zmq_rpc_port', 4501)
504 self.limit_memory = gen_config.get('limit_memory', 1024)
505 self.software_mode = gen_config.get('software_mode', False)
506 self.interfaces = gen_config.interfaces
507 if self.interfaces[0].port != 0 or self.interfaces[1].port != 1:
508 raise TrafficClientException('Invalid port order/id in generator_profile.interfaces')
509 self.service_chain = config.service_chain
510 self.service_chain_count = config.service_chain_count
511 self.flow_count = config.flow_count
512 self.host_name = gen_config.host_name
513 self.bidirectional = config.traffic.bidirectional
514 self.tg_gateway_ip_addrs = gen_config.tg_gateway_ip_addrs
515 self.ip_addrs = gen_config.ip_addrs
516 self.ip_addrs_step = gen_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
517 self.tg_gateway_ip_addrs_step = \
518 gen_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
519 self.gateway_ip_addrs_step = gen_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
520 self.gateway_ips = gen_config.gateway_ip_addrs
521 self.ip_src_static = gen_config.ip_src_static
522 self.vteps = gen_config.get('vteps')
523 self.devices = [Device(port, self) for port in [0, 1]]
524 # This should normally always be [0, 1]
525 self.ports = [device.port for device in self.devices]
527 # check that pci is not empty
528 if not gen_config.interfaces[0].get('pci', None) or \
529 not gen_config.interfaces[1].get('pci', None):
530 raise TrafficClientException("configuration interfaces pci fields cannot be empty")
532 self.pcis = [tgif['pci'] for tgif in gen_config.interfaces]
533 self.vlan_tagging = config.vlan_tagging
535 # needed for result/summarizer
536 config['tg-name'] = gen_config.name
537 config['tg-tool'] = self.tool
540 """Get json form to display the content into the overall result dict."""
541 return dict(self.gen_config)
543 def set_dest_macs(self, port_index, dest_macs):
544 """Set the list of dest MACs indexed by the chain id on given port.
546 port_index: the port for which dest macs must be set
547 dest_macs: a list of dest MACs indexed by chain id
549 if len(dest_macs) < self.config.service_chain_count:
550 raise TrafficClientException('Dest MAC list %s must have %d entries' %
551 (dest_macs, self.config.service_chain_count))
552 # only pass the first scc dest MACs
553 self.devices[port_index].set_dest_macs(dest_macs[:self.config.service_chain_count])
554 LOG.info('Port %d: dst MAC %s', port_index, [str(mac) for mac in dest_macs])
556 def set_vtep_dest_macs(self, port_index, dest_macs):
557 """Set the list of dest MACs indexed by the chain id on given port.
559 port_index: the port for which dest macs must be set
560 dest_macs: a list of dest MACs indexed by chain id
562 if len(dest_macs) != self.config.service_chain_count:
563 raise TrafficClientException('Dest MAC list %s must have %d entries' %
564 (dest_macs, self.config.service_chain_count))
565 self.devices[port_index].set_vtep_dst_mac(dest_macs)
566 LOG.info('Port %d: vtep dst MAC %s', port_index, {str(mac) for mac in dest_macs})
568 def get_dest_macs(self):
569 """Return the list of dest macs indexed by port."""
570 return [dev.get_dest_macs() for dev in self.devices]
572 def set_vlans(self, port_index, vlans):
573 """Set the list of vlans to use indexed by the chain id on given port.
575 port_index: the port for which VLANs must be set
576 vlans: a list of vlan lists indexed by chain id
578 if len(vlans) != self.config.service_chain_count:
579 raise TrafficClientException('VLAN list %s must have %d entries' %
580 (vlans, self.config.service_chain_count))
581 self.devices[port_index].set_vlans(vlans)
583 def set_vxlans(self, port_index, vxlans):
584 """Set the list of vxlans (VNIs) to use indexed by the chain id on given port.
586 port_index: the port for which VXLANs must be set
587 VXLANs: a list of VNIs lists indexed by chain id
589 if len(vxlans) != self.config.service_chain_count:
590 raise TrafficClientException('VXLAN list %s must have %d entries' %
591 (vxlans, self.config.service_chain_count))
592 self.devices[port_index].set_vxlans(vxlans)
594 def set_mpls_inner_labels(self, port_index, labels):
595 """Set the list of MPLS Labels to use indexed by the chain id on given port.
597 port_index: the port for which Labels must be set
598 Labels: a list of Labels lists indexed by chain id
600 if len(labels) != self.config.service_chain_count:
601 raise TrafficClientException('Inner MPLS list %s must have %d entries' %
602 (labels, self.config.service_chain_count))
603 self.devices[port_index].set_mpls_inner_labels(labels)
605 def set_mpls_outer_labels(self, port_index, labels):
606 """Set the list of MPLS Labels to use indexed by the chain id on given port.
608 port_index: the port for which Labels must be set
609 Labels: a list of Labels lists indexed by chain id
611 if len(labels) != self.config.service_chain_count:
612 raise TrafficClientException('Outer MPLS list %s must have %d entries' %
613 (labels, self.config.service_chain_count))
614 self.devices[port_index].set_mpls_outer_labels(labels)
616 def set_vtep_vlan(self, port_index, vlan):
617 """Set the vtep vlan to use indexed by the chain id on given port.
618 port_index: the port for which VLAN must be set
620 self.devices[port_index].set_vtep_vlan(vlan)
622 def set_vxlan_endpoints(self, port_index, src_ip, dst_ip):
623 self.devices[port_index].set_vxlan_endpoints(src_ip, dst_ip)
625 def set_mpls_peers(self, port_index, src_ip, dst_ip):
626 self.devices[port_index].set_mpls_peers(src_ip, dst_ip)
629 def __match_generator_profile(traffic_generator, generator_profile):
630 gen_config = AttrDict(traffic_generator)
631 gen_config.pop('default_profile')
632 gen_config.pop('generator_profile')
633 matching_profile = [profile for profile in traffic_generator.generator_profile if
634 profile.name == generator_profile]
635 if len(matching_profile) != 1:
636 raise Exception('Traffic generator profile not found: ' + generator_profile)
638 gen_config.update(matching_profile[0])
642 class TrafficClient(object):
643 """Traffic generator client with NDR/PDR binary seearch."""
647 def __init__(self, config, notifier=None):
648 """Create a new TrafficClient instance.
650 config: nfvbench config
651 notifier: notifier (optional)
653 A new instance is created everytime the nfvbench config may have changed.
656 self.generator_config = GeneratorConfig(config)
657 self.tool = self.generator_config.tool
658 self.gen = self._get_generator()
659 self.notifier = notifier
660 self.interval_collector = None
661 self.iteration_collector = None
662 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec,
663 self.config.service_mode)
664 self.config.frame_sizes = self._get_frame_sizes()
666 'l2frame_size': None,
667 'duration_sec': self.config.duration_sec,
668 'bidirectional': True,
669 'rates': [] # to avoid unsbuscriptable-obj warning
671 self.current_total_rate = {'rate_percent': '10'}
672 if self.config.single_run:
673 self.current_total_rate = utils.parse_rate_str(self.config.rate)
675 # Speed is either discovered when connecting to TG or set from config
676 # This variable is 0 if not yet discovered from TG or must be the speed of
677 # each interface in bits per second
678 self.intf_speed = self.generator_config.intf_speed
680 def _get_generator(self):
681 tool = self.tool.lower()
683 from .traffic_gen import trex_gen
684 return trex_gen.TRex(self)
686 from .traffic_gen import dummy
687 return dummy.DummyTG(self)
688 raise TrafficClientException('Unsupported generator tool name:' + self.tool)
690 def skip_sleep(self):
691 """Skip all sleeps when doing unit testing with dummy TG.
693 Must be overriden using mock.patch
697 def _get_frame_sizes(self):
698 traffic_profile_name = self.config.traffic.profile
699 matching_profiles = [profile for profile in self.config.traffic_profile if
700 profile.name == traffic_profile_name]
701 if len(matching_profiles) > 1:
702 raise TrafficClientException('Multiple traffic profiles with name: ' +
703 traffic_profile_name)
704 if not matching_profiles:
705 raise TrafficClientException('Cannot find traffic profile: ' + traffic_profile_name)
706 return matching_profiles[0].l2frame_size
708 def start_traffic_generator(self):
709 """Start the traffic generator process (traffic not started yet)."""
711 # pick up the interface speed if it is not set from config
712 intf_speeds = self.gen.get_port_speed_gbps()
713 # convert Gbps unit into bps
714 tg_if_speed = bitmath.parse_string(str(intf_speeds[0]) + 'Gb').bits
716 # interface speed is overriden from config
717 if self.intf_speed != tg_if_speed:
718 # Warn the user if the speed in the config is different
719 LOG.warning('Interface speed provided is different from actual speed (%d Gbps)',
722 # interface speed not provisioned by config
723 self.intf_speed = tg_if_speed
724 # also update the speed in the tg config
725 self.generator_config.intf_speed = tg_if_speed
727 # Save the traffic generator local MAC
728 for mac, device in zip(self.gen.get_macs(), self.generator_config.devices):
732 """Set up the traffic client."""
733 self.gen.clear_stats()
735 def get_version(self):
736 """Get the traffic generator version."""
737 return self.gen.get_version()
739 def ensure_end_to_end(self):
740 """Ensure traffic generator receives packets it has transmitted.
742 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
744 VMs that are started and in active state may not pass traffic yet. It is imperative to make
745 sure that all VMs are passing traffic in both directions before starting any benchmarking.
746 To verify this, we need to send at a low frequency bi-directional packets and make sure
747 that we receive all packets back from all VMs. The number of flows is equal to 2 times
748 the number of chains (1 per direction) and we need to make sure we receive packets coming
749 from exactly 2 x chain count different source MAC addresses.
752 PVP chain (1 VM per chain)
753 N = 10 (number of chains)
754 Flow count = 20 (number of flows)
755 If the number of unique source MAC addresses from received packets is 20 then
756 all 10 VMs 10 VMs are in operational state.
758 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
759 # send 2pps on each chain and each direction
760 rate_pps = {'rate_pps': str(self.config.service_chain_count * 2)}
761 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False,
763 # ensures enough traffic is coming back
764 retry_count = int((self.config.check_traffic_time_sec +
765 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec)
767 # we expect to see packets coming from 2 unique MAC per chain
768 # because there can be flooding in the case of shared net
769 # we must verify that packets from the right VMs are received
770 # and not just count unique src MAC
771 # create a dict of (port, chain) tuples indexed by dest mac
773 for port, dest_macs in enumerate(self.generator_config.get_dest_macs()):
774 for chain, mac in enumerate(dest_macs):
775 mac_map[mac] = (port, chain)
776 unique_src_mac_count = len(mac_map)
777 if self.config.vxlan and self.config.traffic_generator.vtep_vlan:
778 get_mac_id = lambda packet: packet['binary'][60:66]
779 elif self.config.vxlan:
780 get_mac_id = lambda packet: packet['binary'][56:62]
781 elif self.config.mpls:
782 get_mac_id = lambda packet: packet['binary'][24:30]
783 # mpls_transport_label = lambda packet: packet['binary'][14:18]
785 get_mac_id = lambda packet: packet['binary'][6:12]
786 for it in range(retry_count):
787 self.gen.clear_stats()
788 self.gen.start_traffic()
789 self.gen.start_capture()
790 LOG.info('Captured unique src mac %d/%d, capturing return packets (retry %d/%d)...',
791 unique_src_mac_count - len(mac_map), unique_src_mac_count,
793 if not self.skip_sleep():
794 time.sleep(self.config.generic_poll_sec)
795 self.gen.stop_traffic()
796 self.gen.fetch_capture_packets()
797 self.gen.stop_capture()
798 for packet in self.gen.packet_list:
799 mac_id = get_mac_id(packet).decode('latin-1')
800 src_mac = ':'.join(["%02x" % ord(x) for x in mac_id])
802 if src_mac in mac_map and self.is_mpls(packet):
803 port, chain = mac_map[src_mac]
804 LOG.info('Received mpls packet from mac: %s (chain=%d, port=%d)',
805 src_mac, chain, port)
806 mac_map.pop(src_mac, None)
808 if src_mac in mac_map and self.is_udp(packet):
809 port, chain = mac_map[src_mac]
810 LOG.info('Received udp packet from mac: %s (chain=%d, port=%d)',
811 src_mac, chain, port)
812 mac_map.pop(src_mac, None)
815 LOG.info('End-to-end connectivity established')
817 if self.config.l3_router and not self.config.no_arp:
818 # In case of L3 traffic mode, routers are not able to route traffic
819 # until VM interfaces are up and ARP requests are done
820 LOG.info('Waiting for loopback service completely started...')
821 LOG.info('Sending ARP request to assure end-to-end connectivity established')
822 self.ensure_arp_successful()
823 raise TrafficClientException('End-to-end connectivity cannot be ensured')
825 def is_udp(self, packet):
826 pkt = Ether(packet['binary'])
829 def is_mpls(self, packet):
830 pkt = Ether(packet['binary'])
833 def ensure_arp_successful(self):
834 """Resolve all IP using ARP and throw an exception in case of failure."""
835 dest_macs = self.gen.resolve_arp()
837 # all dest macs are discovered, saved them into the generator config
838 if self.config.vxlan or self.config.mpls:
839 self.generator_config.set_vtep_dest_macs(0, dest_macs[0])
840 self.generator_config.set_vtep_dest_macs(1, dest_macs[1])
842 self.generator_config.set_dest_macs(0, dest_macs[0])
843 self.generator_config.set_dest_macs(1, dest_macs[1])
845 raise TrafficClientException('ARP cannot be resolved')
847 def set_traffic(self, frame_size, bidirectional):
848 """Reconfigure the traffic generator for a new frame size."""
849 self.run_config['bidirectional'] = bidirectional
850 self.run_config['l2frame_size'] = frame_size
851 self.run_config['rates'] = [self.get_per_direction_rate()]
853 self.run_config['rates'].append(self.get_per_direction_rate())
855 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
856 if unidir_reverse_pps > 0:
857 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
858 # Fix for [NFVBENCH-67], convert the rate string to PPS
859 for idx, rate in enumerate(self.run_config['rates']):
860 if 'rate_pps' not in rate:
861 self.run_config['rates'][idx] = {'rate_pps': self.__convert_rates(rate)['rate_pps']}
863 self.gen.clear_streamblock()
865 if self.config.no_latency_streams:
866 LOG.info("Latency streams are disabled")
867 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional,
868 latency=not self.config.no_latency_streams)
870 def _modify_load(self, load):
871 self.current_total_rate = {'rate_percent': str(load)}
872 rate_per_direction = self.get_per_direction_rate()
874 self.gen.modify_rate(rate_per_direction, False)
875 self.run_config['rates'][0] = rate_per_direction
876 if self.run_config['bidirectional']:
877 self.gen.modify_rate(rate_per_direction, True)
878 self.run_config['rates'][1] = rate_per_direction
880 def get_ndr_and_pdr(self):
881 """Start the NDR/PDR iteration and return the results."""
882 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
884 if self.config.ndr_run:
885 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
886 targets['ndr'] = self.config.measurement.NDR
887 if self.config.pdr_run:
888 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
889 targets['pdr'] = self.config.measurement.PDR
891 self.run_config['start_time'] = time.time()
892 self.interval_collector = IntervalCollector(self.run_config['start_time'])
893 self.interval_collector.attach_notifier(self.notifier)
894 self.iteration_collector = IterationCollector(self.run_config['start_time'])
896 self.__range_search(0.0, 200.0, targets, results)
898 results['iteration_stats'] = {
899 'ndr_pdr': self.iteration_collector.get()
902 if self.config.ndr_run:
903 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
904 results['ndr']['time_taken_sec'] = \
905 results['ndr']['timestamp_sec'] - self.run_config['start_time']
906 if self.config.pdr_run:
907 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
908 results['pdr']['time_taken_sec'] = \
909 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
911 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
912 results['pdr']['time_taken_sec'] = \
913 results['pdr']['timestamp_sec'] - self.run_config['start_time']
916 def __get_dropped_rate(self, result):
917 dropped_pkts = result['rx']['dropped_pkts']
918 total_pkts = result['tx']['total_pkts']
921 return float(dropped_pkts) / total_pkts * 100
924 """Collect final stats for previous run."""
925 stats = self.gen.get_stats()
926 retDict = {'total_tx_rate': stats['total_tx_rate'],
927 'offered_tx_rate_bps': stats['offered_tx_rate_bps']}
929 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
930 rx_keys = tx_keys + ['dropped_pkts']
932 for port in self.PORTS:
933 port_stats = {'tx': {}, 'rx': {}}
935 port_stats['tx'][key] = int(stats[port]['tx'][key])
938 port_stats['rx'][key] = int(stats[port]['rx'][key])
940 port_stats['rx'][key] = 0
941 port_stats['rx']['avg_delay_usec'] = cast_integer(
942 stats[port]['rx']['avg_delay_usec'])
943 port_stats['rx']['min_delay_usec'] = cast_integer(
944 stats[port]['rx']['min_delay_usec'])
945 port_stats['rx']['max_delay_usec'] = cast_integer(
946 stats[port]['rx']['max_delay_usec'])
947 port_stats['drop_rate_percent'] = self.__get_dropped_rate(port_stats)
948 retDict[str(port)] = port_stats
950 ports = sorted(list(retDict.keys()), key=str)
951 if self.run_config['bidirectional']:
952 retDict['overall'] = {'tx': {}, 'rx': {}}
954 retDict['overall']['tx'][key] = \
955 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
957 retDict['overall']['rx'][key] = \
958 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
959 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
960 retDict[ports[1]]['rx']['total_pkts']]
961 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
962 retDict[ports[1]]['rx']['avg_delay_usec']]
963 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
964 retDict[ports[1]]['rx']['max_delay_usec']]
965 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
966 retDict[ports[1]]['rx']['min_delay_usec']]
967 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
968 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
969 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
970 for key in ['pkt_bit_rate', 'pkt_rate']:
971 for dirc in ['tx', 'rx']:
972 retDict['overall'][dirc][key] /= 2.0
974 retDict['overall'] = retDict[ports[0]]
975 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
978 def __convert_rates(self, rate):
979 return utils.convert_rates(self.run_config['l2frame_size'],
983 def __ndr_pdr_found(self, tag, load):
984 rates = self.__convert_rates({'rate_percent': load})
985 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
986 last_stats = self.iteration_collector.peek()
987 self.interval_collector.add_ndr_pdr(tag, last_stats)
989 def __format_output_stats(self, stats):
990 for key in self.PORTS + ['overall']:
992 interface = stats[key]
994 'tx_pkts': interface['tx']['total_pkts'],
995 'rx_pkts': interface['rx']['total_pkts'],
996 'drop_percentage': interface['drop_rate_percent'],
997 'drop_pct': interface['rx']['dropped_pkts'],
998 'avg_delay_usec': interface['rx']['avg_delay_usec'],
999 'max_delay_usec': interface['rx']['max_delay_usec'],
1000 'min_delay_usec': interface['rx']['min_delay_usec'],
1005 def __targets_found(self, rate, targets, results):
1006 for tag, target in list(targets.items()):
1007 LOG.info('Found %s (%s) load: %s', tag, target, rate)
1008 self.__ndr_pdr_found(tag, rate)
1009 results[tag]['timestamp_sec'] = time.time()
1011 def __range_search(self, left, right, targets, results):
1012 """Perform a binary search for a list of targets inside a [left..right] range or rate.
1014 left the left side of the range to search as a % the line rate (100 = 100% line rate)
1015 indicating the rate to send on each interface
1016 right the right side of the range to search as a % of line rate
1017 indicating the rate to send on each interface
1018 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
1020 results a dict to store results
1024 LOG.info('Range search [%s .. %s] targets: %s', left, right, targets)
1026 # Terminate search when gap is less than load epsilon
1027 if right - left < self.config.measurement.load_epsilon:
1028 self.__targets_found(left, targets, results)
1031 # Obtain the average drop rate in for middle load
1032 middle = (left + right) / 2.0
1034 stats, rates = self.__run_search_iteration(middle)
1036 LOG.exception("Got exception from traffic generator during binary search")
1037 self.__targets_found(left, targets, results)
1039 # Split target dicts based on the avg drop rate
1042 for tag, target in list(targets.items()):
1043 if stats['overall']['drop_rate_percent'] <= target:
1044 # record the best possible rate found for this target
1045 results[tag] = rates
1046 results[tag].update({
1047 'load_percent_per_direction': middle,
1048 'stats': self.__format_output_stats(dict(stats)),
1049 'timestamp_sec': None
1051 right_targets[tag] = target
1053 # initialize to 0 all fields of result for
1054 # the worst case scenario of the binary search (if ndr/pdr is not found)
1055 if tag not in results:
1056 results[tag] = dict.fromkeys(rates, 0)
1057 empty_stats = self.__format_output_stats(dict(stats))
1058 for key in empty_stats:
1059 if isinstance(empty_stats[key], dict):
1060 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
1062 empty_stats[key] = 0
1063 results[tag].update({
1064 'load_percent_per_direction': 0,
1065 'stats': empty_stats,
1066 'timestamp_sec': None
1068 left_targets[tag] = target
1071 self.__range_search(left, middle, left_targets, results)
1073 # search upper half only if the upper rate does not exceed
1074 # 100%, this only happens when the first search at 100%
1075 # yields a DR that is < target DR
1077 self.__targets_found(100, right_targets, results)
1079 self.__range_search(middle, right, right_targets, results)
1081 def __run_search_iteration(self, rate):
1082 """Run one iteration at the given rate level.
1084 rate: the rate to send on each port in percent (0 to 100)
1086 self._modify_load(rate)
1088 # poll interval stats and collect them
1089 for stats in self.run_traffic():
1090 self.interval_collector.add(stats)
1091 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
1092 if time_elapsed_ratio >= 1:
1093 self.cancel_traffic()
1094 if not self.skip_sleep():
1095 time.sleep(self.config.pause_sec)
1096 self.interval_collector.reset()
1098 # get stats from the run
1099 stats = self.runner.client.get_stats()
1100 current_traffic_config = self._get_traffic_config()
1101 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
1102 stats['total_tx_rate'])
1103 if warning is not None:
1104 stats['warning'] = warning
1106 # save reliable stats from whole iteration
1107 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
1108 LOG.info('Average drop rate: %f', stats['overall']['drop_rate_percent'])
1109 return stats, current_traffic_config['direction-total']
1111 def log_stats(self, stats):
1112 """Log estimated stats during run."""
1113 # Calculate a rolling drop rate based on differential to
1114 # the previous reading
1115 cur_tx = stats['overall']['tx']['total_pkts']
1116 cur_rx = stats['overall']['rx']['total_pkts']
1117 delta_tx = cur_tx - self.prev_tx
1118 delta_rx = cur_rx - self.prev_rx
1119 drops = delta_tx - delta_rx
1120 drop_rate_pct = 100 * (delta_tx - delta_rx)/delta_tx
1121 self.prev_tx = cur_tx
1122 self.prev_rx = cur_rx
1123 LOG.info('TX: %15s; RX: %15s; (Est.) Dropped: %12s; Drop rate: %8.4f%%',
1124 format(cur_tx, ',d'),
1125 format(cur_rx, ',d'),
1126 format(drops, ',d'),
1129 def run_traffic(self):
1130 """Start traffic and return intermediate stats for each interval."""
1131 stats = self.runner.run()
1134 while self.runner.is_running:
1135 self.log_stats(stats)
1137 stats = self.runner.poll_stats()
1140 self.log_stats(stats)
1141 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
1144 def cancel_traffic(self):
1148 def _get_traffic_config(self):
1153 for idx, rate in enumerate(self.run_config['rates']):
1154 key = 'direction-forward' if idx == 0 else 'direction-reverse'
1156 'l2frame_size': self.run_config['l2frame_size'],
1157 'duration_sec': self.run_config['duration_sec']
1159 config[key].update(rate)
1160 config[key].update(self.__convert_rates(rate))
1161 load_total += float(config[key]['rate_percent'])
1162 bps_total += float(config[key]['rate_bps'])
1163 pps_total += float(config[key]['rate_pps'])
1164 config['direction-total'] = dict(config['direction-forward'])
1165 config['direction-total'].update({
1166 'rate_percent': load_total,
1167 'rate_pps': cast_integer(pps_total),
1168 'rate_bps': bps_total
1173 def get_run_config(self, results):
1174 """Return configuration which was used for the last run."""
1176 # because we want each direction to have the far end RX rates,
1177 # use the far end index (1-idx) to retrieve the RX rates
1178 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
1179 tx_rate = results["stats"][str(idx)]["tx"]["total_pkts"] / self.config.duration_sec
1180 rx_rate = results["stats"][str(1 - idx)]["rx"]["total_pkts"] / self.config.duration_sec
1182 "orig": self.__convert_rates(self.run_config['rates'][idx]),
1183 "tx": self.__convert_rates({'rate_pps': tx_rate}),
1184 "rx": self.__convert_rates({'rate_pps': rx_rate})
1188 for direction in ['orig', 'tx', 'rx']:
1189 total[direction] = {}
1190 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
1191 total[direction][unit] = sum([float(x[direction][unit]) for x in list(r.values())])
1193 r['direction-total'] = total
1196 def insert_interface_stats(self, pps_list):
1197 """Insert interface stats to a list of packet path stats.
1199 pps_list: a list of packet path stats instances indexed by chain index
1201 This function will insert the packet path stats for the traffic gen ports 0 and 1
1202 with itemized per chain tx/rx counters.
1203 There will be as many packet path stats as chains.
1204 Each packet path stats will have exactly 2 InterfaceStats for port 0 and port 1
1207 PacketPathStats(InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)),
1208 PacketPathStats(InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)),
1212 def get_if_stats(chain_idx):
1213 return [InterfaceStats('p' + str(port), self.tool)
1214 for port in range(2)]
1215 # keep the list of list of interface stats indexed by the chain id
1216 self.ifstats = [get_if_stats(chain_idx)
1217 for chain_idx in range(self.config.service_chain_count)]
1218 # note that we need to make a copy of the ifs list so that any modification in the
1219 # list from pps will not change the list saved in self.ifstats
1220 self.pps_list = [PacketPathStats(list(ifs)) for ifs in self.ifstats]
1221 # insert the corresponding pps in the passed list
1222 pps_list.extend(self.pps_list)
1224 def update_interface_stats(self, diff=False):
1225 """Update all interface stats.
1227 diff: if False, simply refresh the interface stats values with latest values
1228 if True, diff the interface stats with the latest values
1229 Make sure that the interface stats inserted in insert_interface_stats() are updated
1233 [InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)],
1234 [InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)],
1239 stats = self.gen.get_stats()
1240 for chain_idx, ifs in enumerate(self.ifstats):
1241 # each ifs has exactly 2 InterfaceStats and 2 Latency instances
1242 # corresponding to the
1243 # port 0 and port 1 for the given chain_idx
1244 # Note that we cannot use self.pps_list[chain_idx].if_stats to pick the
1245 # interface stats for the pps because it could have been modified to contain
1246 # additional interface stats
1247 self.gen.get_stream_stats(stats, ifs, self.pps_list[chain_idx].latencies, chain_idx)
1248 # special handling for vxlan
1249 # in case of vxlan, flow stats are not available so all rx counters will be
1250 # zeros when the total rx port counter is non zero.
1252 for port in range(2):
1254 for ifs in self.ifstats:
1255 total_rx += ifs[port].rx
1257 # check if the total port rx from Trex is also zero
1258 port_rx = stats[port]['rx']['total_pkts']
1260 # the total rx for all chains from port level stats is non zero
1261 # which means that the per-chain stats are not available
1262 if len(self.ifstats) == 1:
1263 # only one chain, simply report the port level rx to the chain rx stats
1264 self.ifstats[0][port].rx = port_rx
1266 for ifs in self.ifstats:
1267 # mark this data as unavailable
1269 # pitch in the total rx only in the last chain pps
1270 self.ifstats[-1][port].rx_total = port_rx
1273 def compare_tx_rates(required, actual):
1274 """Compare the actual TX rate to the required TX rate."""
1276 are_different = False
1278 if float(actual) / required < threshold:
1279 are_different = True
1280 except ZeroDivisionError:
1281 are_different = True
1284 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
1285 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
1286 "to achieve the requested TX rate.".format(r=required, a=actual)
1292 def get_per_direction_rate(self):
1293 """Get the rate for each direction."""
1294 divisor = 2 if self.run_config['bidirectional'] else 1
1295 if 'rate_percent' in self.current_total_rate:
1296 # don't split rate if it's percentage
1299 return utils.divide_rate(self.current_total_rate, divisor)
1302 """Close this instance."""
1304 self.gen.stop_traffic()
1307 self.gen.clear_stats()