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."""
17 from datetime import datetime
23 from attrdict import AttrDict
25 from netaddr import IPNetwork
26 # pylint: disable=import-error
27 from trex.stl.api import Ether
28 from trex.stl.api import STLError
29 from trex.stl.api import UDP
30 # pylint: disable=wrong-import-order
31 from scapy.contrib.mpls import MPLS # flake8: noqa
32 # pylint: enable=wrong-import-order
33 # pylint: enable=import-error
36 from .packet_stats import InterfaceStats
37 from .packet_stats import PacketPathStats
38 from .stats_collector import IntervalCollector
39 from .stats_collector import IterationCollector
40 from .traffic_gen import traffic_utils as utils
41 from .utils import cast_integer
43 class TrafficClientException(Exception):
44 """Generic traffic client exception."""
46 class TrafficRunner(object):
47 """Serialize various steps required to run traffic."""
49 def __init__(self, client, duration_sec, interval_sec=0, service_mode=False):
50 """Create a traffic runner."""
52 self.start_time = None
53 self.duration_sec = duration_sec
54 self.interval_sec = interval_sec
55 self.service_mode = service_mode
58 """Clear stats and instruct the traffic generator to start generating traffic."""
61 LOG.info('Running traffic generator')
62 self.client.gen.clear_stats()
63 # Debug use only : new '--service-mode' option available for the NFVBench command line.
64 # A read-only mode TRex console would be able to capture the generated traffic.
65 self.client.gen.set_service_mode(enabled=self.service_mode)
66 LOG.info('Service mode is %sabled', 'en' if self.service_mode else 'dis')
67 self.client.gen.start_traffic()
68 self.start_time = time.time()
69 return self.poll_stats()
72 """Stop the current run and instruct the traffic generator to stop traffic."""
74 self.start_time = None
75 self.client.gen.stop_traffic()
78 """Check if a run is still pending."""
79 return self.start_time is not None
81 def time_elapsed(self):
82 """Return time elapsed since start of run."""
84 return time.time() - self.start_time
85 return self.duration_sec
88 """Poll latest stats from the traffic generator at fixed interval - sleeps if necessary.
90 return: latest stats or None if traffic is stopped
92 if not self.is_running():
94 if self.client.skip_sleep():
96 return self.client.get_stats()
97 time_elapsed = self.time_elapsed()
98 if time_elapsed > self.duration_sec:
101 time_left = self.duration_sec - time_elapsed
102 if self.interval_sec > 0.0:
103 if time_left <= self.interval_sec:
104 time.sleep(time_left)
107 time.sleep(self.interval_sec)
109 time.sleep(self.duration_sec)
111 return self.client.get_stats()
114 class IpBlock(object):
115 """Manage a block of IP addresses."""
117 def __init__(self, base_ip, step_ip, count_ip):
118 """Create an IP block."""
119 self.base_ip_int = Device.ip_to_int(base_ip)
120 self.step = Device.ip_to_int(step_ip)
121 self.max_available = count_ip
124 def get_ip(self, index=0):
125 """Return the IP address at given index."""
126 if index < 0 or index >= self.max_available:
127 raise IndexError('Index out of bounds: %d (max=%d)' % (index, self.max_available))
128 return Device.int_to_ip(self.base_ip_int + index * self.step)
130 def reserve_ip_range(self, count, force_ip_reservation=False):
131 """Reserve a range of count consecutive IP addresses spaced by step.
132 force_ip_reservation parameter allows to continue the calculation of IPs when
133 the 2 sides (ports) have different size and the flow is greater than
136 if self.next_free + count > self.max_available and force_ip_reservation is False:
137 raise IndexError('No more IP addresses next free=%d max_available=%d requested=%d' %
141 if self.next_free + count > self.max_available and force_ip_reservation is True:
142 first_ip = self.get_ip(self.next_free)
143 last_ip = self.get_ip(self.next_free + self.max_available - 1)
144 self.next_free += self.max_available
146 first_ip = self.get_ip(self.next_free)
147 last_ip = self.get_ip(self.next_free + count - 1)
148 self.next_free += count
149 return (first_ip, last_ip)
151 def reset_reservation(self):
152 """Reset all reservations and restart with a completely unused IP block."""
156 class UdpPorts(object):
158 def __init__(self, src_min, src_max, dst_min, dst_max, step):
160 self.src_min = src_min
161 self.src_max = src_max
162 self.dst_min = dst_min
163 self.dst_max = dst_max
167 class Device(object):
168 """Represent a port device and all information associated to it.
170 In the curent version we only support 2 port devices for the traffic generator
171 identified as port 0 or port 1.
174 def __init__(self, port, generator_config):
175 """Create a new device for a given port."""
176 self.generator_config = generator_config
177 self.chain_count = generator_config.service_chain_count
178 if generator_config.bidirectional:
179 self.flow_count = generator_config.flow_count / 2
181 self.flow_count = generator_config.flow_count
184 self.switch_port = generator_config.interfaces[port].get('switch_port', None)
185 self.vtep_vlan = None
186 self.vtep_src_mac = None
189 self.inner_labels = None
190 self.outer_labels = None
191 self.pci = generator_config.interfaces[port].pci
193 self.dest_macs = None
194 self.vtep_dst_mac = None
195 self.vtep_dst_ip = None
196 if generator_config.vteps is None:
197 self.vtep_src_ip = None
199 self.vtep_src_ip = generator_config.vteps[port]
202 self.ip_addrs = generator_config.ip_addrs[port]
203 self.ip_src_static = generator_config.ip_src_static
204 self.ip_addrs_step = generator_config.ip_addrs_step
205 if self.ip_addrs_step == 'random':
206 # Set step to 1 to calculate the IP range size (see check_ip_size below)
209 step = self.ip_addrs_step
210 self.ip_size = self.check_ipsize(IPNetwork(self.ip_addrs).size, Device.ip_to_int(step))
211 self.ip = str(IPNetwork(self.ip_addrs).network)
212 ip_addrs_left = generator_config.ip_addrs[0]
213 ip_addrs_right = generator_config.ip_addrs[1]
214 self.ip_addrs_size = {
215 'left': self.check_ipsize(IPNetwork(ip_addrs_left).size, Device.ip_to_int(step)),
216 'right': self.check_ipsize(IPNetwork(ip_addrs_right).size, Device.ip_to_int(step))}
217 udp_src_port = generator_config.gen_config.udp_src_port
218 if udp_src_port is None:
220 udp_dst_port = generator_config.gen_config.udp_dst_port
221 if udp_dst_port is None:
223 src_max, src_min = self.define_udp_range(udp_src_port, 'udp_src_port')
224 dst_max, dst_min = self.define_udp_range(udp_dst_port, 'udp_dst_port')
225 udp_src_range = int(src_max) - int(src_min) + 1
226 udp_dst_range = int(dst_max) - int(dst_min) + 1
227 lcm_port = self.lcm(udp_src_range, udp_dst_range)
228 if self.ip_src_static is True:
229 lcm_ip = self.lcm(1, min(self.ip_addrs_size['left'], self.ip_addrs_size['right']))
231 lcm_ip = self.lcm(self.ip_addrs_size['left'], self.ip_addrs_size['right'])
232 flow_max = self.lcm(lcm_port, lcm_ip)
233 if self.flow_count > flow_max:
234 raise TrafficClientException('Trying to set unachievable traffic (%d > %d)' %
235 (self.flow_count, flow_max))
237 # manage udp range regarding flow count value
238 # UDP dst range is greater than FC => range will be limited to min + FC
239 if self.flow_count <= udp_dst_range:
240 dst_max = int(dst_min) + self.flow_count - 1
241 # UDP src range is greater than FC => range will be limited to min + FC
242 if self.flow_count <= udp_src_range:
243 src_max = int(src_min) + self.flow_count - 1
244 # Define IP block limit regarding flow count
245 if self.flow_count <= self.ip_size:
246 self.ip_block = IpBlock(self.ip, step, self.flow_count)
248 self.ip_block = IpBlock(self.ip, step, self.ip_size)
250 if generator_config.gen_config.udp_port_step == 'random':
253 step = generator_config.gen_config.udp_port_step
254 self.udp_ports = UdpPorts(src_min, src_max, dst_min, dst_max, step)
255 self.gw_ip_block = IpBlock(generator_config.gateway_ips[port],
256 generator_config.gateway_ip_addrs_step,
258 self.tg_gateway_ip_addrs = generator_config.tg_gateway_ip_addrs[port]
259 self.tg_gw_ip_block = IpBlock(self.tg_gateway_ip_addrs,
260 generator_config.tg_gateway_ip_addrs_step,
264 def define_udp_range(udp_port, property_name):
265 if isinstance(udp_port, int):
268 elif isinstance(udp_port, tuple):
272 raise TrafficClientException('Invalid %s property value (53 or [\'53\',\'1024\'])'
278 """Calculate the maximum possible value for both IP and ports,
279 eventually for maximum possible flux."""
280 if a != 0 and b != 0:
281 lcm_value = a * b // gcd(a, b)
283 raise TypeError(" IP size or port range can't be zero !")
286 def check_ipsize(ip_size, step):
287 """Check and set the available IPs, considering the step."""
289 if ip_size % step == 0:
290 value = int(ip_size / step)
292 value = int((ip_size / step)) + 1
294 except ZeroDivisionError:
295 raise ZeroDivisionError("step can't be zero !")
297 def set_mac(self, mac):
298 """Set the local MAC for this port device."""
300 raise TrafficClientException('Trying to set traffic generator MAC address as None')
303 def get_peer_device(self):
304 """Get the peer device (device 0 -> device 1, or device 1 -> device 0)."""
305 return self.generator_config.devices[1 - self.port]
307 def set_vtep_dst_mac(self, dest_macs):
308 """Set the list of dest MACs indexed by the chain id.
310 This is only called in 2 cases:
311 - VM macs discovered using openstack API
312 - dest MACs provisioned in config file
314 self.vtep_dst_mac = list(map(str, dest_macs))
316 def set_dest_macs(self, dest_macs):
317 """Set the list of dest MACs indexed by the chain id.
319 This is only called in 2 cases:
320 - VM macs discovered using openstack API
321 - dest MACs provisioned in config file
323 self.dest_macs = list(map(str, dest_macs))
325 def get_dest_macs(self):
326 """Get the list of dest macs for this device.
328 If set_dest_macs was never called, assumes l2-loopback and return
329 a list of peer mac (as many as chains but normally only 1 chain)
332 return self.dest_macs
333 # assume this is l2-loopback
334 return [self.get_peer_device().mac] * self.chain_count
336 def set_vlans(self, vlans):
337 """Set the list of vlans to use indexed by the chain id."""
339 LOG.info("Port %d: VLANs %s", self.port, self.vlans)
341 def set_vtep_vlan(self, vlan):
342 """Set the vtep vlan to use indexed by specific port."""
343 self.vtep_vlan = vlan
345 self.vlan_tagging = None
346 LOG.info("Port %d: VTEP VLANs %s", self.port, self.vtep_vlan)
348 def set_vxlan_endpoints(self, src_ip, dst_ip):
349 self.vtep_dst_ip = dst_ip
350 self.vtep_src_ip = src_ip
351 LOG.info("Port %d: src_vtep %s, dst_vtep %s", self.port,
352 self.vtep_src_ip, self.vtep_dst_ip)
354 def set_mpls_peers(self, src_ip, dst_ip):
356 self.vtep_dst_ip = dst_ip
357 self.vtep_src_ip = src_ip
358 LOG.info("Port %d: src_mpls_vtep %s, mpls_peer_ip %s", self.port,
359 self.vtep_src_ip, self.vtep_dst_ip)
361 def set_vxlans(self, vnis):
363 LOG.info("Port %d: VNIs %s", self.port, self.vnis)
365 def set_mpls_inner_labels(self, labels):
366 self.inner_labels = labels
367 LOG.info("Port %d: MPLS Inner Labels %s", self.port, self.inner_labels)
369 def set_mpls_outer_labels(self, labels):
370 self.outer_labels = labels
371 LOG.info("Port %d: MPLS Outer Labels %s", self.port, self.outer_labels)
373 def set_gw_ip(self, gateway_ip):
374 self.gw_ip_block = IpBlock(gateway_ip,
375 self.generator_config.gateway_ip_addrs_step,
378 def get_gw_ip(self, chain_index):
379 """Retrieve the IP address assigned for the gateway of a given chain."""
380 return self.gw_ip_block.get_ip(chain_index)
382 def get_stream_configs(self):
383 """Get the stream config for a given chain on this device.
385 Called by the traffic generator driver to program the traffic generator properly
386 before generating traffic
389 # exact flow count for each chain is calculated as follows:
390 # - all chains except the first will have the same flow count
391 # calculated as (total_flows + chain_count - 1) / chain_count
392 # - the first chain will have the remainder
393 # example 11 flows and 3 chains => 3, 4, 4
394 flows_per_chain = int((self.flow_count + self.chain_count - 1) / self.chain_count)
395 cur_chain_flow_count = int(self.flow_count - flows_per_chain * (self.chain_count - 1))
396 force_ip_reservation = False
397 # use case example of this parameter:
398 # - static IP addresses (source & destination), netmask = /30
399 # - 4 varying UDP source ports | 1 UDP destination port
401 # --> parameter 'reserve_ip_range' should have flag 'force_ip_reservation'
402 # in order to assign the maximum available IP on each iteration
403 if self.ip_size < cur_chain_flow_count \
404 or self.ip_addrs_size['left'] != self.ip_addrs_size['right']:
405 force_ip_reservation = True
407 peer = self.get_peer_device()
408 self.ip_block.reset_reservation()
409 peer.ip_block.reset_reservation()
410 dest_macs = self.get_dest_macs()
412 for chain_idx in range(self.chain_count):
413 src_ip_first, src_ip_last = self.ip_block.reserve_ip_range\
414 (cur_chain_flow_count, force_ip_reservation)
415 dst_ip_first, dst_ip_last = peer.ip_block.reserve_ip_range\
416 (cur_chain_flow_count, force_ip_reservation)
419 'count': cur_chain_flow_count,
421 'mac_dst': dest_macs[chain_idx],
422 'ip_src_addr': src_ip_first,
423 'ip_src_addr_max': src_ip_last,
424 'ip_src_count': cur_chain_flow_count,
425 'ip_dst_addr': dst_ip_first,
426 'ip_dst_addr_max': dst_ip_last,
427 'ip_dst_count': cur_chain_flow_count,
428 'ip_addrs_step': self.ip_addrs_step,
429 'ip_src_static': self.ip_src_static,
430 'udp_src_port': self.udp_ports.src_min,
431 'udp_src_port_max': self.udp_ports.src_max,
432 'udp_src_count': int(self.udp_ports.src_max) - int(self.udp_ports.src_min) + 1,
433 'udp_dst_port': self.udp_ports.dst_min,
434 'udp_dst_port_max': self.udp_ports.dst_max,
435 'udp_dst_count': int(self.udp_ports.dst_max) - int(self.udp_ports.dst_min) + 1,
436 'udp_port_step': self.udp_ports.step,
437 'mac_discovery_gw': self.get_gw_ip(chain_idx),
438 'ip_src_tg_gw': self.tg_gw_ip_block.get_ip(chain_idx),
439 'ip_dst_tg_gw': peer.tg_gw_ip_block.get_ip(chain_idx),
440 'vlan_tag': self.vlans[chain_idx] if self.vlans else None,
442 'vtep_vlan': self.vtep_vlan if self.vtep_vlan else None,
443 'vtep_src_mac': self.mac if (self.vxlan or self.mpls) else None,
444 'vtep_dst_mac': self.vtep_dst_mac if (self.vxlan or self.mpls) else None,
445 'vtep_dst_ip': self.vtep_dst_ip if self.vxlan is True else None,
446 'vtep_src_ip': self.vtep_src_ip if self.vxlan is True else None,
447 'net_vni': self.vnis[chain_idx] if self.vxlan is True else None,
449 'mpls_outer_label': self.outer_labels[chain_idx] if self.mpls is True else None,
450 'mpls_inner_label': self.inner_labels[chain_idx] if self.mpls is True else None
453 # after first chain, fall back to the flow count for all other chains
454 cur_chain_flow_count = flows_per_chain
459 """Convert an IP address from string to numeric."""
460 return struct.unpack("!I", socket.inet_aton(addr))[0]
463 def int_to_ip(nvalue):
464 """Convert an IP address from numeric to string."""
465 return socket.inet_ntoa(struct.pack("!I", int(nvalue)))
468 class GeneratorConfig(object):
469 """Represents traffic configuration for currently running traffic profile."""
471 DEFAULT_IP_STEP = '0.0.0.1'
472 DEFAULT_SRC_DST_IP_STEP = '0.0.0.1'
474 def __init__(self, config):
475 """Create a generator config."""
477 # name of the generator profile (normally trex or dummy)
478 # pick the default one if not specified explicitly from cli options
479 if not config.generator_profile:
480 config.generator_profile = config.traffic_generator.default_profile
481 # pick up the profile dict based on the name
482 gen_config = self.__match_generator_profile(config.traffic_generator,
483 config.generator_profile)
484 self.gen_config = gen_config
485 # copy over fields from the dict
486 self.tool = gen_config.tool
487 self.ip = gen_config.ip
488 # overrides on config.cores and config.mbuf_factor
490 self.cores = config.cores
492 self.cores = gen_config.get('cores', 1)
493 self.mbuf_factor = config.mbuf_factor
494 self.mbuf_64 = config.mbuf_64
495 self.hdrh = not config.disable_hdrh
496 if gen_config.intf_speed:
497 # interface speed is overriden from config
498 self.intf_speed = bitmath.parse_string(gen_config.intf_speed.replace('ps', '')).bits
500 # interface speed is discovered/provided by the traffic generator
502 self.name = gen_config.name
503 self.zmq_pub_port = gen_config.get('zmq_pub_port', 4500)
504 self.zmq_rpc_port = gen_config.get('zmq_rpc_port', 4501)
505 self.limit_memory = gen_config.get('limit_memory', 1024)
506 self.software_mode = gen_config.get('software_mode', False)
507 self.interfaces = gen_config.interfaces
508 if self.interfaces[0].port != 0 or self.interfaces[1].port != 1:
509 raise TrafficClientException('Invalid port order/id in generator_profile.interfaces')
510 self.service_chain = config.service_chain
511 self.service_chain_count = config.service_chain_count
512 self.flow_count = config.flow_count
513 self.host_name = gen_config.host_name
514 self.bidirectional = config.traffic.bidirectional
515 self.tg_gateway_ip_addrs = gen_config.tg_gateway_ip_addrs
516 self.ip_addrs = gen_config.ip_addrs
517 self.ip_addrs_step = gen_config.ip_addrs_step or self.DEFAULT_SRC_DST_IP_STEP
518 self.tg_gateway_ip_addrs_step = \
519 gen_config.tg_gateway_ip_addrs_step or self.DEFAULT_IP_STEP
520 self.gateway_ip_addrs_step = gen_config.gateway_ip_addrs_step or self.DEFAULT_IP_STEP
521 self.gateway_ips = gen_config.gateway_ip_addrs
522 self.ip_src_static = gen_config.ip_src_static
523 self.vteps = gen_config.get('vteps')
524 self.devices = [Device(port, self) for port in [0, 1]]
525 # This should normally always be [0, 1]
526 self.ports = [device.port for device in self.devices]
528 # check that pci is not empty
529 if not gen_config.interfaces[0].get('pci', None) or \
530 not gen_config.interfaces[1].get('pci', None):
531 raise TrafficClientException("configuration interfaces pci fields cannot be empty")
533 self.pcis = [tgif['pci'] for tgif in gen_config.interfaces]
534 self.vlan_tagging = config.vlan_tagging
536 # needed for result/summarizer
537 config['tg-name'] = gen_config.name
538 config['tg-tool'] = self.tool
541 """Get json form to display the content into the overall result dict."""
542 return dict(self.gen_config)
544 def set_dest_macs(self, port_index, dest_macs):
545 """Set the list of dest MACs indexed by the chain id on given port.
547 port_index: the port for which dest macs must be set
548 dest_macs: a list of dest MACs indexed by chain id
550 if len(dest_macs) < self.config.service_chain_count:
551 raise TrafficClientException('Dest MAC list %s must have %d entries' %
552 (dest_macs, self.config.service_chain_count))
553 # only pass the first scc dest MACs
554 self.devices[port_index].set_dest_macs(dest_macs[:self.config.service_chain_count])
555 LOG.info('Port %d: dst MAC %s', port_index, [str(mac) for mac in dest_macs])
557 def set_vtep_dest_macs(self, port_index, dest_macs):
558 """Set the list of dest MACs indexed by the chain id on given port.
560 port_index: the port for which dest macs must be set
561 dest_macs: a list of dest MACs indexed by chain id
563 if len(dest_macs) != self.config.service_chain_count:
564 raise TrafficClientException('Dest MAC list %s must have %d entries' %
565 (dest_macs, self.config.service_chain_count))
566 self.devices[port_index].set_vtep_dst_mac(dest_macs)
567 LOG.info('Port %d: vtep dst MAC %s', port_index, {str(mac) for mac in dest_macs})
569 def get_dest_macs(self):
570 """Return the list of dest macs indexed by port."""
571 return [dev.get_dest_macs() for dev in self.devices]
573 def set_vlans(self, port_index, vlans):
574 """Set the list of vlans to use indexed by the chain id on given port.
576 port_index: the port for which VLANs must be set
577 vlans: a list of vlan lists indexed by chain id
579 if len(vlans) != self.config.service_chain_count:
580 raise TrafficClientException('VLAN list %s must have %d entries' %
581 (vlans, self.config.service_chain_count))
582 self.devices[port_index].set_vlans(vlans)
584 def set_vxlans(self, port_index, vxlans):
585 """Set the list of vxlans (VNIs) to use indexed by the chain id on given port.
587 port_index: the port for which VXLANs must be set
588 VXLANs: a list of VNIs lists indexed by chain id
590 if len(vxlans) != self.config.service_chain_count:
591 raise TrafficClientException('VXLAN list %s must have %d entries' %
592 (vxlans, self.config.service_chain_count))
593 self.devices[port_index].set_vxlans(vxlans)
595 def set_mpls_inner_labels(self, port_index, labels):
596 """Set the list of MPLS Labels to use indexed by the chain id on given port.
598 port_index: the port for which Labels must be set
599 Labels: a list of Labels lists indexed by chain id
601 if len(labels) != self.config.service_chain_count:
602 raise TrafficClientException('Inner MPLS list %s must have %d entries' %
603 (labels, self.config.service_chain_count))
604 self.devices[port_index].set_mpls_inner_labels(labels)
606 def set_mpls_outer_labels(self, port_index, labels):
607 """Set the list of MPLS Labels to use indexed by the chain id on given port.
609 port_index: the port for which Labels must be set
610 Labels: a list of Labels lists indexed by chain id
612 if len(labels) != self.config.service_chain_count:
613 raise TrafficClientException('Outer MPLS list %s must have %d entries' %
614 (labels, self.config.service_chain_count))
615 self.devices[port_index].set_mpls_outer_labels(labels)
617 def set_vtep_vlan(self, port_index, vlan):
618 """Set the vtep vlan to use indexed by the chain id on given port.
619 port_index: the port for which VLAN must be set
621 self.devices[port_index].set_vtep_vlan(vlan)
623 def set_vxlan_endpoints(self, port_index, src_ip, dst_ip):
624 self.devices[port_index].set_vxlan_endpoints(src_ip, dst_ip)
626 def set_mpls_peers(self, port_index, src_ip, dst_ip):
627 self.devices[port_index].set_mpls_peers(src_ip, dst_ip)
630 def __match_generator_profile(traffic_generator, generator_profile):
631 gen_config = AttrDict(traffic_generator)
632 gen_config.pop('default_profile')
633 gen_config.pop('generator_profile')
634 matching_profile = [profile for profile in traffic_generator.generator_profile if
635 profile.name == generator_profile]
636 if len(matching_profile) != 1:
637 raise Exception('Traffic generator profile not found: ' + generator_profile)
639 gen_config.update(matching_profile[0])
643 class TrafficClient(object):
644 """Traffic generator client with NDR/PDR binary seearch."""
648 def __init__(self, config, notifier=None):
649 """Create a new TrafficClient instance.
651 config: nfvbench config
652 notifier: notifier (optional)
654 A new instance is created everytime the nfvbench config may have changed.
657 self.generator_config = GeneratorConfig(config)
658 self.tool = self.generator_config.tool
659 self.gen = self._get_generator()
660 self.notifier = notifier
661 self.interval_collector = None
662 self.iteration_collector = None
663 self.runner = TrafficRunner(self, self.config.duration_sec, self.config.interval_sec,
664 self.config.service_mode)
665 self.config.frame_sizes = self._get_frame_sizes()
667 'l2frame_size': None,
668 'duration_sec': self.config.duration_sec,
669 'bidirectional': True,
670 'rates': [] # to avoid unsbuscriptable-obj warning
672 self.current_total_rate = {'rate_percent': '10'}
673 if self.config.single_run:
674 self.current_total_rate = utils.parse_rate_str(self.config.rate)
676 # Speed is either discovered when connecting to TG or set from config
677 # This variable is 0 if not yet discovered from TG or must be the speed of
678 # each interface in bits per second
679 self.intf_speed = self.generator_config.intf_speed
681 def _get_generator(self):
682 tool = self.tool.lower()
684 from .traffic_gen import trex_gen
685 return trex_gen.TRex(self)
687 from .traffic_gen import dummy
688 return dummy.DummyTG(self)
689 raise TrafficClientException('Unsupported generator tool name:' + self.tool)
691 def skip_sleep(self):
692 """Skip all sleeps when doing unit testing with dummy TG.
694 Must be overriden using mock.patch
698 def _get_frame_sizes(self):
699 traffic_profile_name = self.config.traffic.profile
700 matching_profiles = [profile for profile in self.config.traffic_profile if
701 profile.name == traffic_profile_name]
702 if len(matching_profiles) > 1:
703 raise TrafficClientException('Multiple traffic profiles with name: ' +
704 traffic_profile_name)
705 if not matching_profiles:
706 raise TrafficClientException('Cannot find traffic profile: ' + traffic_profile_name)
707 return matching_profiles[0].l2frame_size
709 def start_traffic_generator(self):
710 """Start the traffic generator process (traffic not started yet)."""
712 # pick up the interface speed if it is not set from config
713 intf_speeds = self.gen.get_port_speed_gbps()
714 # convert Gbps unit into bps
715 tg_if_speed = bitmath.parse_string(str(intf_speeds[0]) + 'Gb').bits
717 # interface speed is overriden from config
718 if self.intf_speed != tg_if_speed:
719 # Warn the user if the speed in the config is different
720 LOG.warning('Interface speed provided is different from actual speed (%d Gbps)',
723 # interface speed not provisioned by config
724 self.intf_speed = tg_if_speed
725 # also update the speed in the tg config
726 self.generator_config.intf_speed = tg_if_speed
728 # Save the traffic generator local MAC
729 for mac, device in zip(self.gen.get_macs(), self.generator_config.devices):
733 """Set up the traffic client."""
734 self.gen.clear_stats()
736 def get_version(self):
737 """Get the traffic generator version."""
738 return self.gen.get_version()
740 def ensure_end_to_end(self):
741 """Ensure traffic generator receives packets it has transmitted.
743 This ensures end to end connectivity and also waits until VMs are ready to forward packets.
745 VMs that are started and in active state may not pass traffic yet. It is imperative to make
746 sure that all VMs are passing traffic in both directions before starting any benchmarking.
747 To verify this, we need to send at a low frequency bi-directional packets and make sure
748 that we receive all packets back from all VMs. The number of flows is equal to 2 times
749 the number of chains (1 per direction) and we need to make sure we receive packets coming
750 from exactly 2 x chain count different source MAC addresses.
753 PVP chain (1 VM per chain)
754 N = 10 (number of chains)
755 Flow count = 20 (number of flows)
756 If the number of unique source MAC addresses from received packets is 20 then
757 all 10 VMs 10 VMs are in operational state.
759 LOG.info('Starting traffic generator to ensure end-to-end connectivity')
760 # send 2pps on each chain and each direction
761 rate_pps = {'rate_pps': str(self.config.service_chain_count * 2)}
762 self.gen.create_traffic('64', [rate_pps, rate_pps], bidirectional=True, latency=False,
764 # ensures enough traffic is coming back
765 retry_count = int((self.config.check_traffic_time_sec +
766 self.config.generic_poll_sec - 1) / self.config.generic_poll_sec)
768 # we expect to see packets coming from 2 unique MAC per chain
769 # because there can be flooding in the case of shared net
770 # we must verify that packets from the right VMs are received
771 # and not just count unique src MAC
772 # create a dict of (port, chain) tuples indexed by dest mac
774 for port, dest_macs in enumerate(self.generator_config.get_dest_macs()):
775 for chain, mac in enumerate(dest_macs):
776 mac_map[mac] = (port, chain)
777 unique_src_mac_count = len(mac_map)
778 if self.config.vxlan and self.config.traffic_generator.vtep_vlan:
779 get_mac_id = lambda packet: packet['binary'][60:66]
780 elif self.config.vxlan:
781 get_mac_id = lambda packet: packet['binary'][56:62]
782 elif self.config.mpls:
783 get_mac_id = lambda packet: packet['binary'][24:30]
784 # mpls_transport_label = lambda packet: packet['binary'][14:18]
786 get_mac_id = lambda packet: packet['binary'][6:12]
787 for it in range(retry_count):
788 self.gen.clear_stats()
789 self.gen.start_traffic()
790 self.gen.start_capture()
791 LOG.info('Captured unique src mac %d/%d, capturing return packets (retry %d/%d)...',
792 unique_src_mac_count - len(mac_map), unique_src_mac_count,
794 if not self.skip_sleep():
795 time.sleep(self.config.generic_poll_sec)
796 self.gen.stop_traffic()
797 self.gen.fetch_capture_packets()
798 self.gen.stop_capture()
799 for packet in self.gen.packet_list:
800 mac_id = get_mac_id(packet).decode('latin-1')
801 src_mac = ':'.join(["%02x" % ord(x) for x in mac_id])
803 if src_mac in mac_map and self.is_mpls(packet):
804 port, chain = mac_map[src_mac]
805 LOG.info('Received mpls packet from mac: %s (chain=%d, port=%d)',
806 src_mac, chain, port)
807 mac_map.pop(src_mac, None)
809 if src_mac in mac_map and self.is_udp(packet):
810 port, chain = mac_map[src_mac]
811 LOG.info('Received udp packet from mac: %s (chain=%d, port=%d)',
812 src_mac, chain, port)
813 mac_map.pop(src_mac, None)
816 LOG.info('End-to-end connectivity established')
818 if self.config.l3_router and not self.config.no_arp:
819 # In case of L3 traffic mode, routers are not able to route traffic
820 # until VM interfaces are up and ARP requests are done
821 LOG.info('Waiting for loopback service completely started...')
822 LOG.info('Sending ARP request to assure end-to-end connectivity established')
823 self.ensure_arp_successful()
824 raise TrafficClientException('End-to-end connectivity cannot be ensured')
826 def is_udp(self, packet):
827 pkt = Ether(packet['binary'])
830 def is_mpls(self, packet):
831 pkt = Ether(packet['binary'])
834 def ensure_arp_successful(self):
835 """Resolve all IP using ARP and throw an exception in case of failure."""
836 dest_macs = self.gen.resolve_arp()
838 # all dest macs are discovered, saved them into the generator config
839 if self.config.vxlan or self.config.mpls:
840 self.generator_config.set_vtep_dest_macs(0, dest_macs[0])
841 self.generator_config.set_vtep_dest_macs(1, dest_macs[1])
843 self.generator_config.set_dest_macs(0, dest_macs[0])
844 self.generator_config.set_dest_macs(1, dest_macs[1])
846 raise TrafficClientException('ARP cannot be resolved')
848 def set_traffic(self, frame_size, bidirectional):
849 """Reconfigure the traffic generator for a new frame size."""
850 self.run_config['bidirectional'] = bidirectional
851 self.run_config['l2frame_size'] = frame_size
852 self.run_config['rates'] = [self.get_per_direction_rate()]
854 self.run_config['rates'].append(self.get_per_direction_rate())
856 unidir_reverse_pps = int(self.config.unidir_reverse_traffic_pps)
857 if unidir_reverse_pps > 0:
858 self.run_config['rates'].append({'rate_pps': str(unidir_reverse_pps)})
859 # Fix for [NFVBENCH-67], convert the rate string to PPS
860 for idx, rate in enumerate(self.run_config['rates']):
861 if 'rate_pps' not in rate:
862 self.run_config['rates'][idx] = {'rate_pps': self.__convert_rates(rate)['rate_pps']}
864 self.gen.clear_streamblock()
866 if self.config.no_latency_streams:
867 LOG.info("Latency streams are disabled")
868 self.gen.create_traffic(frame_size, self.run_config['rates'], bidirectional,
869 latency=not self.config.no_latency_streams)
871 def _modify_load(self, load):
872 self.current_total_rate = {'rate_percent': str(load)}
873 rate_per_direction = self.get_per_direction_rate()
875 self.gen.modify_rate(rate_per_direction, False)
876 self.run_config['rates'][0] = rate_per_direction
877 if self.run_config['bidirectional']:
878 self.gen.modify_rate(rate_per_direction, True)
879 self.run_config['rates'][1] = rate_per_direction
881 def get_ndr_and_pdr(self):
882 """Start the NDR/PDR iteration and return the results."""
883 dst = 'Bidirectional' if self.run_config['bidirectional'] else 'Unidirectional'
885 if self.config.ndr_run:
886 LOG.info('*** Searching NDR for %s (%s)...', self.run_config['l2frame_size'], dst)
887 targets['ndr'] = self.config.measurement.NDR
888 if self.config.pdr_run:
889 LOG.info('*** Searching PDR for %s (%s)...', self.run_config['l2frame_size'], dst)
890 targets['pdr'] = self.config.measurement.PDR
892 self.run_config['start_time'] = time.time()
893 self.interval_collector = IntervalCollector(self.run_config['start_time'])
894 self.interval_collector.attach_notifier(self.notifier)
895 self.iteration_collector = IterationCollector(self.run_config['start_time'])
897 self.__range_search(0.0, 200.0, targets, results)
899 results['iteration_stats'] = {
900 'ndr_pdr': self.iteration_collector.get()
903 if self.config.ndr_run:
904 LOG.info('NDR load: %s', results['ndr']['rate_percent'])
905 results['ndr']['time_taken_sec'] = \
906 results['ndr']['timestamp_sec'] - self.run_config['start_time']
907 if self.config.pdr_run:
908 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
909 results['pdr']['time_taken_sec'] = \
910 results['pdr']['timestamp_sec'] - results['ndr']['timestamp_sec']
912 LOG.info('PDR load: %s', results['pdr']['rate_percent'])
913 results['pdr']['time_taken_sec'] = \
914 results['pdr']['timestamp_sec'] - self.run_config['start_time']
917 def __get_dropped_rate(self, result):
918 dropped_pkts = result['rx']['dropped_pkts']
919 total_pkts = result['tx']['total_pkts']
922 return float(dropped_pkts) / total_pkts * 100
925 """Collect final stats for previous run."""
926 stats = self.gen.get_stats()
927 retDict = {'total_tx_rate': stats['total_tx_rate']}
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']
1112 def log_stats(stats):
1113 """Log estimated stats during run."""
1115 'datetime': str(datetime.now()),
1116 'tx_packets': stats['overall']['tx']['total_pkts'],
1117 'rx_packets': stats['overall']['rx']['total_pkts'],
1118 'drop_packets': stats['overall']['rx']['dropped_pkts'],
1119 'drop_rate_percent': stats['overall']['drop_rate_percent']
1121 LOG.info('TX: %(tx_packets)d; '
1122 'RX: %(rx_packets)d; '
1123 'Est. Dropped: %(drop_packets)d; '
1124 'Est. Drop rate: %(drop_rate_percent).4f%%',
1127 def run_traffic(self):
1128 """Start traffic and return intermediate stats for each interval."""
1129 stats = self.runner.run()
1130 while self.runner.is_running:
1131 self.log_stats(stats)
1133 stats = self.runner.poll_stats()
1136 self.log_stats(stats)
1137 LOG.info('Drop rate: %f', stats['overall']['drop_rate_percent'])
1140 def cancel_traffic(self):
1144 def _get_traffic_config(self):
1149 for idx, rate in enumerate(self.run_config['rates']):
1150 key = 'direction-forward' if idx == 0 else 'direction-reverse'
1152 'l2frame_size': self.run_config['l2frame_size'],
1153 'duration_sec': self.run_config['duration_sec']
1155 config[key].update(rate)
1156 config[key].update(self.__convert_rates(rate))
1157 load_total += float(config[key]['rate_percent'])
1158 bps_total += float(config[key]['rate_bps'])
1159 pps_total += float(config[key]['rate_pps'])
1160 config['direction-total'] = dict(config['direction-forward'])
1161 config['direction-total'].update({
1162 'rate_percent': load_total,
1163 'rate_pps': cast_integer(pps_total),
1164 'rate_bps': bps_total
1169 def get_run_config(self, results):
1170 """Return configuration which was used for the last run."""
1172 # because we want each direction to have the far end RX rates,
1173 # use the far end index (1-idx) to retrieve the RX rates
1174 for idx, key in enumerate(["direction-forward", "direction-reverse"]):
1175 tx_rate = results["stats"][str(idx)]["tx"]["total_pkts"] / self.config.duration_sec
1176 rx_rate = results["stats"][str(1 - idx)]["rx"]["total_pkts"] / self.config.duration_sec
1178 "orig": self.__convert_rates(self.run_config['rates'][idx]),
1179 "tx": self.__convert_rates({'rate_pps': tx_rate}),
1180 "rx": self.__convert_rates({'rate_pps': rx_rate})
1184 for direction in ['orig', 'tx', 'rx']:
1185 total[direction] = {}
1186 for unit in ['rate_percent', 'rate_bps', 'rate_pps']:
1187 total[direction][unit] = sum([float(x[direction][unit]) for x in list(r.values())])
1189 r['direction-total'] = total
1192 def insert_interface_stats(self, pps_list):
1193 """Insert interface stats to a list of packet path stats.
1195 pps_list: a list of packet path stats instances indexed by chain index
1197 This function will insert the packet path stats for the traffic gen ports 0 and 1
1198 with itemized per chain tx/rx counters.
1199 There will be as many packet path stats as chains.
1200 Each packet path stats will have exactly 2 InterfaceStats for port 0 and port 1
1203 PacketPathStats(InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)),
1204 PacketPathStats(InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)),
1208 def get_if_stats(chain_idx):
1209 return [InterfaceStats('p' + str(port), self.tool)
1210 for port in range(2)]
1211 # keep the list of list of interface stats indexed by the chain id
1212 self.ifstats = [get_if_stats(chain_idx)
1213 for chain_idx in range(self.config.service_chain_count)]
1214 # note that we need to make a copy of the ifs list so that any modification in the
1215 # list from pps will not change the list saved in self.ifstats
1216 self.pps_list = [PacketPathStats(list(ifs)) for ifs in self.ifstats]
1217 # insert the corresponding pps in the passed list
1218 pps_list.extend(self.pps_list)
1220 def update_interface_stats(self, diff=False):
1221 """Update all interface stats.
1223 diff: if False, simply refresh the interface stats values with latest values
1224 if True, diff the interface stats with the latest values
1225 Make sure that the interface stats inserted in insert_interface_stats() are updated
1229 [InterfaceStats(chain 0, port 0), InterfaceStats(chain 0, port 1)],
1230 [InterfaceStats(chain 1, port 0), InterfaceStats(chain 1, port 1)],
1235 stats = self.gen.get_stats()
1236 for chain_idx, ifs in enumerate(self.ifstats):
1237 # each ifs has exactly 2 InterfaceStats and 2 Latency instances
1238 # corresponding to the
1239 # port 0 and port 1 for the given chain_idx
1240 # Note that we cannot use self.pps_list[chain_idx].if_stats to pick the
1241 # interface stats for the pps because it could have been modified to contain
1242 # additional interface stats
1243 self.gen.get_stream_stats(stats, ifs, self.pps_list[chain_idx].latencies, chain_idx)
1244 # special handling for vxlan
1245 # in case of vxlan, flow stats are not available so all rx counters will be
1246 # zeros when the total rx port counter is non zero.
1248 for port in range(2):
1250 for ifs in self.ifstats:
1251 total_rx += ifs[port].rx
1253 # check if the total port rx from Trex is also zero
1254 port_rx = stats[port]['rx']['total_pkts']
1256 # the total rx for all chains from port level stats is non zero
1257 # which means that the per-chain stats are not available
1258 if len(self.ifstats) == 1:
1259 # only one chain, simply report the port level rx to the chain rx stats
1260 self.ifstats[0][port].rx = port_rx
1262 for ifs in self.ifstats:
1263 # mark this data as unavailable
1265 # pitch in the total rx only in the last chain pps
1266 self.ifstats[-1][port].rx_total = port_rx
1269 def compare_tx_rates(required, actual):
1270 """Compare the actual TX rate to the required TX rate."""
1272 are_different = False
1274 if float(actual) / required < threshold:
1275 are_different = True
1276 except ZeroDivisionError:
1277 are_different = True
1280 msg = "WARNING: There is a significant difference between requested TX rate ({r}) " \
1281 "and actual TX rate ({a}). The traffic generator may not have sufficient CPU " \
1282 "to achieve the requested TX rate.".format(r=required, a=actual)
1288 def get_per_direction_rate(self):
1289 """Get the rate for each direction."""
1290 divisor = 2 if self.run_config['bidirectional'] else 1
1291 if 'rate_percent' in self.current_total_rate:
1292 # don't split rate if it's percentage
1295 return utils.divide_rate(self.current_total_rate, divisor)
1298 """Close this instance."""
1300 self.gen.stop_traffic()
1303 self.gen.clear_stats()