aboutsummaryrefslogtreecommitdiffstats
path: root/tools/pkt_gen/xena/XenaDriver.py
blob: 04a99e9dc8aa1324dd9812427914214afa138410 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
---
# Sample benchmark task config file measure network latency using ping
# between mutiple virtual machines


schema: "yardstick:task:0.1"

scenarios:
-
  type: Ping
  options:
    packetsize: 200
  host: athena.demo
  # key 'targets' for multiple targets
  targets:
  - ares.demo
  - kratos.demo

  runner:
    type: Duration
    duration: 60
    interval: 1

  sla:
    max_rtt: 10
    action: monitor

context:
  name: demo
  image: cirros-0.3.3
  flavor: m1.tiny
  user: cirros

  servers:
    athena:
      floating_ip: true
    ares:
    kratos:

  networks:
    test:
      cidr: '10.0.1.0/24'
href='#n414'>414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
# Copyright 2016 Red Hat Inc & Xena Networks.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# This is a port of code by Xena and Flavios ported to python 3 compatibility.
# Credit given to Xena and Flavio for providing most of the logic of this code.
# The code has changes for PEP 8 and python 3 conversion. Added Stat classes
# for better scaling of future requirements. Also added calculation functions
# for line rate to align within VSPerf project.
# Flavios xena libraries available at https://github.com/fleitner/XenaPythonLib

# Contributors:
#   Flavio Leitner, Red Hat Inc.
#   Dan Amzulescu, Xena Networks
#   Christian Trautman, Red Hat Inc.

"""
Xena Socket API Driver module for communicating directly with Xena system
through socket commands and returning different statistics.
"""
import locale
import logging
import socket
import struct
import sys
import threading
import time

# Xena Socket Commands
CMD_CLEAR_RX_STATS = 'pr_clear'
CMD_CLEAR_TX_STATS = 'pt_clear'
CMD_COMMENT = ';'
CMD_CREATE_STREAM = 'ps_create'
CMD_DELETE_STREAM = 'ps_delete'
CMD_GET_PORT_SPEED = 'p_speed ?'
CMD_GET_PORT_SPEED_REDUCTION = 'p_speedreduction ?'
CMD_GET_RX_STATS_PER_TID = 'pr_tpldtraffic'
CMD_GET_STREAM_DATA = 'pt_stream'
CMD_GET_STREAMS_PER_PORT = 'ps_indices'
CMD_GET_TID_PER_STREAM = 'ps_tpldid'
CMD_GET_TX_STATS_PER_STREAM = 'pt_stream'
CMD_GET_RX_STATS = 'pr_all ?'
CMD_GET_TX_STATS = 'pt_all ?'
CMD_INTERFRAME_GAP = 'p_interframegap'
CMD_LOGIN = 'c_logon'
CMD_LOGOFF = 'c_logoff'
CMD_OWNER = 'c_owner'
CMD_PORT = ';Port:'
CMD_PORT_IP = 'p_ipaddress'
CMD_RESERVE = 'p_reservation reserve'
CMD_RELEASE = 'p_reservation release'
CMD_RELINQUISH = 'p_reservation relinquish'
CMD_RESET = 'p_reset'
CMD_SET_PORT_ARP_REPLY = 'p_arpreply'
CMD_SET_PORT_ARP_V6_REPLY = 'p_arpv6reply'
CMD_SET_PORT_PING_REPLY = 'p_pingreply'
CMD_SET_PORT_PING_V6_REPLY = 'p_pingv6reply'
CMD_SET_PORT_TIME_LIMIT = 'p_txtimelimit'
CMD_SET_STREAM_HEADER_PROTOCOL = 'ps_headerprotocol'
CMD_SET_STREAM_ON_OFF = 'ps_enable'
CMD_SET_STREAM_PACKET_HEADER = 'ps_packetheader'
CMD_SET_STREAM_PACKET_LENGTH = 'ps_packetlength'
CMD_SET_STREAM_PACKET_LIMIT = 'ps_packetlimit'
CMD_SET_STREAM_PACKET_PAYLOAD = 'ps_payload'
CMD_SET_STREAM_RATE_FRACTION = 'ps_ratefraction'
CMD_SET_STREAM_TEST_PAYLOAD_ID = 'ps_tpldid'
CMD_SET_TPLD_MODE = 'p_tpldmode'
CMD_START_TRAFFIC = 'p_traffic on'
CMD_STOP_TRAFFIC = 'p_traffic off'
CMD_STREAM_MODIFIER = 'ps_modifier'
CMD_STREAM_MODIFIER_COUNT = 'ps_modifiercount'
CMD_STREAM_MODIFIER_RANGE = 'ps_modifierrange'
CMD_VERSION = 'c_versionno ?'

_LOCALE = locale.getlocale()[1]
_LOGGER = logging.getLogger(__name__)


class SimpleSocket(object):
    """
    Socket class
    """
    def __init__(self, hostname, port=5025, timeout=1):
        """Constructor
        :param hostname: hostname or ip as string
        :param port: port number to use for socket as int
        :param timeout: socket timeout as int
        :return: SimpleSocket object
        """
        self.hostname = hostname
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.settimeout(timeout)
            self.sock.connect((hostname, port))
        except socket.error as msg:
            _LOGGER.error(
                "Cannot connect to Xena Socket at %s", hostname)
            _LOGGER.error("Exception : %s", msg)
            sys.exit(1)

    def __del__(self):
        """Deconstructor
        :return:
        """
        self.sock.close()

    def ask(self, cmd):
        """ Send the command over the socket
        :param cmd: cmd as string
        :return: byte utf encoded return value from socket
        """
        cmd += '\n'
        try:
            self.sock.send(cmd.encode('utf-8'))
            return self.sock.recv(1024)
        except OSError:
            return ''

    def read_reply(self):
        """ Get the response from the socket
        :return: Return the reply
        """
        reply = self.sock.recv(1024)
        if reply.find("---^".encode('utf-8')) != -1:
            # read again the syntax error msg
            reply = self.sock.recv(1024)
        return reply

    def send_command(self, cmd):
        """ Send the command specified over the socket
        :param cmd: Command to send as string
        :return: None
        """
        cmd += '\n'
        self.sock.send(cmd.encode('utf-8'))

    def set_keep_alive(self):
        """ Set the keep alive for the socket
        :return: None
        """
        self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)


class KeepAliveThread(threading.Thread):
    """
    Keep alive socket class
    """
    message = ''

    def __init__(self, connection, interval=10):
        """ Constructor
        :param connection: Socket for keep alive
        :param interval: interval in seconds to send keep alive
        :return: KeepAliveThread object
        """
        threading.Thread.__init__(self)
        self.connection = connection
        self.interval = interval
        self.finished = threading.Event()
        self.setDaemon(True)
        _LOGGER.debug(
            'Xena Socket keep alive thread initiated, interval ' +
            '{} seconds'.format(self.interval))

    def stop(self):
        """ Thread stop. See python thread docs for more info
        :return: None
        """
        self.finished.set()
        self.join()

    def run(self):
        """ Thread start. See python thread docs for more info
        :return: None
        """
        while not self.finished.isSet():
            self.finished.wait(self.interval)
            self.connection.ask(self.message)


class XenaSocketDriver(SimpleSocket):
    """
    Xena socket class
    """
    reply_ok = '<OK>'

    def __init__(self, hostname, port=22611):
        """ Constructor
        :param hostname: Hostname or ip as string
        :param port: port to use as int
        :return: XenaSocketDriver object
        """
        SimpleSocket.__init__(self, hostname=hostname, port=port)
        SimpleSocket.set_keep_alive(self)
        self.access_semaphor = threading.Semaphore(1)

    def ask(self, cmd):
        """ Send the command over the socket in a thread safe manner
        :param cmd: Command to send
        :return: reply from socket
        """
        self.access_semaphor.acquire()
        reply = SimpleSocket.ask(self, cmd)
        self.access_semaphor.release()
        return reply

    def ask_verify(self, cmd):
        """ Send the command over the socket in a thread safe manner and
        verify the response is good.
        :param cmd: Command to send
        :return: Boolean True if command response is good, False otherwise
        """
        resp = self.ask(cmd).decode(_LOCALE).strip('\n')
        _LOGGER.info('[ask_verify] %s', resp)
        if resp == self.reply_ok:
            return True
        return False

    def disconnect(self):
        """
        Close the socket connection
        :return: None
        """
        self.sock.close()

    def send_command(self, cmd):
        """ Send the command over the socket with no return
        :param cmd: Command to send
        :return: None
        """
        self.access_semaphor.acquire()
        SimpleSocket.send_command(self, cmd)
        self.access_semaphor.release()

    def send_query_replies(self, cmd):
        """ Send the command over the socket and wait for all replies and return
        the lines as a list
        :param cmd: Command to send
        :return: Response from command as list
        """
        # send the command followed by cmd SYNC to find out
        # when the last reply arrives.
        self.send_command(cmd.strip('\n'))
        self.send_command('SYNC')
        replies = []
        self.access_semaphor.acquire()
        msg = SimpleSocket.read_reply(self).decode(_LOCALE)
        msgleft = ''
        while True:
            if '\n' in msg:
                (reply, msgleft) = msg.split('\n', 1)
                # check for syntax problems
                if reply.rfind('Syntax') != -1:
                    self.access_semaphor.release()
                    return []

                if reply.rfind('<SYNC>') == 0:

                    self.access_semaphor.release()
                    return replies

                replies.append(reply + '\n')
                msg = msgleft
            else:
                # more bytes to come
                msgnew = SimpleSocket.read_reply(self).decode(_LOCALE)
                msg = msgleft + msgnew


class XenaManager(object):
    """
    Manager class for port and socket functions
    """
    def __init__(self, socketDriver, user='', password='xena'):
        """Constructor

        Establish a connection to Xena using a ``driver`` with the ``password``
        supplied.

        Attributes:
        :param socketDriver: XenaSocketDriver connection object
        :param password: Password to the Xena traffic generator
        :returns: XenaManager object
        """
        self.driver = socketDriver
        self.ports = list()
        self.keep_alive_thread = KeepAliveThread(self.driver)

        if self.logon(password):
            _LOGGER.info('Connected to Xena at %s', self.driver.hostname)
        else:
            _LOGGER.error('Failed to logon to Xena at %s', self.driver.hostname)
            return

        self.set_owner(user)

    def disconnect(self):
        """ Release ports and disconnect from chassis.
        """
        for module_port in self.ports:
            module_port.release_port()
        self.ports = []
        self.logoff()
        self.keep_alive_thread.stop()

    def add_module_port(self, module, port):
        """Factory for Xena Ports

        :param module: String or int of module
        :param port: String or int of port
        :return: XenaPort object if success, None if port already added
        """
        xenaport = XenaPort(self, module, port)
        if xenaport in self.ports:
            return None
        else:
            self.ports.append(xenaport)
            return xenaport

    def get_module_port(self, module, port):
        """Return the Xena Port object if available
        :param module: module number as int or str
        :param port: port number as int or str
        :return: XenaPort object or None if not found
        """
        for por in self.ports:
            if por.port == str(port) and por.module == str(module):
                return por
        return None

    def get_version(self):
        """
        Get the version from the chassis
        :return: versions of server and driver as string
        """
        res = self.driver.ask(make_manager_command(
            CMD_VERSION, '')).decode(_LOCALE)
        res = res.rstrip('\n').split()
        return "Server: {} Driver: {}".format(res[1], res[2])

    def logoff(self):
        """
        Logoff from the Xena chassis
        :return: Boolean True if response OK, False if error.
        """
        return self.driver.ask_verify(make_manager_command(CMD_LOGOFF))

    def logon(self, password):
        """Login to the Xena traffic generator using the ``password`` supplied.

        :param password: string of password
        :return: Boolean True if response OK, False if error.
        """
        self.keep_alive_thread.start()
        return self.driver.ask_verify(make_manager_command(CMD_LOGIN, password))

    def set_owner(self, username):
        """Set the ports owner.
        :return: Boolean True if response OK, False if error.
        """
        return self.driver.ask_verify(make_manager_command(CMD_OWNER, username))


class XenaPort(object):
    """
    Xena Port emulator class
    """
    def __init__(self, manager, module, port):
        """Constructor

        :param manager: XenaManager object
        :param module: Module as string or int of module to use
        :param port: Port as string or int of port to use
        :return: XenaPort object
        """
        self._manager = manager
        self._module = str(module)
        self._port = str(port)
        self._streams = list()

    @property
    def manager(self):
        """Property for manager attribute
        :return: manager object
        """
        return self._manager

    @property
    def module(self):
        """Property for module attribute
        :return: module value as string
        """
        return self._module

    @property
    def port(self):
        """Property for port attribute
        :return: port value as string
        """
        return self._port

    def port_string(self):
        """String builder with attributes
        :return: String of module port for command sequence
        """
        stringify = "{}/{}".format(self._module, self._port)
        return stringify

    def add_stream(self):
        """Add a stream to the port.
        :return: XenaStream object, None if failure
        """
        identifier = len(self._streams)
        stream = XenaStream(self, identifier)
        if self._manager.driver.ask_verify(make_stream_command(
                CMD_CREATE_STREAM, '', stream)):
            self._streams.append(stream)
            return stream
        else:
            _LOGGER.error("Error during stream creation")
            return None

    def clear_stats(self, rx_clear=True, tx_clear=True):
        """Clear the port stats

        :param rx_clear: Boolean if rx stats are to be cleared
        :param tx_clear: Boolean if tx stats are to be cleared
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_CLEAR_RX_STATS, self)
        res1 = self._manager.driver.ask_verify(command) if rx_clear else True
        command = make_port_command(CMD_CLEAR_TX_STATS, self)
        res2 = self._manager.driver.ask_verify(command) if tx_clear else True
        return all([res1, res2])

    def get_effective_speed(self):
        """
        Get the effective speed on the port
        :return: effective speed as float
        """
        port_speed = self.get_port_speed()
        reduction = self.get_port_speed_reduction()
        effective_speed = port_speed * (1.0 - reduction / 1000000.0)
        return effective_speed

    def get_inter_frame_gap(self):
        """
        Get the interframe gap and return it as string
        :return: integer of interframe gap
        """
        command = make_port_command(CMD_INTERFRAME_GAP + '?', self)
        res = self._manager.driver.ask(command).decode(_LOCALE)
        res = int(res.rstrip('\n').split(' ')[-1])
        return res

    def get_port_speed(self):
        """
        Get the port speed as bits from port and return it as a int.
        :return: Int of port speed
        """
        command = make_port_command(CMD_GET_PORT_SPEED, self)
        res = self._manager.driver.ask(command).decode(_LOCALE)
        port_speed = res.split(' ')[-1].rstrip('\n')
        return int(port_speed) * 1000000

    def get_port_speed_reduction(self):
        """
        Get the port speed reduction value as int
        :return: Integer of port speed reduction value
        """
        command = make_port_command(CMD_GET_PORT_SPEED_REDUCTION, self)
        res = self._manager.driver.ask(command).decode(_LOCALE)
        res = int(res.rstrip('\n').split(' ')[-1])
        return res

    def get_rx_stats(self):
        """Get the rx stats and return the data as a dict.
        :return: Receive stats as dictionary
        """
        command = make_port_command(CMD_GET_RX_STATS, self)
        rx_data = self._manager.driver.send_query_replies(command)
        data = XenaRXStats(rx_data, time.time())
        return data

    def get_tx_stats(self):
        """Get the tx stats and return the data as a dict.
        :return: Receive stats as dictionary
        """
        command = make_port_command(CMD_GET_TX_STATS, self)
        tx_data = self._manager.driver.send_query_replies(command)
        data = XenaTXStats(tx_data, time.time())
        return data

    def micro_tpld_disable(self):
        """Disable micro TPLD and return to standard payload size
        :return: Boolean if response OK, False if error
        """
        command = make_port_command(CMD_SET_TPLD_MODE + ' normal', self)
        return self._manager.driver.ask_verify(command)

    def micro_tpld_enable(self):
        """Enable micro TPLD 6 byte payloads.
        :Return Boolean if response OK, False if error
        """
        command = make_port_command(CMD_SET_TPLD_MODE + ' micro', self)
        return self._manager.driver.ask_verify(command)

    def release_port(self):
        """Release the port
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_RELEASE, self)
        return self._manager.driver.ask_verify(command)

    def reserve_port(self):
        """Reserve the port
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_RESERVE, self)
        return self._manager.driver.ask_verify(command)

    def reset_port(self):
        """Reset the port
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_RESET, self)
        return self._manager.driver.ask_verify(command)

    def set_port_arp_reply(self, on=True, v6=False):
        """
        Set the port arpreply value
        :param on: Enable or disable the arp reply on the port
        :param v6: set the value on the ip v6, disabled will set at ip v4
        :return: Boolean True if response OK, False if error
        """
        command = make_port_command('{} {}'.format(
            CMD_SET_PORT_ARP_V6_REPLY if v6 else CMD_SET_PORT_ARP_REPLY,
            "on" if on else "off"), self)
        return self._manager.driver.ask_verify(command)

    def set_port_ping_reply(self, on=True, v6=False):
        """
        Set the port ping reply value
        :param on: Enable or disable the ping reply on the port
        :param v6: set the value on the ip v6, disabled will set at ip v4
        :return: Boolean True if response OK, False if error
        """
        command = make_port_command('{} {}'.format(
            CMD_SET_PORT_PING_V6_REPLY if v6 else CMD_SET_PORT_PING_REPLY,
            "on" if on else "off"), self)
        return self._manager.driver.ask_verify(command)

    def set_port_ip(self, ip_addr, cidr, gateway, wild='255'):
        """
        Set the port ip address of the specific port
        :param ip_addr: IP address to set to port
        :param cidr: cidr number for the subnet
        :param gateway: Gateway ip for port
        :param wild: wildcard used for ARP and PING replies
        :return: Boolean True if response OK, False if error
        """
        # convert the cidr to a dot notation subnet address
        subnet = socket.inet_ntoa(
            struct.pack(">I", (0xffffffff << (32 - cidr)) & 0xffffffff))

        command = make_port_command('{} {} {} {} 0.0.0.{}'.format(
            CMD_PORT_IP, ip_addr, subnet, gateway, wild), self)
        return self._manager.driver.ask_verify(command)

    def set_port_time_limit(self, micro_seconds):
        """Set the port time limit in ms
        :param micro_seconds: ms for port time limit
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command('{} {}'.format(
            CMD_SET_PORT_TIME_LIMIT, micro_seconds), self)
        return self._manager.driver.ask_verify(command)

    def traffic_off(self):
        """Start traffic
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_STOP_TRAFFIC, self)
        return self._manager.driver.ask_verify(command)

    def traffic_on(self):
        """Stop traffic
        :return: Boolean True if response OK, False if error.
        """
        command = make_port_command(CMD_START_TRAFFIC, self)
        return self._manager.driver.ask_verify(command)


class XenaStream(object):
    """
    Xena stream emulator class
    """
    def __init__(self, xenaPort, streamID):
        """Constructor

        :param xenaPort: XenaPort object
        :param streamID: Stream ID as int or string
        :return: XenaStream object
        """
        self._xena_port = xenaPort
        self._stream_id = str(streamID)
        self._manager = self._xena_port.manager
        self._header_protocol = None

    @property
    def xena_port(self):
        """Property for port attribute
        :return: XenaPort object
        """
        return self._xena_port

    @property
    def stream_id(self):
        """Property for streamID attribute
        :return: streamID value as string
        """
        return self._stream_id

    def enable_multistream(self, flows, layer):
        """
        Basic implementation of multi stream. Enable multi stream by setting
        modifiers on the stream
        :param flows: Numbers of flows or end range
        :param layer: layer to enable multi stream as str. Acceptable values
        are L2, L3, or L4
        :return: True if success False otherwise
        """
        if not self._header_protocol:
            raise RuntimeError(
                "Please set a protocol header before calling this method.")

        # byte offsets for setting the modifier
        offsets = {
            'L2': [0, 6],
            'L3': [32, 36] if 'VLAN' in self._header_protocol else [28, 32],
            'L4': [38, 40] if 'VLAN' in self._header_protocol else [34, 36]
        }

        responses = list()
        if layer in offsets.keys() and flows > 0:
            command = make_port_command(
                CMD_STREAM_MODIFIER_COUNT + ' [{}]'.format(self._stream_id) +
                ' 2', self._xena_port)
            responses.append(self._manager.driver.ask_verify(command))
            command = make_port_command(
                CMD_STREAM_MODIFIER + ' [{},0] {} 0xFFFF0000 INC 1'.format(
                    self._stream_id, offsets[layer][0]), self._xena_port)
            responses.append(self._manager.driver.ask_verify(command))
            command = make_port_command(
                CMD_STREAM_MODIFIER_RANGE + ' [{},0] 0 1 {}'.format(
                    self._stream_id, flows), self._xena_port)
            responses.append(self._manager.driver.ask_verify(command))
            command = make_port_command(
                CMD_STREAM_MODIFIER + ' [{},1] {} 0xFFFF0000 INC 1'.format(
                    self._stream_id, offsets[layer][1]), self._xena_port)
            responses.append(self._manager.driver.ask_verify(command))
            command = make_port_command(
                CMD_STREAM_MODIFIER_RANGE + ' [{},1] 0 1 {}'.format(
                    self._stream_id, flows), self._xena_port)
            responses.append(self._manager.driver.ask_verify(command))
            return all(responses)  # return True if they all worked
        elif flows < 1:
            _LOGGER.warning(
                'No flows specified in enable multistream. Bypassing...')
            return False
        else:
            raise NotImplementedError(
                "Non-implemented stream layer in method enable multistream ",
                "layer=", layer)

    def get_stream_data(self):
        """
        Get the response for stream data
        :return: String of response for stream data info
        """
        command = make_stream_command(CMD_GET_STREAM_DATA, '?', self)
        res = self._manager.driver.ask(command).decode(_LOCALE)
        return res

    def set_header_protocol(self, protocol_header):
        """Set the header info for the packet header hex.
        If the packet header contains just Ethernet and IP info then call this
        method with ETHERNET IP as the protocol header.

        :param protocol_header: protocol header argument
        :return: Boolean True if success, False if error
        """
        command = make_stream_command(
            CMD_SET_STREAM_HEADER_PROTOCOL,
            protocol_header, self)
        if self._manager.driver.ask_verify(command):
            self._header_protocol = protocol_header
            return True
        else:
            return False

    def set_off(self):
        """Set the stream to off
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_ON_OFF, 'off', self))

    def set_on(self):
        """Set the stream to on
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_ON_OFF, 'on', self))

    def set_packet_header(self, header):
        """Set the stream packet header

        :param header: packet header as hex bytes
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_PACKET_HEADER, header, self))

    def set_packet_length(self, pattern_type, minimum, maximum):
        """Set the pattern length with min and max values based on the pattern
        type supplied

        :param pattern_type: String of pattern type, valid entries [ fixed,
         butterfly, random, mix, incrementing ]
        :param minimum: integer of minimum byte value
        :param maximum: integer of maximum byte value
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_PACKET_LENGTH, '{} {} {}'.format(
                pattern_type, minimum, maximum), self))

    def set_packet_limit(self, limit):
        """Set the packet limit

        :param limit: number of packets that will be sent, use -1 to disable
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_PACKET_LIMIT, limit, self))

    def set_packet_payload(self, payload_type, hex_value):
        """Set the payload to the hex value based on the payload type

        :param payload_type: string of the payload type, valid entries [ pattern,
         incrementing, prbs ]
        :param hex_value: hex string of valid hex
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_PACKET_PAYLOAD, '{} {}'.format(
                payload_type, hex_value), self))

    def set_rate_fraction(self, fraction):
        """Set the rate fraction

        :param fraction: fraction for the stream
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_RATE_FRACTION, fraction, self))

    def set_payload_id(self, identifier):
        """ Set the test payload ID
        :param identifier: ID as int or string
        :return: Boolean True if success, False if error
        """
        return self._manager.driver.ask_verify(make_stream_command(
            CMD_SET_STREAM_TEST_PAYLOAD_ID, identifier, self))


class XenaRXStats(object):
    """
    Receive stat class
    """
    def __init__(self, stats, epoc):
        """ Constructor
        :param stats: Stats from pr all command as list
        :param epoc: Current time in epoc
        :return: XenaRXStats object
        """
        self._stats = stats
        self._time = epoc
        self.data = self.parse_stats()
        self.preamble = 8

    @staticmethod
    def _pack_stats(param, start, fields=None):
        """ Pack up the list of stats in a dictionary
        :param param: The list of params to process
        :param start: What element to start at
        :param fields: The field names to pack as keys
        :return: Dictionary of data where fields match up to the params
        """
        if not fields:
            fields = ['bps', 'pps', 'bytes', 'packets']
        data = {}
        i = 0
        for column in fields:
            data[column] = int(param[start + i])
            i += 1

        return data

    @staticmethod
    def _pack_tplds_stats(param, start):
        """ Pack up the tplds stats
        :param param: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        data = {}
        i = 0
        for val in range(start, len(param) - start):
            data[i] = int(param[val])
            i += 1
        return data

    def _pack_rxextra_stats(self, param, start):
        """ Pack up the extra stats
        :param param: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        fields = ['fcserrors', 'pauseframes', 'arprequests', 'arpreplies',
                  'pingrequests', 'pingreplies', 'gapcount', 'gapduration']
        return self._pack_stats(param, start, fields)

    def _pack_tplderrors_stats(self, param, start):
        """ Pack up tlpd errors
        :param param: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        fields = ['dummy', 'seq', 'mis', 'pld']
        return self._pack_stats(param, start, fields)

    def _pack_tpldlatency_stats(self, param, start):
        """ Pack up the tpld latency stats
        :param param: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        fields = ['min', 'avg', 'max', '1sec']
        return self._pack_stats(param, start, fields)

    def _pack_tpldjitter_stats(self, param, start):
        """ Pack up the tpld jitter stats
        :param param: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        fields = ['min', 'avg', 'max', '1sec']
        return self._pack_stats(param, start, fields)

    @property
    def time(self):
        """
        :return: Time as String of epoc of when stats were collected
        """
        return self._time

    def parse_stats(self):
        """ Parse the stats from pr all command
        :return: Dictionary of all stats
        """
        statdict = {}
        for line in self._stats:
            param = line.split()
            if param[1] == 'PR_TOTAL':
                statdict['pr_total'] = self._pack_stats(param, 2)
            elif param[1] == 'PR_NOTPLD':
                statdict['pr_notpld'] = self._pack_stats(param, 2,)
            elif param[1] == 'PR_EXTRA':
                statdict['pr_extra'] = self._pack_rxextra_stats(param, 2)
            elif param[1] == 'PT_STREAM':
                entry_id = "pt_stream_%s" % param[2].strip('[]')
                statdict[entry_id] = self._pack_stats(param, 3)
            elif param[1] == 'PR_TPLDS':
                tid_list = self._pack_tplds_stats(param, 2)
                if len(tid_list):
                    statdict['pr_tplds'] = tid_list
            elif param[1] == 'PR_TPLDTRAFFIC':
                if 'pr_tpldstraffic' in statdict:
                    data = statdict['pr_tpldstraffic']
                else:
                    data = {}
                entry_id = param[2].strip('[]')
                data[entry_id] = self._pack_stats(param, 3)
                statdict['pr_tpldstraffic'] = data
            elif param[1] == 'PR_TPLDERRORS':
                if 'pr_tplderrors' in statdict:
                    data = statdict['pr_tplderrors']
                else:
                    data = {}
                entry_id = param[2].strip('[]')
                data[entry_id] = self._pack_tplderrors_stats(param, 3)
                statdict['pr_tplderrors'] = data
            elif param[1] == 'PR_TPLDLATENCY':
                if 'pr_tpldlatency' in statdict:
                    data = statdict['pr_tpldlatency']
                else:
                    data = {}
                entry_id = param[2].strip('[]')
                data[entry_id] = self._pack_tpldlatency_stats(param, 3)
                statdict['pr_tpldlatency'] = data
            elif param[1] == 'PR_TPLDJITTER':
                if 'pr_tpldjitter' in statdict:
                    data = statdict['pr_tpldjitter']
                else:
                    data = {}
                entry_id = param[2].strip('[]')
                data[entry_id] = self._pack_tpldjitter_stats(param, 3)
                statdict['pr_pldjitter'] = data
            elif param[1] == 'PR_FILTER':
                if 'pr_filter' in statdict:
                    data = statdict['pr_filter']
                else:
                    data = {}
                entry_id = param[2].strip('[]')
                data[entry_id] = self._pack_stats(param, 3)
                statdict['pr_filter'] = data
            elif param[1] == 'P_RECEIVESYNC':
                if param[2] == 'IN_SYNC':
                    statdict['p_receivesync'] = {'IN SYNC': 'True'}
                else:
                    statdict['p_receivesync'] = {'IN SYNC': 'False'}
            else:
                logging.warning("XenaPort: unknown stats: %s", param[1])

        mydict = statdict
        return mydict


class XenaTXStats(object):
    """
    Xena transmit stat class
    """
    def __init__(self, stats, epoc):
        """ Constructor
        :param stats: Stats from pt all command as list
        :param epoc: Current time in epoc
        :return: XenaTXStats object
        """
        self._stats = stats
        self._time = epoc
        self._ptstreamkeys = list()
        self.data = self.parse_stats()
        self.preamble = 8

    @staticmethod
    def _pack_stats(params, start, fields=None):
        """ Pack up the list of stats in a dictionary
        :param params: The list of params to process
        :param start: What element to start at
        :param fields: The field names to pack as keys
        :return: Dictionary of data where fields match up to the params
        """
        if not fields:
            fields = ['bps', 'pps', 'bytes', 'packets']
        data = {}
        i = 0
        for column in fields:
            data[column] = int(params[start + i])
            i += 1

        return data

    def _pack_txextra_stats(self, params, start):
        """ Pack up the tx extra stats
        :param params: List of params to pack
        :param start: What element to start at
        :return: Dictionary of stats
        """
        fields = ['arprequests', 'arpreplies', 'pingrequests', 'pingreplies',
                  'injectedfcs', 'injectedseq', 'injectedmis', 'injectedint',
                  'injectedtid', 'training']
        return self._pack_stats(params, start, fields)

    @property
    def pt_stream_keys(self):
        """
        :return: Return a list of pt_stream_x stream key ids
        """
        return self._ptstreamkeys

    @property
    def time(self):
        """
        :return: Time as String of epoc of when stats were collected
        """
        return self._time

    def parse_stats(self):
        """ Parse the stats from pr all command
        :return: Dictionary of all stats
        """
        statdict = {}
        for line in self._stats:
            param = line.split()
            if param[1] == 'PT_TOTAL':
                statdict['pt_total'] = self._pack_stats(param, 2)
            elif param[1] == 'PT_NOTPLD':
                statdict['pt_notpld'] = self._pack_stats(param, 2,)
            elif param[1] == 'PT_EXTRA':
                statdict['pt_extra'] = self._pack_txextra_stats(param, 2)
            elif param[1] == 'PT_STREAM':
                entry_id = "pt_stream_%s" % param[2].strip('[]')
                self._ptstreamkeys.append(entry_id)
                statdict[entry_id] = self._pack_stats(param, 3)
            else:
                logging.warning("XenaPort: unknown stats: %s", param[1])
        mydict = statdict
        return mydict

def aggregate_stats(stat1, stat2):
    """
    Judge whether stat1 and stat2 both have same key, if both have same key, 
    call the aggregate fuction, else use the stat1's value
    """
    newstat = dict()
    for keys in stat1.keys():
        if keys in stat2 and isinstance(stat1[keys], dict):
            newstat[keys] = aggregate(stat1[keys], stat2[keys])
        else:
            newstat[keys] = stat1[keys]
    return newstat

def aggregate(stat1, stat2):
    """
    Recursive function to aggregate two sets of statistics. This is used when
    bi directional traffic is done and statistics need to be calculated based
    on two sets of statistics.
    :param stat1: One set of dictionary stats from RX or TX stats
    :param stat2: Second set of dictionary stats from RX or TX stats
    :return: stats for data entry in RX or TX Stats instance
    """
    newstat = dict()
    for (keys1, keys2) in zip(stat1.keys(), stat2.keys()):
        if isinstance(stat1[keys1], dict):
            newstat[keys1] = aggregate(stat1[keys1], stat2[keys2])
        else:
            if not isinstance(stat1[keys1], int) and not isinstance(
                    [keys1], float):
                # its some value we don't need to aggregate
                return stat1[keys1]
            # for latency stats do the appropriate calculation
            if keys1 == 'max':
                newstat[keys1] = max(stat1[keys1], stat2[keys2])
            elif keys1 == 'min':
                newstat[keys1] = min(stat1[keys1], stat2[keys2])
            elif keys1 == 'avg':
                newstat[keys1] = (stat1[keys1] + stat2[keys2]) / 2
            else:
                newstat[keys1] = (stat1[keys1] + stat2[keys2])
    return newstat


def line_percentage(port, stats, time_active, packet_size):
    """
    Calculate the line percentage rate from the duration, port object and stat
    object.
    :param port: XenaPort object
    :param stats: Xena RXStat or TXStat object
    :param time_active: time the stream was active in secs as int
    :param packet_size: packet size as int
    :return: line percentage as float
    """
    # this is ugly, but its prettier than calling the get method 3 times...
    try:
        packets = stats.data['pr_total']['packets']
    except KeyError:
        try:
            packets = stats.data['pt_total']['packets']
        except KeyError:
            _LOGGER.error(
                'Could not calculate line rate because packet stat not found.')
            return 0
    ifg = port.get_inter_frame_gap()
    pps = packets_per_second(packets, time_active)
    l2br = l2_bit_rate(packet_size, stats.preamble, pps)
    l1br = l1_bit_rate(l2br, pps, ifg, stats.preamble)
    return 100.0 * l1br / port.get_effective_speed()


def l2_bit_rate(packet_size, preamble, pps):
    """
    Return the l2 bit rate
    :param packet_size: packet size on the line in bytes
    :param preamble: preamble size of the packet header in bytes
    :param pps: packets per second
    :return: l2 bit rate as float
    """
    return (packet_size * preamble) * pps


def l1_bit_rate(l2br, pps, ifg, preamble):
    """
    Return the l1 bit rate
    :param l2br: l2 bit rate int bits per second
    :param pps: packets per second
    :param ifg: the inter frame gap
    :param preamble: preamble size of the packet header in bytes
    :return: l1 bit rate as float
    """
    return l2br + (pps * ifg * preamble)


def make_manager_command(cmd, argument=None):
    """ String builder for Xena socket commands

    :param cmd: Command to send
    :param argument: Arguments for command to send
    :return: String of command
    """
    command = '{} "{}"'.format(cmd, argument) if argument else cmd
    _LOGGER.info("[Command Sent] : %s", command)
    return command


def make_port_command(cmd, xena_port):
    """ String builder for Xena port commands

    :param cmd: Command to send
    :param xena_port: XenaPort object
    :return: String of command
    """
    command = "{} {}".format(xena_port.port_string(), cmd)
    _LOGGER.info("[Command Sent] : %s", command)
    return command


def make_stream_command(cmd, args, xena_stream):
    """ String builder for Xena port commands

    :param cmd: Command to send
    :param xena_stream: XenaStream object
    :return: String of command
    """
    command = "{} {} [{}] {}".format(xena_stream.xena_port.port_string(), cmd,
                                     xena_stream.stream_id, args)
    _LOGGER.info("[Command Sent] : %s", command)
    return command


def packets_per_second(packets, time_in_sec):
    """
    Return the pps as float
    :param packets: total packets
    :param time_in_sec: time in seconds
    :return: float of pps
    """
    return packets / time_in_sec