[WIP]: VSPERF-Xtesting Integration Support. 96/71496/7
authoropensource-tnbt <sridhar.rao@spirent.com>
Wed, 25 Nov 2020 14:00:04 +0000 (19:30 +0530)
committeropensource-tnbt <sridhar.rao@spirent.com>
Sat, 28 Nov 2020 13:21:49 +0000 (18:51 +0530)
This patch adds support for integrationg with Xtesting project

There are two variations - baremetal and openstack.
Update-1: Add Download-result feature to baremetal
Update-2: Update Dockerfile following successful compilations.
Update-3: Add Baremetal-Advanced to Migrate to driver version
Update-4: Remove Bashfeature approach of Baremetal.
Update-5: Update documentation, Baremetal container name and openstack.
Update-6: Update Openstack after successful testing

Signed-off-by: Sridhar K. N. Rao <sridhar.rao@spirent.com>
Change-Id: Idcf9f533a0cc6760ca3e56355e2ff60d41616f0f

22 files changed:
docs/index.rst
docs/xtesting/index.rst [new file with mode: 0644]
docs/xtesting/vsperf-xtesting.png [new file with mode: 0755]
xtesting/baremetal/Dockerfile [new file with mode: 0644]
xtesting/baremetal/exceptions.py [new file with mode: 0644]
xtesting/baremetal/requirements.txt [new file with mode: 0644]
xtesting/baremetal/setup.cfg [new file with mode: 0644]
xtesting/baremetal/setup.py [new file with mode: 0644]
xtesting/baremetal/site.yml [new file with mode: 0644]
xtesting/baremetal/ssh.py [new file with mode: 0644]
xtesting/baremetal/testcases.yaml [new file with mode: 0644]
xtesting/baremetal/utils.py [new file with mode: 0644]
xtesting/baremetal/vsperf.conf [new file with mode: 0644]
xtesting/baremetal/vsperf_controller.py [new file with mode: 0644]
xtesting/openstack/Dockerfile [new file with mode: 0644]
xtesting/openstack/cloud.rc [new file with mode: 0644]
xtesting/openstack/setup.cfg [new file with mode: 0644]
xtesting/openstack/setup.py [new file with mode: 0644]
xtesting/openstack/site.yml [new file with mode: 0644]
xtesting/openstack/testcases.yaml [new file with mode: 0644]
xtesting/openstack/vsperfostack.conf [new file with mode: 0644]
xtesting/openstack/vsperfostack.py [new file with mode: 0755]

index d688c75..c8a400f 100644 (file)
@@ -20,4 +20,5 @@ OPNFV Vswitchperf
    lma/index
    openstack/index
    k8s/index
+   xtesting/index
 
diff --git a/docs/xtesting/index.rst b/docs/xtesting/index.rst
new file mode 100644 (file)
index 0000000..9259a12
--- /dev/null
@@ -0,0 +1,85 @@
+.. This work is licensed under a Creative Commons Attribution 4.0 International License.
+.. http://creativecommons.org/licenses/by/4.0
+.. (c) OPNFV, Spirent, AT&T, Ixia  and others.
+
+.. OPNFV VSPERF Documentation master file.
+
+********************************
+OPNFV VSPERF with OPNFV Xtesting
+********************************
+
+============
+Introduction
+============
+User can use VSPERF with Xtesting for two different usecases.
+
+1. Baremetal Dataplane Testing/Benchmarking.
+2. Openstack Dataplane Testing/Benchmarking.
+
+The Baremetal usecase is the legacy usecase of OPNFV VSPERF.
+
+The below figure summarizes both the usecases.
+
+.. image:: ./vsperf-xtesting.png
+  :width: 400
+
+===========
+How to Use?
+===========
+
+Step-1: Build the container
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Go the xtesting/baremetal or xtesting/openstack and run the following command.
+
+.. code-block:: console
+
+  docker build -t 127.0.0.1:5000/vsperfbm
+
+
+Step-2: Install and run Xtesting Playbook
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+These commands are described in OPNFV Xtesting Documentation. Please refere to OPNFV Xtesting wiki for description of these commands.
+
+.. code-block:: console
+
+  virtualenv xtesting
+  . xtesting/bin/activate
+  ansible-galaxy install collivier.xtesting
+  ansible-playbook site.yml
+
+======================
+Accessing the Results?
+======================
+
+VSPERF automatically publishes the results to any OPNFV Testapi deployment.
+User has to configure following two parameters in VSPERF.
+
+1. OPNFVPOD - The name of the pod.
+2. OPNFV_URL - The endpoint serving testapi.
+
+As Xtesting runs its own testapi, user should point to this (testapi endpoint of Xtesting) using the above two configuration.
+
+The above two configurations should be done wherever VSPERF is running (refer to the figure above)
+
+NOTE: Before running the test, it would help if user can prepre the testapi of Xtesting (if needed). The preparation include setting up the following:
+
+1. Projects
+2. Testcases.
+3. Pods.
+
+Please refer to the documentation of testapi for more details.
+
+=======================================
+Accessing other components of Xtesting?
+=======================================
+
+Please refer to the documentation of Xtesting in OPNFV Wiki.
+
+===========
+Limitations
+===========
+For Jerma Release, following limitations apply:
+
+1. For both baremetal and openstack, only phy2phy_tput testcase is supported.
+2. For openstack, only Spirent's STCv and Keysight's Ixnet-Virtual is supported.  
diff --git a/docs/xtesting/vsperf-xtesting.png b/docs/xtesting/vsperf-xtesting.png
new file mode 100755 (executable)
index 0000000..64cad72
Binary files /dev/null and b/docs/xtesting/vsperf-xtesting.png differ
diff --git a/xtesting/baremetal/Dockerfile b/xtesting/baremetal/Dockerfile
new file mode 100644 (file)
index 0000000..b78594b
--- /dev/null
@@ -0,0 +1,36 @@
+# Copyright 2020 Spirent Communications.
+#
+# 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.
+
+FROM opnfv/xtesting
+LABEL maintainer="sridhar.rao@spirent.com"
+
+ADD . /src/
+RUN apk add --no-cache --update --virtual .build-deps python3 \
+    py3-pip py3-wheel git python3-dev linux-headers libffi-dev \
+    make openssl-dev gcc musl-dev && \
+    pip3 install --upgrade pip chainmap oslo.utils \
+    paramiko scp && \
+    git init /src && pip3 install /src
+
+ENV DUT_IP_ADDRESS=10.10.120.24
+ENV DUT_USERNAME=opnfv
+ENV DUT_PASSWORD=opnfv
+ENV VSPERF_TESTS=phy2phy_tput
+ENV VSPERF_CONFFILE=/vsperf.conf
+ENV RES_PATH=/tmp
+ENV VSPERF_TRAFFICGEN_MODE=NO
+
+COPY vsperf.conf /vsperf.conf
+COPY testcases.yaml /usr/lib/python3.8/site-packages/xtesting/ci/testcases.yaml
+CMD ["run_tests", "-t", "all"]
diff --git a/xtesting/baremetal/exceptions.py b/xtesting/baremetal/exceptions.py
new file mode 100644 (file)
index 0000000..c4e0e09
--- /dev/null
@@ -0,0 +1,65 @@
+"""
+# Copyright (c) 2017 Intel Corporation
+#
+# 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.
+"""
+#pylint: disable=import-error
+from oslo_utils import excutils
+
+
+class VsperfCException(Exception):
+    """Base VSPERF-C Exception.
+
+    To correctly use this class, inherit from it and define
+    a 'message' property. That message will get printf'd
+    with the keyword arguments provided to the constructor.
+
+    Based on NeutronException class.
+    """
+    message = "An unknown exception occurred."
+
+    def __init__(self, **kwargs):
+        try:
+            super(VsperfCException, self).__init__(self.message % kwargs)
+            self.msg = self.message % kwargs
+        except Exception:  # pylint: disable=broad-except
+            with excutils.save_and_reraise_exception() as ctxt:
+                if not self.use_fatal_exceptions():
+                    ctxt.reraise = False
+                    # at least get the core message out if something happened
+                    super(VsperfCException, self).__init__(self.message)
+
+    def __str__(self):
+        return self.msg
+
+    def use_fatal_exceptions(self):
+        """Is the instance using fatal exceptions.
+
+        :returns: Always returns False.
+        """ #pylint: disable=no-self-use
+        return False
+
+
+class InvalidType(VsperfCException):
+    """Invalid type"""
+    message = 'Type "%(type_to_convert)s" is not valid'
+
+
+class SSHError(VsperfCException):
+    """ssh error"""
+    message = '%(error_msg)s'
+
+
+class SSHTimeout(SSHError):
+    """ssh timeout""" #pylint: disable=unnecessary-pass
+    pass
diff --git a/xtesting/baremetal/requirements.txt b/xtesting/baremetal/requirements.txt
new file mode 100644 (file)
index 0000000..f2da6ad
--- /dev/null
@@ -0,0 +1,2 @@
+xtesting
+requests!=2.20.0,!=2.24.0 # Apache-2.0
diff --git a/xtesting/baremetal/setup.cfg b/xtesting/baremetal/setup.cfg
new file mode 100644 (file)
index 0000000..9ca3823
--- /dev/null
@@ -0,0 +1,10 @@
+[metadata]
+name = vsperf
+version = 1
+[files]
+packages = .
+[entry_points]
+xtesting.testcase =
+   vsperf_controller = vsperf_controller:VsperfBm
diff --git a/xtesting/baremetal/setup.py b/xtesting/baremetal/setup.py
new file mode 100644 (file)
index 0000000..fa9d59a
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+# pylint: disable=missing-docstring
+import setuptools
+setuptools.setup(
+    setup_requires=['pbr>=2.0.0'],
+    pbr=True)
diff --git a/xtesting/baremetal/site.yml b/xtesting/baremetal/site.yml
new file mode 100644 (file)
index 0000000..06f8c2e
--- /dev/null
@@ -0,0 +1,13 @@
+---
+- hosts:
+    - 127.0.0.1
+  roles:
+      - role: collivier.xtesting
+        project: vsperf
+        repo: 127.0.0.1
+        dport: 5000
+        gerrit:
+        suites:
+            - container: vsperfbm
+              tests:
+                  - phy2phy_tput
diff --git a/xtesting/baremetal/ssh.py b/xtesting/baremetal/ssh.py
new file mode 100644 (file)
index 0000000..ce560c4
--- /dev/null
@@ -0,0 +1,546 @@
+# Copyright 2020: Mirantis Inc.
+# All Rights Reserved.
+#
+#    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.
+#pylint: disable=I,C,R,locally-disabled
+#pylint: disable=import-error,arguments-differ
+
+# this is a modified copy of rally/rally/common/sshutils.py
+
+"""High level ssh library.
+
+Usage examples:
+
+Execute command and get output:
+
+    ssh = sshclient.SSH("root", "example.com", port=33)
+    status, stdout, stderr = ssh.execute("ps ax")
+    if status:
+        raise Exception("Command failed with non-zero status.")
+    print(stdout.splitlines())
+
+Execute command with huge output:
+
+    class PseudoFile(io.RawIOBase):
+        def write(chunk):
+            if "error" in chunk:
+                email_admin(chunk)
+
+    ssh = SSH("root", "example.com")
+    with PseudoFile() as p:
+        ssh.run("tail -f /var/log/syslog", stdout=p, timeout=False)
+
+Execute local script on remote side:
+
+    ssh = sshclient.SSH("user", "example.com")
+
+    with open("~/myscript.sh", "r") as stdin_file:
+        status, out, err = ssh.execute('/bin/sh -s "arg1" "arg2"',
+                                       stdin=stdin_file)
+
+Upload file:
+
+    ssh = SSH("user", "example.com")
+    # use rb for binary files
+    with open("/store/file.gz", "rb") as stdin_file:
+        ssh.run("cat > ~/upload/file.gz", stdin=stdin_file)
+
+Eventlet:
+
+    eventlet.monkey_patch(select=True, time=True)
+    or
+    eventlet.monkey_patch()
+    or
+    sshclient = eventlet.import_patched("vsperf.ssh")
+
+"""
+from __future__ import print_function
+import io
+import logging
+import os
+import re
+import select
+import socket
+import time
+
+import paramiko
+from chainmap import ChainMap
+from oslo_utils import encodeutils
+from scp import SCPClient
+import six
+
+# When building container change this to
+import exceptions as exceptions
+#else keep it as
+#import exceptions
+# When building container change this to
+from utils import try_int, NON_NONE_DEFAULT, make_dict_from_map
+#else keep it as
+#from utils import try_int, NON_NONE_DEFAULT, make_dict_from_map
+
+
+def convert_key_to_str(key):
+    if not isinstance(key, (paramiko.RSAKey, paramiko.DSSKey)):
+        return key
+    k = io.StringIO()
+    key.write_private_key(k)
+    return k.getvalue()
+
+
+# class SSHError(Exception):
+#     pass
+#
+#
+# class SSHTimeout(SSHError):
+#     pass
+
+
+class SSH(object):
+    """Represent ssh connection."""
+    #pylint: disable=no-member
+
+    SSH_PORT = paramiko.config.SSH_PORT
+    DEFAULT_WAIT_TIMEOUT = 120
+
+    @staticmethod
+    def gen_keys(key_filename, bit_count=2048):
+        rsa_key = paramiko.RSAKey.generate(bits=bit_count, progress_func=None)
+        rsa_key.write_private_key_file(key_filename)
+        print("Writing %s ..." % key_filename)
+        with open('.'.join([key_filename, "pub"]), "w") as pubkey_file:
+            pubkey_file.write(rsa_key.get_name())
+            pubkey_file.write(' ')
+            pubkey_file.write(rsa_key.get_base64())
+            pubkey_file.write('\n')
+
+    @staticmethod
+    def get_class():
+        # must return static class name, anything else
+        # refers to the calling class
+        # i.e. the subclass, not the superclass
+        return SSH
+
+    @classmethod
+    def get_arg_key_map(cls):
+        return {
+            'user': ('user', NON_NONE_DEFAULT),
+            'host': ('ip', NON_NONE_DEFAULT),
+            'port': ('ssh_port', cls.SSH_PORT),
+            'pkey': ('pkey', None),
+            'key_filename': ('key_filename', None),
+            'password': ('password', None),
+            'name': ('name', None),
+        }
+
+    def __init__(self, user, host, port=None, pkey=None,
+                 key_filename=None, password=None, name=None):
+        """Initialize SSH client.
+
+        :param user: ssh username
+        :param host: hostname or ip address of remote ssh server
+        :param port: remote ssh port
+        :param pkey: RSA or DSS private key string or file object
+        :param key_filename: private key filename
+        :param password: password
+        """
+        self.name = name
+        if name:
+            self.log = logging.getLogger(__name__ + '.' + self.name)
+        else:
+            self.log = logging.getLogger(__name__)
+
+        self.wait_timeout = self.DEFAULT_WAIT_TIMEOUT
+        self.user = user
+        self.host = host
+        # everybody wants to debug this in the caller, do it here instead
+        self.log.debug("user:%s host:%s", user, host)
+
+        # we may get text port from YAML, convert to int
+        self.port = try_int(port, self.SSH_PORT)
+        self.pkey = self._get_pkey(pkey) if pkey else None
+        self.password = password
+        self.key_filename = key_filename
+        self._client = False
+        # paramiko loglevel debug will output ssh protocl debug
+        # we don't ever really want that unless we are debugging paramiko
+        # ssh issues
+        if os.environ.get("PARAMIKO_DEBUG", "").lower() == "true":
+            logging.getLogger("paramiko").setLevel(logging.DEBUG)
+        else:
+            logging.getLogger("paramiko").setLevel(logging.WARN)
+
+    @classmethod
+    def args_from_node(cls, node, overrides=None, defaults=None):
+        if overrides is None:
+            overrides = {}
+        if defaults is None:
+            defaults = {}
+
+        params = ChainMap(overrides, node, defaults)
+        return make_dict_from_map(params, cls.get_arg_key_map())
+
+    @classmethod
+    def from_node(cls, node, overrides=None, defaults=None):
+        return cls(**cls.args_from_node(node, overrides, defaults))
+
+    def _get_pkey(self, key):
+        if isinstance(key, six.string_types):
+            key = six.moves.StringIO(key)
+        errors = []
+        for key_class in (paramiko.rsakey.RSAKey, paramiko.dsskey.DSSKey):
+            try:
+                return key_class.from_private_key(key)
+            except paramiko.SSHException as e:
+                errors.append(e)
+        raise exceptions.SSHError(error_msg='Invalid pkey: %s' % errors)
+
+    @property
+    def is_connected(self):
+        return bool(self._client)
+
+    def _get_client(self):
+        if self.is_connected:
+            return self._client
+        try:
+            self._client = paramiko.SSHClient()
+            self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
+            self._client.connect(self.host, username=self.user,
+                                 port=self.port, pkey=self.pkey,
+                                 key_filename=self.key_filename,
+                                 password=self.password,
+                                 allow_agent=False, look_for_keys=False,
+                                 timeout=1)
+            return self._client
+        except Exception as e:
+            message = ("Exception %(exception_type)s was raised "
+                       "during connect. Exception value is: %(exception)r" %
+                       {"exception": e, "exception_type": type(e)})
+            self._client = False
+            raise exceptions.SSHError(error_msg=message)
+
+    def _make_dict(self):
+        return {
+            'user': self.user,
+            'host': self.host,
+            'port': self.port,
+            'pkey': self.pkey,
+            'key_filename': self.key_filename,
+            'password': self.password,
+            'name': self.name,
+        }
+
+    def copy(self):
+        return self.get_class()(**self._make_dict())
+
+    def close(self):
+        if self._client:
+            self._client.close()
+            self._client = False
+
+    def run(self, cmd, stdin=None, stdout=None, stderr=None,
+            raise_on_error=True, timeout=3600,
+            keep_stdin_open=False, pty=False):
+        """Execute specified command on the server.
+
+        :param cmd:             Command to be executed.
+        :type cmd:              str
+        :param stdin:           Open file or string to pass to stdin.
+        :param stdout:          Open file to connect to stdout.
+        :param stderr:          Open file to connect to stderr.
+        :param raise_on_error:  If False then exit code will be return. If True
+                                then exception will be raized if non-zero code.
+        :param timeout:         Timeout in seconds for command execution.
+                                Default 1 hour. No timeout if set to 0.
+        :param keep_stdin_open: don't close stdin on empty reads
+        :type keep_stdin_open:  bool
+        :param pty:             Request a pseudo terminal for this connection.
+                                This allows passing control characters.
+                                Default False.
+        :type pty:              bool
+        """
+
+        client = self._get_client()
+
+        if isinstance(stdin, six.string_types):
+            stdin = six.moves.StringIO(stdin)
+
+        return self._run(client, cmd, stdin=stdin, stdout=stdout,
+                         stderr=stderr, raise_on_error=raise_on_error,
+                         timeout=timeout,
+                         keep_stdin_open=keep_stdin_open, pty=pty)
+
+    def _run(self, client, cmd, stdin=None, stdout=None, stderr=None,
+             raise_on_error=True, timeout=3600,
+             keep_stdin_open=False, pty=False):
+
+        transport = client.get_transport()
+        session = transport.open_session()
+        if pty:
+            session.get_pty()
+        session.exec_command(cmd)
+        start_time = time.time()
+
+        # encode on transmit, decode on receive
+        data_to_send = encodeutils.safe_encode("", incoming='utf-8')
+        stderr_data = None
+
+        # If we have data to be sent to stdin then `select' should also
+        # check for stdin availability.
+        if stdin and not stdin.closed:
+            writes = [session]
+        else:
+            writes = []
+
+        while True:
+            # Block until data can be read/write.
+            e = select.select([session], writes, [session], 1)[2]
+
+            if session.recv_ready():
+                data = encodeutils.safe_decode(session.recv(4096), 'utf-8')
+                self.log.debug("stdout: %r", data)
+                if stdout is not None:
+                    stdout.write(data)
+                continue
+
+            if session.recv_stderr_ready():
+                stderr_data = encodeutils.safe_decode(
+                    session.recv_stderr(4096), 'utf-8')
+                self.log.debug("stderr: %r", stderr_data)
+                if stderr is not None:
+                    stderr.write(stderr_data)
+                continue
+
+            if session.send_ready():
+                if stdin is not None and not stdin.closed:
+                    if not data_to_send:
+                        stdin_txt = stdin.read(4096)
+                        if stdin_txt is None:
+                            stdin_txt = ''
+                        data_to_send = encodeutils.safe_encode(
+                            stdin_txt, incoming='utf-8')
+                        if not data_to_send:
+                            # we may need to keep stdin open
+                            if not keep_stdin_open:
+                                stdin.close()
+                                session.shutdown_write()
+                                writes = []
+                    if data_to_send:
+                        sent_bytes = session.send(data_to_send)
+                        # LOG.debug("sent: %s" % data_to_send[:sent_bytes])
+                        data_to_send = data_to_send[sent_bytes:]
+
+            if session.exit_status_ready():
+                break
+
+            if timeout and (time.time() - timeout) > start_time:
+                message = ('Timeout executing command %(cmd)s on host %(host)s'
+                           % {"cmd": cmd, "host": self.host})
+                raise exceptions.SSHTimeout(error_msg=message)
+            if e:
+                raise exceptions.SSHError(error_msg='Socket error')
+
+        exit_status = session.recv_exit_status()
+        if exit_status != 0 and raise_on_error:
+            fmt = "Command '%(cmd)s' failed with exit_status %(status)d."
+            details = fmt % {"cmd": cmd, "status": exit_status}
+            if stderr_data:
+                details += " Last stderr data: '%s'." % stderr_data
+            raise exceptions.SSHError(error_msg=details)
+        return exit_status
+
+    def execute(self, cmd, stdin=None, timeout=3600, raise_on_error=False):
+        """Execute the specified command on the server.
+
+        :param cmd: (str)             Command to be executed.
+        :param stdin: (StringIO)      Open file to be sent on process stdin.
+        :param timeout: (int)         Timeout for execution of the command.
+        :param raise_on_error: (bool) If True, then an SSHError will be raised
+                                      when non-zero exit code.
+
+        :returns: tuple (exit_status, stdout, stderr)
+        """
+        stdout = six.moves.StringIO()
+        stderr = six.moves.StringIO()
+
+        exit_status = self.run(cmd, stderr=stderr,
+                               stdout=stdout, stdin=stdin,
+                               timeout=timeout, raise_on_error=raise_on_error)
+        stdout.seek(0)
+        stderr.seek(0)
+        return exit_status, stdout.read(), stderr.read()
+
+    def wait(self, timeout=None, interval=1):
+        """Wait for the host will be available via ssh."""
+        if timeout is None:
+            timeout = self.wait_timeout
+
+        end_time = time.time() + timeout
+        while True:
+            try:
+                return self.execute("uname")
+            except (socket.error, exceptions.SSHError) as e:
+                self.log.debug("Ssh is still unavailable: %r", e)
+                time.sleep(interval)
+            if time.time() > end_time:
+                raise exceptions.SSHTimeout(
+                    error_msg='Timeout waiting for "%s"' % self.host)
+
+    def put(self, files, remote_path=b'.', recursive=False):
+        client = self._get_client()
+
+        with SCPClient(client.get_transport()) as scp:
+            scp.put(files, remote_path, recursive)
+
+    def get(self, remote_path, local_path='/tmp/', recursive=True):
+        client = self._get_client()
+
+        with SCPClient(client.get_transport()) as scp:
+            scp.get(remote_path, local_path, recursive)
+
+    # keep shell running in the background, e.g. screen
+    def send_command(self, command):
+        client = self._get_client()
+        client.exec_command(command, get_pty=True)
+
+    def _put_file_sftp(self, localpath, remotepath, mode=None):
+        client = self._get_client()
+
+        with client.open_sftp() as sftp:
+            sftp.put(localpath, remotepath)
+            if mode is None:
+                mode = 0o777 & os.stat(localpath).st_mode
+            sftp.chmod(remotepath, mode)
+
+    TILDE_EXPANSIONS_RE = re.compile("(^~[^/]*/)?(.*)")
+
+    def _put_file_shell(self, localpath, remotepath, mode=None):
+        # quote to stop wordpslit
+        tilde, remotepath = self.TILDE_EXPANSIONS_RE.match(remotepath).groups()
+        if not tilde:
+            tilde = ''
+        cmd = ['cat > %s"%s"' % (tilde, remotepath)]
+        if mode is not None:
+            # use -- so no options
+            cmd.append('chmod -- 0%o %s"%s"' % (mode, tilde, remotepath))
+
+        with open(localpath, "rb") as localfile:
+            # only chmod on successful cat
+            self.run("&& ".join(cmd), stdin=localfile)
+
+    def put_file(self, localpath, remotepath, mode=None):
+        """Copy specified local file to the server.
+
+        :param localpath:   Local filename.
+        :param remotepath:  Remote filename.
+        :param mode:        Permissions to set after upload
+        """
+        try:
+            self._put_file_sftp(localpath, remotepath, mode=mode)
+        except (paramiko.SSHException, socket.error):
+            self._put_file_shell(localpath, remotepath, mode=mode)
+
+    def put_file_obj(self, file_obj, remotepath, mode=None):
+        client = self._get_client()
+
+        with client.open_sftp() as sftp:
+            sftp.putfo(file_obj, remotepath)
+            if mode is not None:
+                sftp.chmod(remotepath, mode)
+
+    def get_file_obj(self, remotepath, file_obj):
+        client = self._get_client()
+
+        with client.open_sftp() as sftp:
+            sftp.getfo(remotepath, file_obj)
+
+
+class AutoConnectSSH(SSH):
+
+    @classmethod
+    def get_arg_key_map(cls):
+        arg_key_map = super(AutoConnectSSH, cls).get_arg_key_map()
+        arg_key_map['wait'] = ('wait', True)
+        return arg_key_map
+
+    # always wait or we will get OpenStack SSH errors
+    def __init__(self, user, host, port=None, pkey=None,
+                 key_filename=None, password=None, name=None, wait=True):
+        super(AutoConnectSSH, self).__init__(user, host, port, pkey,
+                                             key_filename, password, name)
+        if wait and wait is not True:
+            self.wait_timeout = int(wait)
+
+    def _make_dict(self):
+        data = super(AutoConnectSSH, self)._make_dict()
+        data.update({
+            'wait': self.wait_timeout
+        })
+        return data
+
+    def _connect(self):
+        if not self.is_connected:
+            interval = 1
+            timeout = self.wait_timeout
+
+            end_time = time.time() + timeout
+            while True:
+                try:
+                    return self._get_client()
+                except (socket.error, exceptions.SSHError) as e:
+                    self.log.debug("Ssh is still unavailable: %r", e)
+                    time.sleep(interval)
+                if time.time() > end_time:
+                    raise exceptions.SSHTimeout(
+                        error_msg='Timeout waiting for "%s"' % self.host)
+
+    def drop_connection(self):
+        """ Don't close anything, just force creation of a new client """
+        self._client = False
+
+    def execute(self, cmd, stdin=None, timeout=3600, raise_on_error=False):
+        self._connect()
+        return super(AutoConnectSSH, self).execute(cmd, stdin, timeout,
+                                                   raise_on_error)
+
+    def run(self, cmd, stdin=None, stdout=None, stderr=None,
+            raise_on_error=True, timeout=3600,
+            keep_stdin_open=False, pty=False):
+        self._connect()
+        return super(AutoConnectSSH, self).run(cmd, stdin, stdout,
+                                               stderr, raise_on_error,
+                                               timeout, keep_stdin_open, pty)
+
+    def put(self, files, remote_path=b'.', recursive=False):
+        self._connect()
+        return super(AutoConnectSSH, self).put(files, remote_path, recursive)
+
+    def put_file(self, local_path, remote_path, mode=None):
+        self._connect()
+        return super(AutoConnectSSH, self).put_file(local_path,
+                                                    remote_path, mode)
+
+    def put_file_obj(self, file_obj, remote_path, mode=None):
+        self._connect()
+        return super(AutoConnectSSH, self).put_file_obj(file_obj,
+                                                        remote_path, mode)
+
+    def get_file_obj(self, remote_path, file_obj):
+        self._connect()
+        return super(AutoConnectSSH, self).get_file_obj(remote_path, file_obj)
+
+    @staticmethod
+    def get_class():
+        # must return static class name,
+        # anything else refers to the calling class
+        # i.e. the subclass, not the superclass
+        return AutoConnectSSH
diff --git a/xtesting/baremetal/testcases.yaml b/xtesting/baremetal/testcases.yaml
new file mode 100644 (file)
index 0000000..91cef45
--- /dev/null
@@ -0,0 +1,16 @@
+---
+tiers:
+    -
+        name: vsperfbm
+        order: 1
+        description: ''
+        testcases:
+            -
+                case_name: phy2phy_tput
+                project_name: vsperf
+                criteria: 100
+                blocking: true
+                clean_flag: false
+                description: ''
+                run:
+                    name: vsperf_controller
diff --git a/xtesting/baremetal/utils.py b/xtesting/baremetal/utils.py
new file mode 100644 (file)
index 0000000..d945381
--- /dev/null
@@ -0,0 +1,41 @@
+"""
+# Copyright 2013: Mirantis Inc.
+# All Rights Reserved.
+#
+#    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.
+"""
+
+
+NON_NONE_DEFAULT = object()
+
+
+def get_key_with_default(data, key, default=NON_NONE_DEFAULT):
+    """get default key"""
+    value = data.get(key, default)
+    if value is NON_NONE_DEFAULT:
+        raise KeyError(key)
+    return value
+
+
+def make_dict_from_map(data, key_map):
+    """mapping dict"""
+    return {dest_key: get_key_with_default(data, src_key, default)
+            for dest_key, (src_key, default) in key_map.items()}
+
+def try_int(s, *args):
+    """Convert to integer if possible."""
+    #pylint: disable=invalid-name
+    try:
+        return int(s)
+    except (TypeError, ValueError):
+        return args[0] if args else s
diff --git a/xtesting/baremetal/vsperf.conf b/xtesting/baremetal/vsperf.conf
new file mode 100644 (file)
index 0000000..8ed7115
--- /dev/null
@@ -0,0 +1,21 @@
+VSWITCH_BRIDGE_NAME = 'vsperf-br0'
+WHITELIST_NICS = ['02:00.0', '02:00.1']
+TRAFFICGEN = 'Trex'
+TRAFFICGEN_TREX_HOST_IP_ADDR = '10.10.120.25'
+TRAFFICGEN_TREX_USER = 'root'
+TRAFFICGEN_TREX_BASE_DIR = '/root/trex_2.86/'
+TRAFFICGEN_TREX_LINE_SPEED_GBPS = '10'
+TRAFFICGEN_TREX_PORT1 = '0000:81:00.0'
+TRAFFICGEN_TREX_PORT2 = '0000:81:00.1'
+TRAFFICGEN_TREX_PROMISCUOUS = False
+TRAFFICGEN_DURATION=1
+TRAFFICGEN_LOSSRATE=0
+TRAFFICGEN_RFC2544_TESTS=10
+#TRAFFICGEN_PKT_SIZES=(64,128,256,512,1024,1280,1518)
+TRAFFICGEN_PKT_SIZES=(1024,)
+GUEST_TESTPMD_FWD_MODE = ['io']
+GUEST_IMAGE = ['/home/opnfv/vnfs/vloop-vnf-ubuntu-18.04_20180920.qcow2']
+TRAFFICGEN_TREX_LATENCY_PPS = 1000
+TRAFFICGEN_TREX_RFC2544_BINARY_SEARCH_LOSS_VERIFICATION = True
+TRAFFICGEN_TREX_RFC2544_MAX_REPEAT = 2
+
diff --git a/xtesting/baremetal/vsperf_controller.py b/xtesting/baremetal/vsperf_controller.py
new file mode 100644 (file)
index 0000000..91bad76
--- /dev/null
@@ -0,0 +1,194 @@
+#!/usr/bin/env python
+
+# Copyright 2018-19 Spirent Communications.
+#
+# 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.
+
+"""
+VSPERF-controller
+"""
+
+# Fetching Environment Variable for controller, You can configure or
+# modifies list.env file for setting your environment variable.
+
+#pylint: disable=global-statement,no-else-continue
+#pylint: disable=too-many-branches
+
+import os
+import sys
+from stat import S_ISDIR
+import time
+import paramiko
+from xtesting.core import testcase
+import ssh
+
+TIMER = float()
+
+
+
+DUT_IP = os.getenv('DUT_IP_ADDRESS')
+DUT_USER = os.getenv('DUT_USERNAME')
+DUT_PWD = os.getenv('DUT_PASSWORD')
+RES_PATH= os.getenv('RES_PATH')
+
+VSPERF_TEST = os.getenv('VSPERF_TESTS')
+VSPERF_CONF = os.getenv('VSPERF_CONFFILE')
+VSPERF_TRAFFICGEN_MODE = str(os.getenv('VSPERF_TRAFFICGEN_MODE'))
+
+DUT_CLIENT = None
+TGEN_CLIENT = None
+
+RECV_BYTES = 4096
+
+def host_connect():
+    """
+    Handle host connectivity to DUT
+    """
+    global DUT_CLIENT
+    DUT_CLIENT = ssh.SSH(host=DUT_IP, user=DUT_USER, password=DUT_PWD)
+    print("DUT Successfully Connected ..............................................[OK] \n ")
+
+def upload_test_config_file():
+    """
+    #Upload Test Config File on DUT
+    """
+    #localpath = '/usr/src/app/vsperf/vsperf.conf'
+    if VSPERF_CONF:
+        localpath = VSPERF_CONF
+    else:
+        localpath = 'vsperf.conf'
+    if not os.path.exists(localpath):
+        print("VSPERF Test config File does not exists.......................[Failed]")
+        return
+    remotepath = '~/vsperf.conf'
+    check_test_config_cmd = "find ~/ -maxdepth 1 -name '{}'".format(
+        remotepath[2:])
+    check_test_result = str(DUT_CLIENT.execute(check_test_config_cmd)[1])
+    if remotepath[2:] in check_test_result:
+        DUT_CLIENT.run("rm -f {}".format(remotepath[2:]))
+    DUT_CLIENT.put_file(localpath, remotepath)
+    check_test_config_cmd_1= "find ~/ -maxdepth 1 -name '{}'".format(
+        remotepath[2:])
+    print(check_test_config_cmd_1)
+    check_test_result_1= str(DUT_CLIENT.execute(check_test_config_cmd)[1])
+    if remotepath[2:] in check_test_result_1:
+        print(
+        "Test Configuration File Uploaded on DUT-Host.............................[OK] \n ")
+    else:
+        print("VSPERF Test config file upload failed.....................................[Critical]")
+
+def run_vsperf_test():
+    """
+    Here we will perform the actual vsperf test
+    """
+    global TIMER
+    rmv_cmd = "cd /mnt/huge && echo {} | sudo -S rm -rf *".format(DUT_PWD)
+    DUT_CLIENT.run(rmv_cmd, pty=True)
+    cmd = "source ~/vsperfenv/bin/activate ; "
+    #cmd = "scl enable python33 bash ; "
+    cmd += "cd vswitchperf && "
+    cmd += "./vsperf "
+    cmd += "--conf-file ~/vsperf.conf "
+    if "yes" in VSPERF_TRAFFICGEN_MODE.lower():
+        cmd += "--mode trafficgen"
+    vsperf_test_list = VSPERF_TEST.split(",")
+    print(vsperf_test_list)
+    for test in vsperf_test_list:
+        atest = cmd
+        atest += test
+        DUT_CLIENT.run(atest, pty=True)
+    print(
+        "Test Successfully Completed................................................[OK]\n ")
+
+def get_result():
+    """
+    Get Latest results from DUT
+    """
+    stdout_data = []
+    stderr_data = []
+    client = paramiko.Transport((DUT_IP, 22))
+    client.connect(username=DUT_USER, password=DUT_PWD)
+    session = client.open_channel(kind='session')
+    directory_to_download = ''
+    session.exec_command('ls /tmp | grep results')
+    if not directory_to_download:
+        while True:
+            if session.recv_ready():
+                stdout_data.append(session.recv(RECV_BYTES))
+            if session.recv_stderr_ready():
+                stderr_data.append(session.recv_stderr(RECV_BYTES))
+            if session.exit_status_ready():
+                break
+        if stdout_data:
+            line = stdout_data[0]
+            filenames = line.decode("utf-8").rstrip("\n").split("\n")
+            filenames = sorted(filenames)
+            latest = filenames[-1]
+            directory_to_download = os.path.join('/tmp', latest)
+    stdout_data = []
+    stderr_data = []
+    if directory_to_download:
+        destination = os.path.join(RES_PATH,
+                               os.path.basename(os.path.normpath(
+                                   directory_to_download)))
+        os.makedirs(destination)
+        print(directory_to_download)
+        # Begin the actual downlaod
+        sftp = paramiko.SFTPClient.from_transport(client)
+        def sftp_walk(remotepath):
+            path=remotepath
+            files=[]
+            folders=[]
+            for fle in sftp.listdir_attr(remotepath):
+                if S_ISDIR(fle.st_mode):
+                    folders.append(fle.filename)
+                else:
+                    files.append(fle.filename)
+            if files:
+                yield path, files
+        # Filewise download happens here
+        for path,files  in sftp_walk(directory_to_download):
+            for fil in files:
+                remote = os.path.join(path,fil)
+                local = os.path.join(destination, fil)
+                print(local)
+                sftp.get(remote, local)
+    # Ready to work with downloaded data, close the session and client.
+    session.close()
+    client.close()
+
+class VsperfBm(testcase.TestCase):
+    """
+    VSPERF-Xtesting Baremetal Control Class
+    """
+    def run(self, **kwargs):
+        global RES_PATH
+        try:
+            self.start_time = time.time()
+            self.result=100
+            os.makedirs(self.res_dir, exist_ok=True)
+            RES_PATH = self.res_dir
+            if DUT_IP:
+                host_connect()
+            if not DUT_CLIENT:
+                print('Failed to connect to DUT ...............[Critical]')
+                self.result = 0
+            else:
+                upload_test_config_file()
+                run_vsperf_test()
+                get_result()
+            self.stop_time = time.time()
+        except Exception:  # pylint: disable=broad-except
+            print("Unexpected error:", sys.exc_info()[0])
+            self.result = 0
+            self.stop_time = time.time()
diff --git a/xtesting/openstack/Dockerfile b/xtesting/openstack/Dockerfile
new file mode 100644 (file)
index 0000000..2e61387
--- /dev/null
@@ -0,0 +1,61 @@
+# Copyright 2020 Spirent Communications.
+#
+# 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.
+
+FROM opnfv/xtesting
+LABEL maintainer="sridhar.rao@spirent.com"
+
+# Install required packages
+RUN apk add --no-cache --update python3 python3-dev \
+    py3-wheel py3-pip git openssh-client python3-tkinter \
+    tk gcc musl-dev libffi-dev openssl-dev make
+
+# Clone VSPERF.
+RUN git clone https://gerrit.opnfv.org/gerrit/vswitchperf /vswitchperf
+
+#
+# Remove unnecessary python packages.
+#
+RUN cd /vswitchperf && \
+  sed -e '/numpy/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/matplotlib/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/pycrypto/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/pypsi/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/paramiko/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/pyzmq/ s/^#*/#\ /' -i requirements.txt && \
+  sed -e '/kubernetes/ s/^#*/#\ /' -i requirements.txt
+
+#
+# Build VSPERF
+#
+RUN cd /vswitchperf && \
+    pip3 install --ignore-installed distlib -r requirements.txt && \
+    cd /vswitchperf/src/trex && make
+
+# Include vsperf into Path.
+ENV PATH "$PATH:/vswitchperf"
+
+COPY vsperfostack.conf /vsperfostack.conf
+
+# Required step for Xtesting
+ADD . /src/
+RUN git init /src && pip3 install /src
+
+# Copy Testcase
+COPY testcases.yaml /usr/lib/python3.8/site-packages/xtesting/ci/testcases.yaml
+
+# Set working directory - This helps to resolve path to templates.
+WORKDIR /vswitchperf
+
+# Command Run
+CMD ["run_tests", "-t", "all"]
diff --git a/xtesting/openstack/cloud.rc b/xtesting/openstack/cloud.rc
new file mode 100644 (file)
index 0000000..3f86774
--- /dev/null
@@ -0,0 +1,10 @@
+export OS_AUTH_URL=http://10.10.180.21/identity
+export OS_PROJECT_ID=0440a230a799460facec0d09dde64497
+export OS_PROJECT_NAME="admin"
+export OS_USER_DOMAIN_NAME="Default"
+export OS_PROJECT_DOMAIN_ID="default"
+export OS_USERNAME="admin"
+export OS_PASSWORD="admin123"
+export OS_REGION_NAME="RegionOne"
+export OS_INTERFACE=public
+export OS_IDENTITY_API_VERSION=3
diff --git a/xtesting/openstack/setup.cfg b/xtesting/openstack/setup.cfg
new file mode 100644 (file)
index 0000000..4b98992
--- /dev/null
@@ -0,0 +1,10 @@
+[metadata]
+name = vsperfostack
+version = 1
+
+[files]
+packages = .
+
+[entry_points]
+xtesting.testcase =
+    vsperfostack = vsperfostack:VsperfOstack
diff --git a/xtesting/openstack/setup.py b/xtesting/openstack/setup.py
new file mode 100644 (file)
index 0000000..1394cdf
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+
+# pylint: disable=missing-docstring
+
+import setuptools
+
+setuptools.setup(
+    setup_requires=['pbr>=2.0.0'],
+    pbr=True)
diff --git a/xtesting/openstack/site.yml b/xtesting/openstack/site.yml
new file mode 100644 (file)
index 0000000..1ca663f
--- /dev/null
@@ -0,0 +1,13 @@
+---
+- hosts:
+    - 127.0.0.1
+  roles:
+    - role: collivier.xtesting
+      project: vsperfostack
+      repo: 127.0.0.1
+      dport: 5000
+      gerrit:
+      suites:
+        - container: vsperfos
+          tests:
+            - phy2phy_tput
diff --git a/xtesting/openstack/testcases.yaml b/xtesting/openstack/testcases.yaml
new file mode 100644 (file)
index 0000000..aab3b16
--- /dev/null
@@ -0,0 +1,19 @@
+---
+tiers:
+    -
+        name: vsperfostack
+        order: 1
+        description: 'VSPERF Openstack Testing'
+        testcases:
+            -
+                case_name: phy2phy_tput
+                project_name: vsperfostack
+                criteria: 100
+                blocking: true
+                clean_flag: false
+                description: 'VSPERF Openstack RFC2544 Throughput Test'
+                run:
+                    name: vsperfostack
+                    args:
+                        conf_file: vsperfostack.conf
+                        deploy_tgen: false
diff --git a/xtesting/openstack/vsperfostack.conf b/xtesting/openstack/vsperfostack.conf
new file mode 100644 (file)
index 0000000..489054a
--- /dev/null
@@ -0,0 +1,80 @@
+# Copyright 20202 Spirent Communications.
+#
+# 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.
+
+# OPenstack Information
+
+OS_AUTH_URL="http://10.10.180.21/identity"
+OS_PROJECT_ID="0440a230a799460facec0d09dde64497"
+OS_PROJECT_NAME="admin"
+OS_USER_DOMAIN_NAME="Default"
+OS_PROJECT_DOMAIN_ID="default"
+OS_USERNAME="admin"
+OS_PASSWORD="admin123"
+OS_REGION_NAME="RegionOne"
+OS_INTERFACE="public"
+OS_IDENTITY_API_VERSION=3
+OS_INSECURE=False
+OS_CA_CERT= 'None'
+
+# Deployment Information
+SCENARIOS = ['templates/l2_2c_2i.yaml']
+FLAVOR_NAME = 'm1.large'
+IMAGE_NAME = 'stcv'
+EXTERNAL_NET = 'public'
+
+# Traffic Information
+TRAFFICGEN_PKT_SIZES = (1024,)
+TRAFFICGEN_DURATION = 10
+
+# Traffigen to Use
+TRAFFICGEN='TestCenter'
+
+
+# Trafficgen Specific Information
+# STC
+TRAFFICGEN_STC_LAB_SERVER_ADDR = "10.10.180.245"
+TRAFFICGEN_STC_LICENSE_SERVER_ADDR = "10.10.50.226"
+TRAFFICGEN_STC_EAST_SLOT_NUM = "1"
+TRAFFICGEN_STC_EAST_PORT_NUM = "1"
+TRAFFICGEN_STC_WEST_SLOT_NUM = "1"
+TRAFFICGEN_STC_WEST_PORT_NUM = "1"
+TRAFFICGEN_STC_PYTHON2_PATH = "/usr/bin/python3"
+TRAFFICGEN_STC_RFC2544_TPUT_TEST_FILE_NAME = "testcenter-rfc2544-rest.py"
+TRAFFICGEN_STC_RFC2544_METRIC="throughput"
+
+
+# Ixia
+TRAFFICGEN_EAST_IXIA_CARD = '1'
+TRAFFICGEN_WEST_IXIA_CARD = '1'
+TRAFFICGEN_EAST_IXIA_PORT = '1'
+TRAFFICGEN_WEST_IXIA_PORT = '1'
+TRAFFICGEN_IXIA_LIB_PATH = '/opt/ixia/ixos-api/9.00.0.20/lib/ixTcl1.0'
+TRAFFICGEN_IXNET_LIB_PATH = '/opt/ixia/ixnetwork/9.00.1915.16/lib/TclApi/IxTclNetwork'
+TRAFFICGEN_IXNET_MACHINE = '10.10.180.240'  # quad dotted ip address
+TRAFFICGEN_IXNET_PORT = '443'
+TRAFFICGEN_IXNET_USER = 'admin'
+TRAFFICGEN_IXNET_TESTER_RESULT_DIR = 'c:/ixia_results/vsperf_sandbox'
+TRAFFICGEN_IXNET_DUT_RESULT_DIR = '/mnt/ixia_results/vsperf_sandbox'
+
+# Trex
+TRAFFICGEN_TREX_HOST_IP_ADDR = '10.10.120.25'
+TRAFFICGEN_TREX_USER = 'root'
+TRAFFICGEN_TREX_BASE_DIR = '/root/trex_2.86/'
+TRAFFICGEN_TREX_LINE_SPEED_GBPS = '10'
+TRAFFICGEN_TREX_PORT1 = '0000:81:00.0'
+TRAFFICGEN_TREX_PORT2 = '0000:81:00.1'
+TRAFFICGEN_TREX_PROMISCUOUS = False
+TRAFFICGEN_TREX_LATENCY_PPS = 1000
+TRAFFICGEN_TREX_RFC2544_BINARY_SEARCH_LOSS_VERIFICATION = False
+TRAFFICGEN_TREX_RFC2544_MAX_REPEAT = 2
diff --git a/xtesting/openstack/vsperfostack.py b/xtesting/openstack/vsperfostack.py
new file mode 100755 (executable)
index 0000000..d4a14c0
--- /dev/null
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+
+# Copyright 2020 Spirent Communications.
+#
+# 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.
+
+"""VSPERF-Xtesting-Openstack Control script.
+"""
+
+import os
+import subprocess
+import sys
+import time
+
+from xtesting.core import testcase
+
+
+class VsperfOstack(testcase.TestCase):
+    """
+    Implement Xtesting's testcase class
+    """
+    def run(self, **kwargs):
+        """
+        Main Run.
+        """
+        custom_conffile = '/vswitchperf/conf/99_xtesting.conf'
+        try:
+            test_params = {}
+            for key in kwargs:
+                test_params[key] = kwargs[key]
+            # Make results directory - Xtesting Requirement
+            os.makedirs(self.res_dir, exist_ok=True)
+            # Start the timer
+            self.start_time = time.time()
+
+            # Get the parameter
+            if 'conf_file' in test_params.keys():
+                conffile = os.path.join('/', test_params['conf_file'])
+            else:
+                conffile = '/vsperfostack.conf'
+
+            # Remove customfile if it exists.
+            if os.path.exists(custom_conffile):
+                os.remove(custom_conffile)
+
+            # Write custom configuration.
+            with open(custom_conffile, 'a+') as fil:
+                fil.writelines("LOG_DIR='{}'".format(self.res_dir))
+            fil.close()
+            # Start the vsperf command
+            if('deploy_tgen' in test_params.keys() and
+               test_params['deploy_tgen']):
+                output = subprocess.check_output(['vsperf',
+                                                  '--conf-file',
+                                                  conffile,
+                                                  '--openstack',
+                                                  '--load-env',
+                                                  '--tests',
+                                                  self.case_name])
+            else:
+                output = subprocess.check_output(['vsperf',
+                                                  '--conf-file',
+                                                  conffile,
+                                                  '--load-env',
+                                                  '--mode',
+                                                  'traffigen',
+                                                  '--tests',
+                                                  self.case_name])
+            print(output)
+            self.result = 100
+            self.stop_time = time.time()
+        except Exception:  # pylint: disable=broad-except
+            print("Unexpected error:", sys.exc_info()[0])
+            self.result = 0
+            self.stop_time = time.time()