aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoropensource-tnbt <sridhar.rao@spirent.com>2020-11-25 19:30:04 +0530
committeropensource-tnbt <sridhar.rao@spirent.com>2020-11-28 18:51:49 +0530
commit0bbc7e2674bd06921e9fe4a0b3318724327d3ff2 (patch)
treeb8c5bbf7dadf8e6e06adc969bac1d75d5ff2f908
parent54995886ec5135bca580cb81a3ed8fd2eb4bae3a (diff)
[WIP]: VSPERF-Xtesting Integration Support.
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
-rw-r--r--docs/index.rst1
-rw-r--r--docs/xtesting/index.rst85
-rwxr-xr-xdocs/xtesting/vsperf-xtesting.pngbin0 -> 93202 bytes
-rw-r--r--xtesting/baremetal/Dockerfile36
-rw-r--r--xtesting/baremetal/exceptions.py65
-rw-r--r--xtesting/baremetal/requirements.txt2
-rw-r--r--xtesting/baremetal/setup.cfg10
-rw-r--r--xtesting/baremetal/setup.py9
-rw-r--r--xtesting/baremetal/site.yml13
-rw-r--r--xtesting/baremetal/ssh.py546
-rw-r--r--xtesting/baremetal/testcases.yaml16
-rw-r--r--xtesting/baremetal/utils.py41
-rw-r--r--xtesting/baremetal/vsperf.conf21
-rw-r--r--xtesting/baremetal/vsperf_controller.py194
-rw-r--r--xtesting/openstack/Dockerfile61
-rw-r--r--xtesting/openstack/cloud.rc10
-rw-r--r--xtesting/openstack/setup.cfg10
-rw-r--r--xtesting/openstack/setup.py9
-rw-r--r--xtesting/openstack/site.yml13
-rw-r--r--xtesting/openstack/testcases.yaml19
-rw-r--r--xtesting/openstack/vsperfostack.conf80
-rwxr-xr-xxtesting/openstack/vsperfostack.py85
22 files changed, 1326 insertions, 0 deletions
diff --git a/docs/index.rst b/docs/index.rst
index d688c752..c8a400f8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -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
index 00000000..9259a12a
--- /dev/null
+++ b/docs/xtesting/index.rst
@@ -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
index 00000000..64cad722
--- /dev/null
+++ b/docs/xtesting/vsperf-xtesting.png
Binary files differ
diff --git a/xtesting/baremetal/Dockerfile b/xtesting/baremetal/Dockerfile
new file mode 100644
index 00000000..b78594b5
--- /dev/null
+++ b/xtesting/baremetal/Dockerfile
@@ -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
index 00000000..c4e0e097
--- /dev/null
+++ b/xtesting/baremetal/exceptions.py
@@ -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
index 00000000..f2da6ad5
--- /dev/null
+++ b/xtesting/baremetal/requirements.txt
@@ -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
index 00000000..9ca38236
--- /dev/null
+++ b/xtesting/baremetal/setup.cfg
@@ -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
index 00000000..fa9d59ac
--- /dev/null
+++ b/xtesting/baremetal/setup.py
@@ -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
index 00000000..06f8c2e2
--- /dev/null
+++ b/xtesting/baremetal/site.yml
@@ -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
index 00000000..ce560c49
--- /dev/null
+++ b/xtesting/baremetal/ssh.py
@@ -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
index 00000000..91cef451
--- /dev/null
+++ b/xtesting/baremetal/testcases.yaml
@@ -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
index 00000000..d945381e
--- /dev/null
+++ b/xtesting/baremetal/utils.py
@@ -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
index 00000000..8ed7115f
--- /dev/null
+++ b/xtesting/baremetal/vsperf.conf
@@ -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
index 00000000..91bad766
--- /dev/null
+++ b/xtesting/baremetal/vsperf_controller.py
@@ -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
index 00000000..2e613872
--- /dev/null
+++ b/xtesting/openstack/Dockerfile
@@ -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
index 00000000..3f867743
--- /dev/null
+++ b/xtesting/openstack/cloud.rc
@@ -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
index 00000000..4b98992a
--- /dev/null
+++ b/xtesting/openstack/setup.cfg
@@ -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
index 00000000..1394cdfe
--- /dev/null
+++ b/xtesting/openstack/setup.py
@@ -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
index 00000000..1ca663f4
--- /dev/null
+++ b/xtesting/openstack/site.yml
@@ -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
index 00000000..aab3b16a
--- /dev/null
+++ b/xtesting/openstack/testcases.yaml
@@ -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
index 00000000..489054a7
--- /dev/null
+++ b/xtesting/openstack/vsperfostack.conf
@@ -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
index 00000000..d4a14c06
--- /dev/null
+++ b/xtesting/openstack/vsperfostack.py
@@ -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()