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 self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max,
250 generator_config.gen_config.udp_port_step)
251 self.gw_ip_block = IpBlock(generator_config.gateway_ips[port],
252 generator_config.gateway_ip_addrs_step,
254 self.tg_gateway_ip_addrs = generator_config.tg_gateway_ip_addrs[port]
255 self.tg_gw_ip_block = IpBlock(self.tg_gateway_ip_addrs,
256 generator_config.tg_gateway_ip_addrs_step,
260 def define_udp_range(udp_port, property_name):
261 if isinstance(udp_port, int):
264 elif isinstance(udp_port, tuple):
268 raise TrafficClientException('Invalid %s property value (53 or [\'53\',\'1024\'])'
274 """Calculate the maximum possible value for both IP and ports,
275 eventually for maximum possible flux."""
276 if a != 0 and b != 0:
277 lcm_value = a * b // gcd(a, b)
279 raise TypeError(" IP size or port range can't be zero !")
282 def check_ipsize(ip_size, step):
283 """Check and set the available IPs, considering the step."""
285 if ip_size % step == 0:
286 value = int(ip_size / step)
288 value = int((ip_size / step)) + 1
290 except ZeroDivisionError:
291 raise ZeroDivisionError("step can't be zero !")
293 def set_mac(self, mac):
294 """Set the local MAC for this port device."""
296 raise TrafficClientException('Trying to set traffic generator MAC address as None')
299 def get_peer_device(self):
300 """Get the peer device (device 0 -> device 1, or device 1 -> device 0)."""
301 return self.generator_config.devices[1 - self.port]
303 def set_vtep_dst_mac(self, dest_macs):
304 """Set the list of dest MACs indexed by the chain id.
306 This is only called in 2 cases:
307 - VM macs discovered using openstack API
308 - dest MACs provisioned in config file
310 self.vtep_dst_mac = list(map(str, dest_macs))
312 def set_dest_macs(self, dest_macs):
313 """Set the list of dest MACs indexed by the chain id.
315 This is only called in 2 cases:
316 - VM macs discovered using openstack API
317 - dest MACs provisioned in config file
319 self.dest_macs = list(map(str, dest_macs))
321 def get_dest_macs(self):
322 """Get the list of dest macs for this device.
324 If set_dest_macs was never called, assumes l2-loopback and return
325 a list of peer mac (as many as chains but normally only 1 chain)
328 return self.dest_macs
329 # assume this is l2-loopback
330 return [self.get_peer_device().mac] * self.chain_count
332 def set_vlans(self, vlans):
333 """Set the list of vlans to use indexed by the chain id."""
335 LOG.info("Port %d: VLANs %s", self.port, self.vlans)
337 def set_vtep_vlan(self, vlan):
338 """Set the vtep vlan to use indexed by specific port."""
339 self.vtep_vlan = vlan
341 self.vlan_tagging = None
342 LOG.info("Port %d: VTEP VLANs %s", self.port, self.vtep_vlan)
344 def set_vxlan_endpoints(self, src_ip, dst_ip):
345 self.vtep_dst_ip = dst_ip
346 self.vtep_src_ip = src_ip
347 LOG.info("Port %d: src_vtep %s, dst_vtep %s", self.port,
348 self.vtep_src_ip, self.vtep_dst_ip)
350 def set_mpls_peers(self, src_ip, dst_ip):
352 self.vtep_dst_ip = dst_ip
353 self.vtep_src_ip = src_ip
354 LOG.info("Port %d: src_mpls_vtep %s, mpls_peer_ip %s", self.port,
355 self.vtep_src_ip, self.vtep_dst_ip)
357 def set_vxlans(self, vnis):
359 LOG.info("Port %d: VNIs %s", self.port, self.vnis)
361 def set_mpls_inner_labels(self, labels):
362 self.inner_labels = labels
363 LOG.info("Port %d: MPLS Inner Labels %s", self.port, self.inner_labels)
365 def set_mpls_outer_labels(self, labels):
366 self.outer_labels = labels
367 LOG.info("Port %d: MPLS Outer Labels %s", self.port, self.outer_labels)
369 def set_gw_ip(self, gateway_ip):
370 self.gw_ip_block = IpBlock(gateway_ip,
371 self.generator_config.gateway_ip_addrs_step,
374 def get_gw_ip(self, chain_index):
375 """Retrieve the IP address assigned for the gateway of a given chain."""
376 return self.gw_ip_block.get_ip(chain_index)
378 def get_stream_configs(self):
379 """Get the stream config for a given chain on this device.
381 Called by the traffic generator driver to program the traffic generator properly
382 before generating traffic
385 # exact flow count for each chain is calculated as follows:
386 # - all chains except the first will have the same flow count
387 # calculated as (total_flows + chain_count - 1) / chain_count
388 # - the first chain will have the remainder
389 # example 11 flows and 3 chains => 3, 4, 4
390 flows_per_chain = int((self.flow_count + self.chain_count - 1) / self.chain_count)
391 cur_chain_flow_count = int(self.flow_count - flows_per_chain * (self.chain_count - 1))
392 force_ip_reservation = False
393 # use case example of this parameter:
394 # - static IP addresses (source & destination), netmask = /30
395 # - 4 varying UDP source ports | 1 UDP destination port
397 # --> parameter 'reserve_ip_range' should have flag 'force_ip_reservation'
398 # in order to assign the maximum available IP on each iteration
399 if self.ip_size < cur_chain_flow_count \
400 or self.ip_addrs_size['left'] != self.ip_addrs_size['right']:
401 force_ip_reservation = True
403 peer = self.get_peer_device()
404 self.ip_block.reset_reservation()
405 peer.ip_block.reset_reservation()
406 dest_macs = self.get_dest_macs()
408 for chain_idx in range(self.chain_count):
409 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range\
410 (cur_chain_flow_count, force_ip_reservation)
411 dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range\
412 (cur_chain_flow_count, force_ip_reservation)
415 'count': cur_chain_flow_count,
417 'mac_dst': dest_macs[chain_idx],
418 'ip_src_addr': src_ip_first,
419 'ip_src_addr_max': src_ip_last,
420 'ip_src_count': cur_chain_flow_count,
421 'ip_dst_addr': dst_ip_first,
422 'ip_dst_addr_max': dst_ip_last,
423 'ip_dst_count': cur_chain_flow_count,
424 'ip_addrs_step': self.ip_addrs_step,
425 'ip_src_static': self.ip_src_static,
426 'udp_src_port': self.udp_ports.src_min,
427 'udp_src_port_max': self.udp_ports.src_max,
428 'udp_src_count': int(self.udp_ports.src_max) - int(self.udp_ports.src_min) + 1,
429 'udp_dst_port': self.udp_ports.dst_min,
430 'udp_dst_port_max': self.udp_ports.dst_max,
431 'udp_dst_count': int(self.udp_ports.dst_max) - int(self.udp_ports.dst_min) + 1,
432 'udp_port_step': self.udp_ports.step,
433 'mac_discovery_gw': self.get_gw_ip(chain_idx),
434 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
435 'ip_dst_tg_gw': peer.tg_gw_ip_block.get_ip(chain_idx),
436 'vlan_tag': self.vlans[chain_idx] if self.vlans else None,
438 'vtep_vlan': self.vtep_vlan if self.vtep_vlan else None,
439 'vtep_src_mac': self.mac if (self.vxlan or self.mpls) else None,
440 'vtep_dst_mac': self.vtep_dst_mac if (self.vxlan or self.mpls) else None,
441 'vtep_dst_ip': self.vtep_dst_ip if self.vxlan is True else None,
442 'vtep_src_ip': self.vtep_src_ip if self.vxlan is True else None,
443 'net_vni': self.vnis[chain_idx] if self.vxlan is True else None,
445 'mpls_outer_label': self.outer_labels[chain_idx] if self.mpls is True else None,
446 'mpls_inner_label': self.inner_labels[chain_idx] if self.mpls is True else None
449 # after first chain, fall back to the flow count for all other chains
450 cur_chain_flow_count = flows_per_chain
455 """Convert an IP address from string to numeric."""
456 return struct.unpack("!I", socket.inet_aton(addr))[0]
459 def int_to_ip(nvalue):
460 """Convert an IP address from numeric to string."""
461 return socket.inet_ntoa(struct.pack("!I", int(nvalue)))
464 class GeneratorConfig(object):
465 """Represents traffic configuration for currently running traffic profile."""
467 DEFAULT_IP_STEP = '0.0.0.1'
468 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
470 def __init__(self, config):
471 """Create a generator config."""
473 # name of the generator profile (normally trex or dummy)
474 # pick the default one if not specified explicitly from cli options
475 if not config.generator_profile:
476 config.generator_profile = config.traffic_generator.default_profile
477 # pick up the profile dict based on the name
478 gen_config = self.__match_generator_profile(config.traffic_generator,
479 config.generator_profile)
480 self.gen_config = gen_config
481 # copy over fields from the dict
482 self.tool = gen_config.tool
483 self.ip = gen_config.ip
484 # overrides on config.cores and config.mbuf_factor
486 self.cores = config.cores
488 self.cores = gen_config.get('cores', 1)
489 self.mbuf_factor = config.mbuf_factor
490 self.mbuf_64 = config.mbuf_64
491 self.hdrh = not config.disable_hdrh
492 if gen_config.intf_speed:
493 # interface speed is overriden from config
494 self.intf_speed = bitmath.parse_string(gen_config.intf_speed.replace('ps', '')).bits
496 # interface speed is discovered/provided by the traffic generator
498 self.name = gen_config.name
499 self.zmq_pub_port = gen_config.get('zmq_pub_port', 4500)
500 self.zmq_rpc_port = gen_config.get('zmq_rpc_port', 4501)
501 self.limit_memory = gen_config.get('limit_memory', 1024)
502 self.software_mode = gen_config.get('software_mode', False)
503 self.interfaces = gen_config.interfaces
504 if self.interfaces[0].port != 0 or self.interfaces[1].port != 1:
505 raise TrafficClientException('Invalid port order/id in generator_profile.interfaces')
506 self.service_chain = config.service_chain
507 self.service_chain_count = config.service_chain_count
508 self.flow_count = config.flow_count
509 self.host_name = gen_config.host_name
510 self.bidirectional = config.traffic.bidirectional
511 self.tg_gateway_ip_addrs = gen_config.tg_gateway_ip_addrs
512 self.ip_addrs = gen_config.ip_addrs
513 self.ip_addrs_step = gen_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
514 self.tg_gateway_ip_addrs_step = \
515 gen_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
516 self.gateway_ip_addrs_step = gen_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
517 self.gateway_ips = gen_config.gateway_ip_addrs
518 self.ip_src_static = gen_config.ip_src_static
519 self.vteps = gen_config.get('vteps')
520 self.devices = [Device(port, self) for port in [0, 1]]
521 # This should normally always be [0, 1]
522 self.ports = [device.port for device in self.devices]
524 # check that pci is not empty
525 if not gen_config.interfaces[0].get('pci', None) or \
526 not gen_config.interfaces[1].get('pci', None):
527 raise TrafficClientException("configuration interfaces pci fields cannot be empty")
529 self.pcis = [tgif['pci'] for tgif in gen_config.interfaces]
530 self.vlan_tagging = config.vlan_tagging
532 # needed for result/summarizer
533 config['tg-name'] = gen_config.name
534 config['tg-tool'] = self.tool
537 """Get json form to display the content into the overall result dict."""
538 return dict(self.gen_config)
540 def set_dest_macs(self, port_index, dest_macs):
541 """Set the list of dest MACs indexed by the chain id on given port.
543 port_index: the port for which dest macs must be set
544 dest_macs: a list of dest MACs indexed by chain id
546 if len(dest_macs) < self.config.service_chain_count:
547 raise TrafficClientException('Dest MAC list %s must have %d entries' %
548 (dest_macs, self.config.service_chain_count))
549 # only pass the first scc dest MACs
550 self.devices[port_index].set_dest_macs(dest_macs[:self.config.service_chain_count])
551 LOG.info('Port %d: dst MAC %s', port_index, [str(mac) for mac in dest_macs])
553 def set_vtep_dest_macs(self, port_index, dest_macs):
554 """Set the list of dest MACs indexed by the chain id on given port.
556 port_index: the port for which dest macs must be set
557 dest_macs: a list of dest MACs indexed by chain id
559 if len(dest_macs) != self.config.service_chain_count:
560 raise TrafficClientException('Dest MAC list %s must have %d entries' %
561 (dest_macs, self.config.service_chain_count))
562 self.devices[port_index].set_vtep_dst_mac(dest_macs)
563 LOG.info('Port %d: vtep dst MAC %s', port_index, {str(mac) for mac in dest_macs})
565 def get_dest_macs(self):
566 """Return the list of dest macs indexed by port."""
567 return [dev.get_dest_macs() for dev in self.devices]
569 def set_vlans(self, port_index, vlans):
570 """Set the list of vlans to use indexed by the chain id on given port.
572 port_index: the port for which VLANs must be set
573 vlans: a list of vlan lists indexed by chain id
575 if len(vlans) != self.config.service_chain_count:
576 raise TrafficClientException('VLAN list %s must have %d entries' %
577 (vlans, self.config.service_chain_count))
578 self.devices[port_index].set_vlans(vlans)
580 def set_vxlans(self, port_index, vxlans):
581 """Set the list of vxlans (VNIs) to use indexed by the chain id on given port.
583 port_index: the port for which VXLANs must be set
584 VXLANs: a list of VNIs lists indexed by chain id
586 if len(vxlans) != self.config.service_chain_count:
587 raise TrafficClientException('VXLAN list %s must have %d entries' %
588 (vxlans, self.config.service_chain_count))
589 self.devices[port_index].set_vxlans(vxlans)
591 def set_mpls_inner_labels(self, port_index, labels):
592 """Set the list of MPLS Labels to use indexed by the chain id on given port.
594 port_index: the port for which Labels must be set
595 Labels: a list of Labels lists indexed by chain id
597 if len(labels) != self.config.service_chain_count:
598 raise TrafficClientException('Inner MPLS list %s must have %d entries' %
599 (labels, self.config.service_chain_count))
600 self.devices[port_index].set_mpls_inner_labels(labels)
602 def set_mpls_outer_labels(self, port_index, labels):
603 """Set the list of MPLS Labels to use indexed by the chain id on given port.
605 port_index: the port for which Labels must be set
606 Labels: a list of Labels lists indexed by chain id
608 if len(labels) != self.config.service_chain_count:
609 raise TrafficClientException('Outer MPLS list %s must have %d entries' %
610 (labels, self.config.service_chain_count))
611 self.devices[port_index].set_mpls_outer_labels(labels)
613 def set_vtep_vlan(self, port_index, vlan):
614 """Set the vtep vlan to use indexed by the chain id on given port.
615 port_index: the port for which VLAN must be set
617 self.devices[port_index].set_vtep_vlan(vlan)
619 def set_vxlan_endpoints(self, port_index, src_ip, dst_ip):
620 self.devices[port_index].set_vxlan_endpoints(src_ip, dst_ip)
622 def set_mpls_peers(self, port_index, src_ip, dst_ip):
623 self.devices[port_index].set_mpls_peers(src_ip, dst_ip)
626 def __match_generator_profile(traffic_generator, generator_profile):
627 gen_config = AttrDict(traffic_generator)
628 gen_config.pop('default_profile')
629 gen_config.pop('generator_profile')
630 matching_profile = [profile for profile in traffic_generator.generator_profile if
631 profile.name == generator_profile]
632 if len(matching_profile) != 1:
633 raise Exception('Traffic generator profile not found: ' + generator_profile)
635 gen_config.update(matching_profile[0])
639 class TrafficClient(object):
640 """Traffic generator client with NDR/PDR binary seearch."""
644 def __init__(self, config, notifier=None):
645 """Create a new TrafficClient instance.
647 config: nfvbench config
648 notifier: notifier (optional)
650 A new instance is created everytime the nfvbench config may have changed.
653 self.generator_config = GeneratorConfig(config)
654 self.tool = self.generator_config.tool
655 self.gen = self._get_generator()
656 self.notifier = notifier
657 self.interval_collector = None
658 self.iteration_collector = None
659 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec,
660 self.config.service_mode)
661 self.config.frame_sizes = self._get_frame_sizes()
663 'l2frame_size': None,
664 'duration_sec': self.config.duration_sec,
665 'bidirectional': True,
666 'rates': [] # to avoid unsbuscriptable-obj warning
668 self.current_total_rate = {'rate_percent': '10'}
669 if self.config.single_run:
670 self.current_total_rate = utils.parse_rate_str(self.config.rate)
672 # Speed is either discovered when connecting to TG or set from config
673 # This variable is 0 if not yet discovered from TG or must be the speed of
674 # each interface in bits per second
675 self.intf_speed = self.generator_config.intf_speed
677 def _get_generator(self):
678 tool = self.tool.lower()
680 from .traffic_gen import trex_gen
681 return trex_gen.TRex(self)
683 from .traffic_gen import dummy
684 return dummy.DummyTG(self)
685 raise TrafficClientException('Unsupported generator tool name:' + self.tool)
687 def skip_sleep(self):
688 """Skip all sleeps when doing unit testing with dummy TG.
690 Must be overriden using mock.patch
694 def _get_frame_sizes(self):
695 traffic_profile_name = self.config.traffic.profile
696 matching_profiles = [profile for profile in self.config.traffic_profile if
697 profile.name == traffic_profile_name]
698 if len(matching_profiles) > 1:
699 raise TrafficClientException('Multiple traffic profiles with name: ' +
700 traffic_profile_name)
701 if not matching_profiles:
702 raise TrafficClientException('Cannot find traffic profile: ' + traffic_profile_name)
703 return matching_profiles[0].l2frame_size
705 def start_traffic_generator(self):
706 """Start the traffic generator process (traffic not started yet)."""
708 # pick up the interface speed if it is not set from config
709 intf_speeds = self.gen.get_port_speed_gbps()
710 # convert Gbps unit into bps
711 tg_if_speed = bitmath.parse_string(str(intf_speeds[0]) + 'Gb').bits
713 # interface speed is overriden from config
714 if self.intf_speed != tg_if_speed:
715 # Warn the user if the speed in the config is different
716 LOG.warning('Interface speed provided is different from actual speed (%d Gbps)',
719 # interface speed not provisioned by config
720 self.intf_speed = tg_if_speed
721 # also update the speed in the tg config
722 self.generator_config.intf_speed = tg_if_speed
724 # Save the traffic generator local MAC
725 for mac, device in zip(self.gen.get_macs(), self.generator_config.devices):
729 """Set up the traffic client."""
730 self.gen.clear_stats()
732 def get_version(self):
733 """Get the traffic generator version."""
734 return self.gen.get_version()
736 def ensure_end_to_end(self):
737 """Ensure traffic generator receives packets it has transmitted.
739 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
741 VMs that are started and in active state may not pass traffic yet. It is imperative to make
742 sure that all VMs are passing traffic in both directions before starting any benchmarking.
743 To verify this, we need to send at a low frequency bi-directional packets and make sure
744 that we receive all packets back from all VMs. The number of flows is equal to 2 times
745 the number of chains (1 per direction) and we need to make sure we receive packets coming
746 from exactly 2 x chain count different source MAC addresses.
749 PVP chain (1 VM per chain)
750 N = 10 (number of chains)
751 Flow count = 20 (number of flows)
752 If the number of unique source MAC addresses from received packets is 20 then
753 all 10 VMs 10 VMs are in operational state.
755 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
756 # send 2pps on each chain and each direction
757 rate_pps = {'rate_pps': str(self.config.service_chain_count * 2)}
758 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False,
760 # ensures enough traffic is coming back
761 retry_count = int((self.config.check_traffic_time_sec +
762 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec)
764 # we expect to see packets coming from 2 unique MAC per chain
765 # because there can be flooding in the case of shared net
766 # we must verify that packets from the right VMs are received
767 # and not just count unique src MAC
768 # create a dict of (port, chain) tuples indexed by dest mac
770 for port, dest_macs in enumerate(self.generator_config.get_dest_macs()):
771 for chain, mac in enumerate(dest_macs):
772 mac_map[mac] = (port, chain)
773 unique_src_mac_count = len(mac_map)
774 if self.config.vxlan and self.config.traffic_generator.vtep_vlan:
775 get_mac_id = lambda packet: packet['binary'][60:66]
776 elif self.config.vxlan:
777 get_mac_id = lambda packet: packet['binary'][56:62]
778 elif self.config.mpls:
779 get_mac_id = lambda packet: packet['binary'][24:30]
780 # mpls_transport_label = lambda packet: packet['binary'][14:18]
782 get_mac_id = lambda packet: packet['binary'][6:12]
783 for it in range(retry_count):
784 self.gen.clear_stats()
785 self.gen.start_traffic()
786 self.gen.start_capture()
787 LOG.info('Captured unique src mac %d/%d, capturing return packets (retry %d/%d)...',
788 unique_src_mac_count - len(mac_map), unique_src_mac_count,
790 if not self.skip_sleep():
791 time.sleep(self.config.generic_poll_sec)
792 self.gen.stop_traffic()
793 self.gen.fetch_capture_packets()
794 self.gen.stop_capture()
795 for packet in self.gen.packet_list:
796 mac_id = get_mac_id(packet).decode('latin-1')
797 src_mac = ':'.join(["%02x" % ord(x) for x in mac_id])
799 if src_mac in mac_map and self.is_mpls(packet):
800 port, chain = mac_map[src_mac]
801 LOG.info('Received mpls packet from mac: %s (chain=%d, port=%d)',
802 src_mac, chain, port)
803 mac_map.pop(src_mac, None)
805 if src_mac in mac_map and self.is_udp(packet):
806 port, chain = mac_map[src_mac]
807 LOG.info('Received udp packet from mac: %s (chain=%d, port=%d)',
808 src_mac, chain, port)
809 mac_map.pop(src_mac, None)
812 LOG.info('End-to-end connectivity established')
814 if self.config.l3_router and not self.config.no_arp:
815 # In case of L3 traffic mode, routers are not able to route traffic
816 # until VM interfaces are up and ARP requests are done
817 LOG.info('Waiting for loopback service completely started...')
818 LOG.info('Sending ARP request to assure end-to-end connectivity established')
819 self.ensure_arp_successful()
820 raise TrafficClientException('End-to-end connectivity cannot be ensured')
822 def is_udp(self, packet):
823 pkt = Ether(packet['binary'])
826 def is_mpls(self, packet):
827 pkt = Ether(packet['binary'])
830 def ensure_arp_successful(self):
831 """Resolve all IP using ARP and throw an exception in case of failure."""
832 dest_macs = self.gen.resolve_arp()
834 # all dest macs are discovered, saved them into the generator config
835 if self.config.vxlan or self.config.mpls:
836 self.generator_config.set_vtep_dest_macs(0, dest_macs[0])
837 self.generator_config.set_vtep_dest_macs(1, dest_macs[1])
839 self.generator_config.set_dest_macs(0, dest_macs[0])
840 self.generator_config.set_dest_macs(1, dest_macs[1])
842 raise TrafficClientException('ARP cannot be resolved')
844 def set_traffic(self, frame_size, bidirectional):
845 """Reconfigure the traffic generator for a new frame size."""
846 self.run_config['bidirectional'] = bidirectional
847 self.run_config['l2frame_size'] = frame_size
848 self.run_config['rates'] = [self.get_per_direction_rate()]
850 self.run_config['rates'].append(self.get_per_direction_rate())
852 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
853 if unidir_reverse_pps > 0:
854 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
855 # Fix for [NFVBENCH-67], convert the rate string to PPS
856 for idx, rate in enumerate(self.run_config['rates']):
857 if 'rate_pps' not in rate:
858 self.run_config['rates'][idx] = {'rate_pps': self.__convert_rates(rate)['rate_pps']}
860 self.gen.clear_streamblock()
862 if self.config.no_latency_streams:
863 LOG.info("Latency streams are disabled")
864 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional,
865 latency=not self.config.no_latency_streams)
867 def _modify_load(self, load):
868 self.current_total_rate = {'rate_percent': str(load)}
869 rate_per_direction = self.get_per_direction_rate()
871 self.gen.modify_rate(rate_per_direction, False)
872 self.run_config['rates'][0] = rate_per_direction
873 if self.run_config['bidirectional']:
874 self.gen.modify_rate(rate_per_direction, True)
875 self.run_config['rates'][1] = rate_per_direction
877 def get_ndr_and_pdr(self):
878 """Start the NDR/PDR iteration and return the results."""
879 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
881 if self.config.ndr_run:
882 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
883 targets['ndr'] = self.config.measurement.NDR
884 if self.config.pdr_run:
885 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
886 targets['pdr'] = self.config.measurement.PDR
888 self.run_config['start_time'] = time.time()
889 self.interval_collector = IntervalCollector(self.run_config['start_time'])
890 self.interval_collector.attach_notifier(self.notifier)
891 self.iteration_collector = IterationCollector(self.run_config['start_time'])
893 self.__range_search(0.0, 200.0, targets, results)
895 results['iteration_stats'] = {
896 'ndr_pdr': self.iteration_collector.get()
899 if self.config.ndr_run:
900 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
901 results['ndr']['time_taken_sec'] = \
902 results['ndr']['timestamp_sec'] - self.run_config['start_time']
903 if self.config.pdr_run:
904 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
905 results['pdr']['time_taken_sec'] = \
906 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
908 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
909 results['pdr']['time_taken_sec'] = \
910 results['pdr']['timestamp_sec'] - self.run_config['start_time']
913 def __get_dropped_rate(self, result):
914 dropped_pkts = result['rx']['dropped_pkts']
915 total_pkts = result['tx']['total_pkts']
918 return float(dropped_pkts) / total_pkts * 100
921 """Collect final stats for previous run."""
922 stats = self.gen.get_stats()
923 retDict = {'total_tx_rate': stats['total_tx_rate'],
924 'offered_tx_rate_bps': stats['offered_tx_rate_bps']}
926 tx_keys = ['total_pkts', 'total_pkt_bytes', 'pkt_rate', 'pkt_bit_rate']
927 rx_keys = tx_keys + ['dropped_pkts']
929 for port in self.PORTS:
930 port_stats = {'tx': {}, 'rx': {}}
932 port_stats['tx'][key] = int(stats[port]['tx'][key])
935 port_stats['rx'][key] = int(stats[port]['rx'][key])
937 port_stats['rx'][key] = 0
938 port_stats['rx']['avg_delay_usec'] = cast_integer(
939 stats[port]['rx']['avg_delay_usec'])
940 port_stats['rx']['min_delay_usec'] = cast_integer(
941 stats[port]['rx']['min_delay_usec'])
942 port_stats['rx']['max_delay_usec'] = cast_integer(
943 stats[port]['rx']['max_delay_usec'])
944 port_stats['drop_rate_percent'] = self.__get_dropped_rate(port_stats)
945 retDict[str(port)] = port_stats
947 ports = sorted(list(retDict.keys()), key=str)
948 if self.run_config['bidirectional']:
949 retDict['overall'] = {'tx': {}, 'rx': {}}
951 retDict['overall']['tx'][key] = \
952 retDict[ports[0]]['tx'][key] + retDict[ports[1]]['tx'][key]
954 retDict['overall']['rx'][key] = \
955 retDict[ports[0]]['rx'][key] + retDict[ports[1]]['rx'][key]
956 total_pkts = [retDict[ports[0]]['rx']['total_pkts'],
957 retDict[ports[1]]['rx']['total_pkts']]
958 avg_delays = [retDict[ports[0]]['rx']['avg_delay_usec'],
959 retDict[ports[1]]['rx']['avg_delay_usec']]
960 max_delays = [retDict[ports[0]]['rx']['max_delay_usec'],
961 retDict[ports[1]]['rx']['max_delay_usec']]
962 min_delays = [retDict[ports[0]]['rx']['min_delay_usec'],
963 retDict[ports[1]]['rx']['min_delay_usec']]
964 retDict['overall']['rx']['avg_delay_usec'] = utils.weighted_avg(total_pkts, avg_delays)
965 retDict['overall']['rx']['min_delay_usec'] = min(min_delays)
966 retDict['overall']['rx']['max_delay_usec'] = max(max_delays)
967 for key in ['pkt_bit_rate', 'pkt_rate']:
968 for dirc in ['tx', 'rx']:
969 retDict['overall'][dirc][key] /= 2.0
971 retDict['overall'] = retDict[ports[0]]
972 retDict['overall']['drop_rate_percent'] = self.__get_dropped_rate(retDict['overall'])
975 def __convert_rates(self, rate):
976 return utils.convert_rates(self.run_config['l2frame_size'],
980 def __ndr_pdr_found(self, tag, load):
981 rates = self.__convert_rates({'rate_percent': load})
982 self.iteration_collector.add_ndr_pdr(tag, rates['rate_pps'])
983 last_stats = self.iteration_collector.peek()
984 self.interval_collector.add_ndr_pdr(tag, last_stats)
986 def __format_output_stats(self, stats):
987 for key in self.PORTS + ['overall']:
989 interface = stats[key]
991 'tx_pkts': interface['tx']['total_pkts'],
992 'rx_pkts': interface['rx']['total_pkts'],
993 'drop_percentage': interface['drop_rate_percent'],
994 'drop_pct': interface['rx']['dropped_pkts'],
995 'avg_delay_usec': interface['rx']['avg_delay_usec'],
996 'max_delay_usec': interface['rx']['max_delay_usec'],
997 'min_delay_usec': interface['rx']['min_delay_usec'],
1002 def __targets_found(self, rate, targets, results):
1003 for tag, target in list(targets.items()):
1004 LOG.info('Found %s (%s) load: %s', tag, target, rate)
1005 self.__ndr_pdr_found(tag, rate)
1006 results[tag]['timestamp_sec'] = time.time()
1008 def __range_search(self, left, right, targets, results):
1009 """Perform a binary search for a list of targets inside a [left..right] range or rate.
1011 left the left side of the range to search as a % the line rate (100 = 100% line rate)
1012 indicating the rate to send on each interface
1013 right the right side of the range to search as a % of line rate
1014 indicating the rate to send on each interface
1015 targets a dict of drop rates to search (0.1 = 0.1%), indexed by the DR name or "tag"
1017 results a dict to store results
1021 LOG.info('Range search [%s .. %s] targets: %s', left, right, targets)
1023 # Terminate search when gap is less than load epsilon
1024 if right - left < self.config.measurement.load_epsilon:
1025 self.__targets_found(left, targets, results)
1028 # Obtain the average drop rate in for middle load
1029 middle = (left + right) / 2.0
1031 stats, rates = self.__run_search_iteration(middle)
1033 LOG.exception("Got exception from traffic generator during binary search")
1034 self.__targets_found(left, targets, results)
1036 # Split target dicts based on the avg drop rate
1039 for tag, target in list(targets.items()):
1040 if stats['overall']['drop_rate_percent'] <= target:
1041 # record the best possible rate found for this target
1042 results[tag] = rates
1043 results[tag].update({
1044 'load_percent_per_direction': middle,
1045 'stats': self.__format_output_stats(dict(stats)),
1046 'timestamp_sec': None
1048 right_targets[tag] = target
1050 # initialize to 0 all fields of result for
1051 # the worst case scenario of the binary search (if ndr/pdr is not found)
1052 if tag not in results:
1053 results[tag] = dict.fromkeys(rates, 0)
1054 empty_stats = self.__format_output_stats(dict(stats))
1055 for key in empty_stats:
1056 if isinstance(empty_stats[key], dict):
1057 empty_stats[key] = dict.fromkeys(empty_stats[key], 0)
1059 empty_stats[key] = 0
1060 results[tag].update({
1061 'load_percent_per_direction': 0,
1062 'stats': empty_stats,
1063 'timestamp_sec': None
1065 left_targets[tag] = target
1068 self.__range_search(left, middle, left_targets, results)
1070 # search upper half only if the upper rate does not exceed
1071 # 100%, this only happens when the first search at 100%
1072 # yields a DR that is < target DR
1074 self.__targets_found(100, right_targets, results)
1076 self.__range_search(middle, right, right_targets, results)
1078 def __run_search_iteration(self, rate):
1079 """Run one iteration at the given rate level.
1081 rate: the rate to send on each port in percent (0 to 100)
1083 self._modify_load(rate)
1085 # poll interval stats and collect them
1086 for stats in self.run_traffic():
1087 self.interval_collector.add(stats)
1088 time_elapsed_ratio = self.runner.time_elapsed() / self.run_config['duration_sec']
1089 if time_elapsed_ratio >= 1:
1090 self.cancel_traffic()
1091 if not self.skip_sleep():
1092 time.sleep(self.config.pause_sec)
1093 self.interval_collector.reset()
1095 # get stats from the run
1096 stats = self.runner.client.get_stats()
1097 current_traffic_config = self._get_traffic_config()
1098 warning = self.compare_tx_rates(current_traffic_config['direction-total']['rate_pps'],
1099 stats['total_tx_rate'])
1100 if warning is not None:
1101 stats['warning'] = warning
1103 # save reliable stats from whole iteration
1104 self.iteration_collector.add(stats, current_traffic_config['direction-total']['rate_pps'])
1105 LOG.info('Average drop rate: %f', stats['overall']['drop_rate_percent'])
1106 return stats, current_traffic_config['direction-total']
1108 def log_stats(self, stats):
1109 """Log estimated stats during run."""
1110 # Calculate a rolling drop rate based on differential to
1111 # the previous reading
1112 cur_tx = stats['overall']['tx']['total_pkts']
1113 cur_rx = stats['overall']['rx']['total_pkts']
1114 delta_tx = cur_tx - self.prev_tx
1115 delta_rx = cur_rx - self.prev_rx
1116 drops = delta_tx - delta_rx
1117 drop_rate_pct = 100 * (delta_tx - delta_rx)/delta_tx
1118 self.prev_tx = cur_tx
1119 self.prev_rx = cur_rx
1120 LOG.info('TX: %15s; RX: %15s; (Est.) Dropped: %12s; Drop rate: %8.4f%%',
1121 format(cur_tx, ',d'),
1122 format(cur_rx, ',d'),
1123 format(drops, ',d'),
1126 def run_traffic(self):
1127 """Start traffic and return intermediate stats for each interval."""
1128 stats = self.runner.run()
1131 while self.runner.is_running:
1132 self.log_stats(stats)
1134 stats = self.runner.poll_stats()
1137 self.log_stats(stats)
1138 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
1141 def cancel_traffic(self):
1145 def _get_traffic_config(self):
1150 for idx, rate in enumerate(self.run_config['rates']):
1151 key = 'direction-forward' if idx == 0 else 'direction-reverse'
1153 'l2frame_size': self.run_config['l2frame_size'],
1154 'duration_sec': self.run_config['duration_sec']
1156 config[key].update(rate)
1157 config[key].update(self.__convert_rates(rate))
1158 load_total += float(config[key]['rate_percent'])
1159 bps_total += float(config[key]['rate_bps'])
1160 pps_total += float(config[key]['rate_pps'])
1161 config['direction-total'] = dict(config['direction-forward'])
1162 config['direction-total'].update({
1163 'rate_percent': load_total,
1164 'rate_pps': cast_integer(pps_total),
1165 'rate_bps': bps_total
1170 def get_run_config(self, results):
1171 """Return configuration which was used for the last run."""
1173 # because we want each direction to have the far end RX rates,
1174 # use the far end index (1-idx) to retrieve the RX rates
1175 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
1176 tx_rate = results["stats"][str(idx)]["tx"]["total_pkts"] / self.config.duration_sec
1177 rx_rate = results["stats"][str(1 - idx)]["rx"]["total_pkts"] / self.config.duration_sec
1179 "orig": self.__convert_rates(self.run_config['rates'][idx]),
1180 "tx": self.__convert_rates({'rate_pps': tx_rate}),
1181 "rx": self.__convert_rates({'rate_pps': rx_rate})
1185 for direction in ['orig', 'tx', 'rx']:
1186 total[direction] = {}
1187 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
1188 total[direction][unit] = sum([float(x[direction][unit]) for x in list(r.values())])
1190 r['direction-total'] = total
1193 def insert_interface_stats(self, pps_list):
1194 """Insert interface stats to a list of packet path stats.
1196 pps_list: a list of packet path stats instances indexed by chain index
1198 This function will insert the packet path stats for the traffic gen ports 0 and 1
1199 with itemized per chain tx/rx counters.
1200 There will be as many packet path stats as chains.
1201 Each packet path stats will have exactly 2 InterfaceStats for port 0 and port 1
1204 PacketPathStats(InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)),
1205 PacketPathStats(InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)),
1209 def get_if_stats(chain_idx):
1210 return [InterfaceStats('p' + str(port), self.tool)
1211 for port in range(2)]
1212 # keep the list of list of interface stats indexed by the chain id
1213 self.ifstats = [get_if_stats(chain_idx)
1214 for chain_idx in range(self.config.service_chain_count)]
1215 # note that we need to make a copy of the ifs list so that any modification in the
1216 # list from pps will not change the list saved in self.ifstats
1217 self.pps_list = [PacketPathStats(list(ifs)) for ifs in self.ifstats]
1218 # insert the corresponding pps in the passed list
1219 pps_list.extend(self.pps_list)
1221 def update_interface_stats(self, diff=False):
1222 """Update all interface stats.
1224 diff: if False, simply refresh the interface stats values with latest values
1225 if True, diff the interface stats with the latest values
1226 Make sure that the interface stats inserted in insert_interface_stats() are updated
1230 [InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)],
1231 [InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)],
1236 stats = self.gen.get_stats()
1237 for chain_idx, ifs in enumerate(self.ifstats):
1238 # each ifs has exactly 2 InterfaceStats and 2 Latency instances
1239 # corresponding to the
1240 # port 0 and port 1 for the given chain_idx
1241 # Note that we cannot use self.pps_list[chain_idx].if_stats to pick the
1242 # interface stats for the pps because it could have been modified to contain
1243 # additional interface stats
1244 self.gen.get_stream_stats(stats, ifs, self.pps_list[chain_idx].latencies, chain_idx)
1245 # special handling for vxlan
1246 # in case of vxlan, flow stats are not available so all rx counters will be
1247 # zeros when the total rx port counter is non zero.
1249 for port in range(2):
1251 for ifs in self.ifstats:
1252 total_rx += ifs[port].rx
1254 # check if the total port rx from Trex is also zero
1255 port_rx = stats[port]['rx']['total_pkts']
1257 # the total rx for all chains from port level stats is non zero
1258 # which means that the per-chain stats are not available
1259 if len(self.ifstats) == 1:
1260 # only one chain, simply report the port level rx to the chain rx stats
1261 self.ifstats[0][port].rx = port_rx
1263 for ifs in self.ifstats:
1264 # mark this data as unavailable
1266 # pitch in the total rx only in the last chain pps
1267 self.ifstats[-1][port].rx_total = port_rx
1270 def compare_tx_rates(required, actual):
1271 """Compare the actual TX rate to the required TX rate."""
1273 are_different = False
1275 if float(actual) / required < threshold:
1276 are_different = True
1277 except ZeroDivisionError:
1278 are_different = True
1281 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
1282 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
1283 "to achieve the requested TX rate.".format(r=required, a=actual)
1289 def get_per_direction_rate(self):
1290 """Get the rate for each direction."""
1291 divisor = 2 if self.run_config['bidirectional'] else 1
1292 if 'rate_percent' in self.current_total_rate:
1293 # don't split rate if it's percentage
1296 return utils.divide_rate(self.current_total_rate, divisor)
1299 """Close this instance."""
1301 self.gen.stop_traffic()
1304 self.gen.clear_stats()