support rfc2544 test using spirent virtual test center 03/52503/4
authorQiang Dai <Qiang.Dai@spirent.com>
Fri, 23 Feb 2018 07:54:05 +0000 (15:54 +0800)
committerzhihui wu <wu.zhihui1@zte.com.cn>
Tue, 27 Feb 2018 03:24:07 +0000 (11:24 +0800)
1. support rfc2544 throughput and latency test with different packet size
2. support vswitch performance test based on STCv affinity deployment

Change-Id: I597de973ab95039dcbcd2da8473a3a5c87a10c14
Signed-off-by: Qiang Dai <Qiang.Dai@spirent.com>
contrib/nettest/Dockerfile [new file with mode: 0644]
contrib/nettest/README.md [new file with mode: 0644]
contrib/nettest/nettest/heat_2stcv.yaml [new file with mode: 0644]
contrib/nettest/nettest/nettest.py [new file with mode: 0644]
contrib/nettest/nettest/requirements.txt [new file with mode: 0644]
contrib/nettest/nettest/rest_server.py [new file with mode: 0644]
contrib/nettest/nettest/rfc2544test.py [new file with mode: 0644]
contrib/nettest/nettest/start.sh [new file with mode: 0644]
contrib/nettest/nettest/stcv_stack.py [new file with mode: 0644]

diff --git a/contrib/nettest/Dockerfile b/contrib/nettest/Dockerfile
new file mode 100644 (file)
index 0000000..a0ecabf
--- /dev/null
@@ -0,0 +1,47 @@
+##########################################################
+# Dockerfile to run a flask-based web application# Based on an ubuntu:16.04
+##########################################################
+
+# Set the base image to use to centos
+FROM ubuntu:16.04
+
+# Set the file maintainer
+MAINTAINER Qiang.Dai@spirent.com
+LABEL version="0.1" description="Spirent networking test Docker container"
+
+# Set env varibles used in this Dockerfile (add a unique prefix, such as DOCKYARD)
+# Local directory with project source
+ENV DOCKYARD_SRC=nettest \
+    DOCKYARD_SRCHOME=/opt \
+    DOCKYARD_SRCPROJ=/opt/nettest
+
+# Update the defualt application repository source list
+RUN apt-get update && apt-get install -y \
+    gcc \
+    python-dev \
+    python-pip \
+    python-setuptools \
+    --no-install-recommends \
+    && rm -rf /var/lib/apt/lists/*
+
+# Copy application source code to SRCDIR
+COPY $DOCKYARD_SRC $DOCKYARD_SRCPROJ
+
+# Create application subdirectories
+WORKDIR $DOCKYARD_SRCPROJ
+RUN mkdir -p log
+VOLUME ["$DOCKYARD_SRCPROJ/log/"]
+
+# Install Python dependencies
+RUN pip install -U pip \
+    && pip install -U setuptools \
+    && pip install -r $DOCKYARD_SRCPROJ/requirements.txt
+
+# Port to expose
+EXPOSE 5001
+
+# Copy entrypoint script into the image
+WORKDIR $DOCKYARD_SRCPROJ
+
+#CMD ["/bin/bash"]
+CMD ["/bin/bash", "start.sh"]
diff --git a/contrib/nettest/README.md b/contrib/nettest/README.md
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/contrib/nettest/nettest/heat_2stcv.yaml b/contrib/nettest/nettest/heat_2stcv.yaml
new file mode 100644 (file)
index 0000000..77c6e6e
--- /dev/null
@@ -0,0 +1,170 @@
+##############################################################################\r
+# Copyright (c) 2018 Spirent Communications and others.\r
+#\r
+# All rights reserved. This program and the accompanying materials\r
+# are made available under the terms of the Apache License, Version 2.0\r
+# which accompanies this distribution, and is available at\r
+# http://www.apache.org/licenses/LICENSE-2.0\r
+##############################################################################\r
+\r
+heat_template_version: 2016-10-14\r
+\r
+description: Template for deploying 2 STCv and 1 labserver\r
+\r
+parameters:\r
+  public_net_name: {default: external, description: Public network to allocate floating IPs to VMs', type: string}\r
+  #public_net_id: {description: public_network id for exernal connectivity,type: string}\r
+  mgmt_net_name: {default: admin, description: Name of STCv mgmt network to be created, type: string}\r
+  mgmt_net_cidr: {default: 10.10.10.0/24, description: STCv mgmt network CIDR,type: string}\r
+  mgmt_net_gw: {default: 10.10.10.1, description: STCv mgmt network gateway address, type: string}\r
+  mgmt_net_pool_start: {default: 10.10.10.10, description: Start of mgmt network IP address allocation pool, type: string}\r
+  mgmt_net_pool_end: {default: 10.10.10.20, description: End of mgmt network IP address allocation pool, type: string}\r
+  tst_net_name: {default: tst, description: Name of STCv private network to be created, type: string}\r
+  tst_net_cidr: {default: 192.168.1.0/24, description: STCv private network CIDR,type: string}\r
+  tst_net_gw: {default: 192.168.1.1, description: STCv private network gateway address, type: string}\r
+  tst_net_pool_start: {default: 192.168.1.10, description: Start of private network IP address allocation pool, type: string}\r
+  tst_net_pool_end: {default: 192.168.1.20, description: End of private network IP address allocation pool, type: string}\r
+  stcv_image: {default: "stcv-4.79", description: Image name to use for STCv, type: string}\r
+  stcv_flavor: {default: "m1.tiny", description: Flavor to use for STCv, type: string}\r
+  #stcv_user_data: {default: "", description: user data such as ntp server ip for stcv, type: string}\r
+  #stcv_config_file: {default: "stcv_config_file", description: user data such as ntp server ip for stcv, type: string}\r
+  ntp_server_ip: {default: "", description: user data such as ntp server ip for stcv, type: string}\r
+  stcv_sg_name: {default: stcv_sg, description: server group name, type: string}\r
+  stcv_sg_affinity: {default: affinity, description: server group affinity for stcv, type: string}\r
+\r
+resources:\r
+  stcv_server_group:\r
+    type: OS::Nova::ServerGroup\r
+    properties:\r
+      name: {get_param: stcv_sg_name}\r
+      policies: [{get_param: stcv_sg_affinity}]\r
+  mgmt_net:\r
+    type: OS::Neutron::Net\r
+    properties:\r
+      name: {get_param: mgmt_net_name}\r
+  mgmt_net_subnet:\r
+    type: OS::Neutron::Subnet\r
+    properties:\r
+      allocation_pools:\r
+        - end: {get_param: mgmt_net_pool_end}\r
+          start: {get_param: mgmt_net_pool_start}\r
+      cidr: {get_param: mgmt_net_cidr}\r
+      gateway_ip: {get_param: mgmt_net_gw}\r
+      network: {get_resource: mgmt_net}\r
+  public_router:\r
+    type: OS::Neutron::Router\r
+    properties:\r
+      external_gateway_info:\r
+        network: {get_param: public_net_name}\r
+  router_interface:\r
+    type: OS::Neutron::RouterInterface\r
+    properties:\r
+      router: {get_resource: public_router}\r
+      subnet: {get_resource: mgmt_net_subnet}\r
+  tst_net:\r
+    type: OS::Neutron::Net\r
+    properties:\r
+      name: {get_param: tst_net_name}\r
+  tst_subnet:\r
+    type: OS::Neutron::Subnet\r
+    properties:\r
+      allocation_pools:\r
+        - end: {get_param: tst_net_pool_end}\r
+          start: {get_param: tst_net_pool_start}\r
+      cidr: {get_param: tst_net_cidr}\r
+      gateway_ip: {get_param: tst_net_gw}\r
+      network: {get_resource: tst_net}\r
+  stcv_1_port_1:\r
+    type: OS::Neutron::Port\r
+    properties:\r
+      network: {get_resource: mgmt_net}\r
+      fixed_ips:\r
+      - subnet: {get_resource: mgmt_net_subnet}\r
+  floating_ip1:\r
+    type: OS::Neutron::FloatingIP\r
+    properties:\r
+      floating_network: {get_param: public_net_name}\r
+      port_id: {get_resource: stcv_1_port_1}\r
+  stcv_1_port_2:\r
+    type: OS::Neutron::Port\r
+    properties:\r
+      network: {get_resource: tst_net}\r
+      port_security_enabled: False\r
+      fixed_ips:\r
+      - subnet: {get_resource: tst_subnet}\r
+  STCv_1:\r
+    type: OS::Nova::Server\r
+    properties:\r
+      #availability_zone : {get_param: availability_zone_name}\r
+      flavor: {get_param: stcv_flavor}\r
+      image: {get_param: stcv_image}\r
+      name: STCv_1\r
+      user_data:\r
+        str_replace:\r
+          template: |\r
+            #cloud-config\r
+            spirent:\r
+                ntp: $ntp_server_ip\r
+          params:\r
+            $ntp_server_ip: {get_param: ntp_server_ip}\r
+      user_data_format: RAW\r
+      config_drive: True\r
+      scheduler_hints:\r
+        group: {get_resource: stcv_server_group}\r
+      networks:\r
+      - port: {get_resource: stcv_1_port_1}\r
+      - port: {get_resource: stcv_1_port_2}\r
+  stcv_2_port_1:\r
+    type: OS::Neutron::Port\r
+    properties:\r
+      network: {get_resource: mgmt_net}\r
+      fixed_ips:\r
+      - subnet: {get_resource: mgmt_net_subnet}\r
+  floating_ip2:\r
+    type: OS::Neutron::FloatingIP\r
+    properties:\r
+      floating_network: {get_param: public_net_name}\r
+      port_id: {get_resource: stcv_2_port_1}\r
+  stcv_2_port_2:\r
+    type: OS::Neutron::Port\r
+    properties:\r
+      network: {get_resource: tst_net}\r
+      port_security_enabled: False\r
+      fixed_ips:\r
+      - subnet: {get_resource: tst_subnet}\r
+  STCv_2:\r
+    type: OS::Nova::Server\r
+    properties:\r
+      #availability_zone : {get_param: availability_zone_name}\r
+      flavor: {get_param: stcv_flavor}\r
+      image: {get_param: stcv_image}\r
+      name: STCv_2\r
+      user_data:\r
+        str_replace:\r
+          template: |\r
+            #cloud-config\r
+            spirent:\r
+                ntp: $ntp_server_ip\r
+          params:\r
+            $ntp_server_ip: {get_param: ntp_server_ip}\r
+      user_data_format: RAW\r
+      config_drive: True\r
+      scheduler_hints:\r
+        group: {get_resource: stcv_server_group}\r
+      networks:\r
+      - port: {get_resource: stcv_2_port_1}\r
+      - port: {get_resource: stcv_2_port_2}\r
+outputs:\r
+  STCv_1_Mgmt_Ip:\r
+     value: {get_attr: [floating_ip1, floating_ip_address]}\r
+     description: STCv_1 Mgmt IP\r
+  STCv_2_Mgmt_Ip:\r
+     value: {get_attr: [floating_ip2, floating_ip_address]}\r
+     description: STCv_2 Mgmt IP\r
+  STCv_1_Tst_Ip:\r
+     value: {get_attr: [stcv_1_port_2, fixed_ips]}\r
+     description: STCv_1 Tst IP\r
+  STCv_2_Tst_Ip:\r
+     value: {get_attr: [stcv_2_port_2, fixed_ips]}\r
+     description: STCv_2 Tst IP\r
+\r
diff --git a/contrib/nettest/nettest/nettest.py b/contrib/nettest/nettest/nettest.py
new file mode 100644 (file)
index 0000000..c5a203e
--- /dev/null
@@ -0,0 +1,157 @@
+##############################################################################
+# Copyright (c) 2018 Spirent Communications and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+from time import sleep
+
+from rfc2544test import StcRfc2544Test
+from stcv_stack import StcvStack
+
+
+class NetTestMaster(object):
+
+    def __init__(self):
+        self.logger = logging.getLogger(__name__)
+
+        self.stacks = []
+        self.testcases = []
+
+        self.stack_created = False
+        self.status_reason = ''
+
+    def get_stack_by_id(self, id):
+        for stack in self.stacks:
+            if id == stack.stack_id:
+                return stack
+        return None
+
+    def get_stack_by_name(self, name):
+        for stack in self.stacks:
+            if name == stack.name:
+                return stack
+        return None
+
+    def create_stack(self, name, stack_type, pub_net_name, **kwargs):
+        if stack_type != 'stcv':
+            raise Exception('only support stcv stack type currently')
+
+        try:
+            stack = StcvStack(name=name,
+                              pub_net_name=pub_net_name,
+                              ntp_server_ip=kwargs.get('license_server_ip'),
+                              lab_server_ip=kwargs.get('lab_server_ip'),
+                              stcv_image=kwargs.get('stcv_image'),
+                              stcv_flavor=kwargs.get('stcv_flavor'),
+                              stcv_affinity=kwargs.get('stcv_affinity'))
+            stack.create_stack()
+            self.stacks.append(stack)
+
+        except Exception as err:
+            self.logger.error('create stack fail. err = %s', str(err))
+            raise err
+
+        return stack
+
+    def delete_stack(self, stack_id):
+        stack = self.get_stack_by_id(stack_id)
+        if stack is None:
+            raise Exception('stack does not exist, stack_id = %s', stack_id)
+
+        self.stacks.remove(stack)
+        stack.delete_stack()
+
+    def get_tc_result(self, tc_id):
+        tc = self.get_tc_by_id(tc_id)
+        return tc.get_result()
+
+    def get_tc_status(self, tc_id):
+        tc = self.get_tc_by_id(tc_id)
+        return tc.get_status()
+
+    def execute_testcase(self, name, category, stack_id, **kwargs):
+        if category != 'rfc2544':
+            raise Exception("currently only support rfc2544 test")
+
+        stack = self.get_stack_by_id(stack_id)
+        if stack is None:
+            raise Exception("defined stack not exist, stack_id = %s", stack_id)
+
+        tc = StcRfc2544Test(name=name,
+                            lab_server_ip=stack.lab_server_ip,
+                            license_server_ip=stack.ntp_server_ip,
+                            west_stcv_admin_ip=stack.get_west_stcv_ip(),
+                            west_stcv_tst_ip=stack.get_west_stcv_tst_ip(),
+                            east_stcv_admin_ip=stack.get_east_stcv_ip(),
+                            east_stcv_tst_ip=stack.get_east_stcv_tst_ip(),
+                            stack_id=stack_id,
+                            **kwargs)
+        self.testcases.append(tc)
+        tc.execute()
+
+        return tc.tc_id
+
+    def get_tc_by_id(self, id):
+        for tc in self.testcases:
+            if id == tc.tc_id:
+                return tc
+        return None
+
+    def delete_testcase(self, tc_id):
+        tc = self.get_tc_by_id(tc_id)
+
+        if tc.status == 'finished':
+            tc.delete_result()
+
+        if tc.status == 'running':
+            tc.cancel_run()
+
+        self.testcases.remove(tc)
+
+
+if __name__ == "__main__":
+    try:
+        nettest = NetTestMaster()
+        stack_params = {
+            "stcv_affinity": True,
+            "stcv_image": "stcv-4.79",
+            "stcv_flavor": "m1.tiny",
+            "lab_server_ip": "192.168.37.122",
+            "license_server_ip": "192.168.37.251"
+        }
+
+        stack = nettest.create_stack(name='stack1',
+                                     stack_type='stcv',
+                                     pub_net_name='external',
+                                     **stack_params)
+        tc_params = {
+            'metric': 'throughput',
+            'framesizes': [64, 128]
+        }
+        tc = nettest.execute_testcase(name='tc1',
+                                      category='rfc2544',
+                                      stack_id=stack.stack_id,
+                                      **tc_params)
+
+        print "test case id is %s" % tc.id
+
+        status = tc.get_status()
+        while (status != tc.TC_STATUS_FINISHED):
+            if status == tc.TC_STATUS_ERROR:
+                print "tc exectue fail, reason %s" % tc.get_err_reason()
+                break
+            sleep(2)
+        if status == tc.TC_STATUS_FINISHED:
+            print tc.get_result()
+
+        nettest.delete_testcase(tc.id)
+
+        nettest.delete_stack(stack.stack_id)
+
+    except Exception as err:
+        print err
diff --git a/contrib/nettest/nettest/requirements.txt b/contrib/nettest/nettest/requirements.txt
new file mode 100644 (file)
index 0000000..3efb124
--- /dev/null
@@ -0,0 +1,9 @@
+flask
+flask_cors
+flask_restful
+flask_restful_swagger
+#openstacksdk
+keystoneauth1
+python-heatclient
+stcrestclient
+
diff --git a/contrib/nettest/nettest/rest_server.py b/contrib/nettest/nettest/rest_server.py
new file mode 100644 (file)
index 0000000..ee13c91
--- /dev/null
@@ -0,0 +1,343 @@
+##############################################################################
+# Copyright (c) 2018 Spirent Communications and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+
+from flask import Flask, abort, jsonify, request
+from flask_cors import CORS
+from flask_restful import Api, Resource, fields
+from flask_restful_swagger import swagger
+
+from nettest import NetTestMaster
+
+app = Flask(__name__)
+CORS(app)
+api = swagger.docs(Api(app), apiVersion="1.0")
+
+stcv_master = NetTestMaster()
+
+
+@swagger.model
+class StackRequestModel:
+    resource_fields = {
+        'stack_name': fields.String,
+        'stack_type': fields.String,
+        'public_network': fields.String,
+        "stack_params": fields.Nested,
+    }
+
+
+@swagger.model
+class StackResponseModel:
+    resource_fields = {
+        'stack_name': fields.String,
+        'stack_created': fields.Boolean,
+        "stack_id": fields.String
+    }
+
+
+class Stack(Resource):
+    def __init__(self):
+        self.logger = logging.getLogger(__name__)
+
+    @swagger.operation(
+        notes='Fetch the stack configuration',
+        parameters=[
+            {
+                "name": "id",
+                "description": "The UUID of the stack in the format "
+                               "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN",
+                "required": True,
+                "type": "string",
+                "allowMultiple": False,
+                "paramType": "query"
+            },
+        ],
+        type=StackResponseModel.__name__
+    )
+    def get(self):
+        stack_id = request.args.get('id')
+        stack = stcv_master.get_stack_by_id(stack_id)
+
+        if not stack:
+            abort(404)
+
+        return jsonify({
+            'stack_name': stack.name,
+            'stack_created': True,
+            "stack_id": stack_id})
+
+    @swagger.operation(
+        notes='''set the current agent configuration and create a stack in
+              the controller. Returns once the stack create is completed.''',
+        parameters=[
+            {
+                "name": "stack",
+                "description": '''Configuration to be set. All parameters are
+            necessory.
+            ''',
+                "required": True,
+                "type": "StackRequestModel",
+                "paramType": "body"
+            }
+        ],
+        type=StackResponseModel.__name__
+    )
+    def post(self):
+        if not request.json:
+            abort(400, "ERROR: No data specified")
+
+        self.logger.info(request.json)
+
+        try:
+            params = {
+                'lab_server_ip': request.json['stack_params'].get('lab_server_ip'),
+                'license_server_ip': request.json['stack_params'].get('license_server_ip'),
+                'stcv_image': request.json['stack_params'].get('stcv_image'),
+                'stcv_flavor': request.json['stack_params'].get('stcv_flavor'),
+                'stcv_affinity': request.json['stack_params'].get('stcv_affinity')
+            }
+
+            stack = stcv_master.create_stack(name=request.json['stack_name'],
+                                             stack_type=request.json['stack_type'],
+                                             pub_net_name=request.json['public_network'],
+                                             **params)
+            if stack is None:
+                abort(400, "ERROR: create stack fail")
+
+            return jsonify({'stack_name': request.json['stack_name'],
+                            'stack_created': True,
+                            'stack_id': stack.stack_id})
+
+        except Exception as e:
+            abort(400, str(e))
+
+    @swagger.operation(
+        notes='delete deployed stack',
+        parameters=[
+            {
+                "name": "id",
+                "description": "The UUID of the stack in the format "
+                "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN",
+                "required": True,
+                "type": "string",
+                "allowMultiple": False,
+                "paramType": "query"
+            },
+        ],
+        responseMessages=[
+            {
+                "code": 200,
+                "message": "Stack ID found, response in JSON format"
+            },
+            {
+                "code": 404,
+                "message": "Stack ID not found"
+            }
+        ]
+    )
+    def delete(self):
+        try:
+            stack_id = request.args.get('id')
+            stcv_master.delete_stack(stack_id)
+        except Exception as e:
+            abort(400, str(e))
+
+
+@swagger.model
+class TestcaseRequestModel:
+    resource_fields = {
+        'name': fields.String,
+        'category': fields.String,
+        'stack_id': fields.String,
+        'params': fields.Nested
+    }
+
+
+@swagger.model
+class TestcaseResponseModel:
+    resource_fields = {
+        'name': fields.String,
+        'category': fields.String,
+        'stack_id': fields.String,
+        'tc_id': fields.String
+    }
+
+
+class TestCase(Resource):
+
+    """TestCase API"""
+
+    def __init__(self):
+        self.logger = logging.getLogger(__name__)
+
+    @swagger.operation(
+        notes='Fetch the metrics of the specified testcase',
+        parameters=[
+            {
+                "name": "id",
+                "description": "The UUID of the testcase in the format "
+                "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN",
+                "required": True,
+                "type": "string",
+                "allowMultiple": False,
+                "paramType": "query"
+            },
+            {
+                "name": "type",
+                "description": "The type of metrics to report. May be "
+                "metrics (default), metadata, or status",
+                "required": True,
+                "type": "string",
+                "allowMultiple": False,
+                "paramType": "query"
+            }
+        ],
+        responseMessages=[
+            {
+                "code": 200,
+                "message": "Workload ID found, response in JSON format"
+            },
+            {
+                "code": 404,
+                "message": "Workload ID not found"
+            }
+        ]
+    )
+    def get(self):
+        tc_id = request.args.get('id')
+        query_type = request.args.get('type')
+        ret = {}
+
+        try:
+            tc = stcv_master.get_tc_by_id(tc_id)
+            if query_type == "result":
+                ret = tc.get_result()
+
+            if query_type == "status":
+                status = tc.get_status()
+                ret['status'] = status
+                if 'error' == status:
+                    reason = tc.get_err_reason()
+                    ret['reason'] = reason
+
+            return jsonify(ret)
+
+        except Exception as err:
+            abort(400, str(err))
+
+    @swagger.operation(
+        parameters=[
+            {
+                "name": "body",
+                "description": """Start execution of a testcase with the
+parameters, only support rfc25cc test
+                """,
+                "required": True,
+                "type": "TestcaseRequestModel",
+                "paramType": "body"
+            }
+        ],
+        type=TestcaseResponseModel.__name__,
+        responseMessages=[
+            {
+                "code": 200,
+                "message": "TestCase submitted"
+            },
+            {
+                "code": 400,
+                "message": "Missing configuration data"
+            }
+        ]
+    )
+    def post(self):
+        if not request.json:
+            abort(400, "ERROR: Missing configuration data")
+
+        self.logger.info(request.json)
+
+        try:
+            name = request.json['name']
+            category = request.json['category']
+            stack_id = request.json['stack_id']
+            tc_id = stcv_master.execute_testcase(name=request.json['name'],
+                                                 category=request.json['category'],
+                                                 stack_id=request.json['stack_id'],
+                                                 **request.json['params'])
+
+            return jsonify({'name': name,
+                            'category': category,
+                            'stack_id': stack_id,
+                            'tc_id': tc_id})
+
+        except Exception as e:
+            abort(400, str(e))
+
+    @swagger.operation(
+        notes='Cancels the currently running testcase or delete testcase result',
+        parameters=[
+            {
+                "name": "id",
+                "description": "The UUID of the testcase in the format "
+                               "NNNNNNNN-NNNN-NNNN-NNNN-NNNNNNNNNNNN",
+                "required": True,
+                "type": "string",
+                "allowMultiple": False,
+                "paramType": "query"
+            },
+        ],
+        responseMessages=[
+            {
+                "code": 200,
+                "message": "Wordload ID found, response in JSON format"
+            },
+        ]
+    )
+    def delete(self):
+        try:
+            tc_id = request.args.get("id")
+            self.logger.info("receive delete testcase msg. tc_id = %s", tc_id)
+
+            stcv_master.delete_testcase(tc_id)
+
+        except Exception as e:
+            abort(400, str(e))
+
+
+api.add_resource(Stack, "/api/v1.0/stack")
+api.add_resource(TestCase, "/api/v1.0/testcase")
+
+'''
+@app.route("/")
+def hello_world():
+    return 'hello world'
+
+@app.route("/testcases")
+def get_testcases():
+    return []
+
+
+@app.route("/testcases/<int: tc_id>")
+def query_testcase(tc_id):
+    return []
+
+@app.route("/stctest/api/v1.0/testcase/<string: tc_name>", methods = ['GET'])
+def query_tc_result(tc_name):
+    return []
+
+@app.route("/stctest/api/v1.0/testcase", methods = ['POST'])
+def execut_testcase():
+    return []
+'''
+
+
+if __name__ == "__main__":
+    logger = logging.getLogger("nettest").setLevel(logging.DEBUG)
+
+    app.run(host="0.0.0.0", debug=True, threaded=True)
diff --git a/contrib/nettest/nettest/rfc2544test.py b/contrib/nettest/nettest/rfc2544test.py
new file mode 100644 (file)
index 0000000..688b4d1
--- /dev/null
@@ -0,0 +1,576 @@
+##############################################################################
+# Copyright (c) 2018 Spirent Communications and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import base64
+import copy
+import logging
+import os
+import shutil
+import threading
+from time import sleep
+import uuid
+
+import requests
+from stcrestclient import stchttp
+
+
+class Stcv2Net1Stack(object):
+    ADMIN_NETWORK_NAME = "admin"
+    ADMIN_SUBNET_ADDR = "50.50.50.0/24"
+    ADMIN_GW_IP = "50.50.50.1"
+    TST_NETWORK_NAME = "tst"
+    TST_SUBNET_ADDR = "192.168.0.0/24"
+    TST_GW_IP = "192.168.0.1"
+    ROUTER_NAME = "router"
+    WEST_STCV_NAME = "west_stcv"
+    EAST_STCV_NAME = "east_stcv"
+    AFFINITY_SG_NAME = "affinity"
+    STCV_USER_DATA = '''#cloud-config
+spirent:
+    ntp: '''
+
+    def __init__(self, name, conn, ext_network_name, params):
+        self.logger = logging.getLogger(__name__)
+
+        self.name = name
+        self.conn = conn
+        self.ext_network_name = ext_network_name
+        self.image_name = params['stcv_image']
+        self.flavor_name = params['stcv_flavor']
+        self.ntp_server_ip = params['license_server_ip']
+        self.affinity = params['stcv_affinity']
+
+        self.stack_id = str(uuid.uuid4())
+        self.admin_network = None
+        self.admin_subnet = None
+        self.tst_network = None
+        self.tst_subnet = None
+        self.ext_network = None
+        self.router = None
+        self.affinity_sg = None
+
+        self.west_stcv = None
+        self.west_stcv_ip = ''
+        self.east_stcv = None
+        self.east_stcv_ip = ''
+
+    def _deploy_test_network(self):
+
+        # create tst network and subnet
+        self.tst_network = self.conn.network.create_network(
+            name=self.TST_NETWORK_NAME)
+        self.tst_subnet = self.conn.network.create_subnet(
+            name=self.TST_NETWORK_NAME + '_subnet',
+            network_id=self.tst_network.id,
+            ip_version='4',
+            cidr=self.TST_SUBNET_ADDR,
+            gateway_ip=self.TST_GW_IP,
+            is_dhcp_enabled=True)
+
+        # create admin network and subnet
+        self.admin_network = self.conn.network.create_network(
+            name=self.ADMIN_NETWORK_NAME)
+        self.admin_subnet = self.conn.network.create_subnet(
+            name=self.ADMIN_NETWORK_NAME + '_subnet',
+            network_id=self.admin_network.id,
+            ip_version='4',
+            cidr=self.ADMIN_SUBNET_ADDR,
+            gateway_ip=self.ADMIN_GW_IP,
+            is_dhcp_enabled=True)
+
+        # create external gateway and connect admin subnet to router
+        self.ext_network = self.conn.network.find_network(self.ext_network_name)
+        self.router = self.conn.network.create_router(name=self.ROUTER_NAME,
+                                                      external_gateway_info={"network_id": self.ext_network.id},
+                                                      is_admin_state_up=True)
+        self.conn.network.add_interface_to_router(self.router, subnet_id=self.admin_subnet.id)
+
+    def _depoly_stcv(self, name, image_id, flavor_id, scheduler_hints, user_data):
+
+        stcv = self.conn.compute.create_server(
+            name=name, image_id=image_id, flavor_id=flavor_id,
+            networks=[{"uuid": self.admin_network.id}, {"uuid": self.tst_network.id}],
+            config_drive=True,
+            user_data=base64.encodestring(user_data)
+        )
+        stcv = self.conn.compute.wait_for_server(stcv)
+
+        stcv_fixed_ip = stcv.addresses[self.admin_network.name][0]['addr']
+        stcv_floating_ip = self.conn.network.create_ip(floating_network_id=self.ext_network.id)
+        self.conn.compute.add_floating_ip_to_server(server=stcv, address=stcv_floating_ip.floating_ip_address,
+                                                    fixed_address=stcv_fixed_ip)
+
+        return {'stcv': stcv, 'fixed_ip': stcv_fixed_ip, 'floating_ip': stcv_floating_ip}
+
+    def create_stack(self):
+
+        image = self.conn.compute.find_image(self.image_name)
+        flavor = self.conn.compute.find_flavor(self.flavor_name)
+
+        if self.affinity:
+            self.affinity_sg = \
+                self.conn.compute.create_server_group(name=self.AFFINITY_SG_NAME,
+                                                      policies=["affinity"])
+        else:
+            self.affinity_sg = \
+                self.conn.compute.create_server_group(name=self.AFFINITY_SG_NAME,
+                                                      policies=["anti-affinity"])
+        self._deploy_test_network()
+
+        user_data = self.STCV_USER_DATA + self.ntp_server_ip
+
+        stcv = self._depoly_stcv(name=self.WEST_STCV_NAME,
+                                 image_id=image.id,
+                                 flavor_id=flavor.id,
+                                 scheduler_hints=self.affinity_sg,
+                                 user_data=user_data)
+        self.west_stcv = stcv['stcv']
+        self.west_stcv_ip = stcv['floating_ip']
+
+        stcv = self._depoly_stcv(name=self.EAST_STCV_NAME,
+                                 image_id=image.id,
+                                 flavor_id=flavor.id,
+                                 scheduler_hints=self.affinity_sg,
+                                 user_data=user_data)
+        self.east_stcv = stcv['stcv']
+        self.east_stcv_ip = stcv['floating_ip']
+
+    def delete_stack(self):
+
+        self.conn.compute.delete_server(self.west_stcv, ignore_missing=True)
+        self.conn.compute.delete_server(self.east_stcv, ignore_missing=True)
+
+        self.conn.compute.delete_server_group(server_group=self.affinity_sg,
+                                              ignore_missing=True)
+
+        # delete external gateway
+        self.conn.network.delete_router(self.router, ignore_missing=True)
+
+        # delete tst network
+        self.conn.network.delete_subnet(self.tst_subnet, ignore_missing=True)
+        self.conn.network.delete_network(self.tst_network, ignore_missing=True)
+
+        # delete admin network
+        self.conn.network.delete_subnet(self.admin_subnet, ignore_missing=True)
+        self.conn.network.delete_network(self.admin_network, ignore_missing=True)
+
+
+class StcSession:
+    """ wrapper class for stc session"""
+
+    def __init__(self, labserver_addr, user_name, session_name):
+        self.logger = logging.getLogger(__name__)
+
+        # create connection obj
+        self.stc = stchttp.StcHttp(labserver_addr)
+        self.user_name = user_name
+        self.session_name = session_name
+
+        # create session on labserver
+        self.session_id = self.stc.new_session(self.user_name, self.session_name)
+        self.stc.join_session(self.session_id)
+        return
+
+    def __del__(self):
+        # destroy resource on labserver
+        self.stc.end_session()
+
+    def clean_all_session(self):
+        session_urls = self.stc.session_urls()
+        for session in session_urls:
+            resp = requests.delete(session)
+            self.logger.info("delete session resp: %s", str(resp))
+        return
+
+
+class StcRfc2544Test:
+    """ RFC2544 test class"""
+
+    RESULT_PATH_PREFIX = './tc_results/rfc2544/'
+    TC_STATUS_INIT = 'init'
+    TC_STATUS_RUNNING = 'running'
+    TC_STATUS_FINISHED = 'finished'
+    TC_STATUS_ERROR = 'error'
+
+    default_additional_params = {
+        "AcceptableFrameLoss": 0.0,
+        "Duration": 60,
+        "FrameSizeList": 64,
+        "LearningMode": 'AUTO',
+        "NumOfTrials": 1,
+        "RateInitial": 99.0,
+        "RateLowerLimit": 99.0,
+        "RateStep": 10.0,
+        "RateUpperLimit": 99.0,
+        "Resolution": 1.0,
+        "SearchMode": 'BINARY',
+        "TrafficPattern": 'PAIR'
+    }
+
+    def __init__(self, name, lab_server_ip, license_server_ip,
+                 west_stcv_admin_ip, west_stcv_tst_ip,
+                 east_stcv_admin_ip, east_stcv_tst_ip,
+                 stack_id=None, **kwargs):
+        self.logger = logging.getLogger(__name__)
+
+        self.name = name
+        self.lab_server_ip = lab_server_ip
+        self.license_server_ip = license_server_ip
+        self.west_stcv_ip = west_stcv_admin_ip
+        self.west_stcv_tst_ip = west_stcv_tst_ip
+        self.east_stcv_ip = east_stcv_admin_ip
+        self.east_stcv_tst_ip = east_stcv_tst_ip
+        self.stack_id = stack_id
+        self.metric = kwargs.get('metric')
+        self.additional_params = copy.copy(self.default_additional_params)
+        self.additional_params['FrameSizeList'] = kwargs.get('framesizes')
+
+        self.tc_id = str(uuid.uuid4())
+
+        self.stc = None
+        self.sess = None
+        self.executor = None
+        self.status = 'init'
+        self.err_reason = ''
+
+    def config_license(self, license_server_addr):
+        license_mgr = self.stc.get("system1", "children-licenseservermanager")
+        self.stc.create("LicenseServer",
+                        under=license_mgr,
+                        attributes={"server": license_server_addr})
+        return
+
+    def create_project(self, traffic_custom=None):
+        self.project = self.stc.get("System1", "children-Project")
+        # Configure any custom traffic parameters
+        if traffic_custom == "cont":
+            self.stc.create("ContinuousTestConfig", under=self.project)
+        return
+
+    def config_test_port(self, chassis_addr, slot_no, port_no, intf_addr, gateway_addr):
+        # create test port
+        port_loc = "//%s/%s/%s" % (chassis_addr, slot_no, port_no)
+        chassis_port = self.stc.create('port', self.project)
+        self.stc.config(chassis_port, {'location': port_loc})
+
+        # Create emulated genparam for east port
+        device_gen_params = self.stc.create("EmulatedDeviceGenParams",
+                                            under=self.project,
+                                            attributes={"Port": chassis_port})
+        # Create the DeviceGenEthIIIfParams object
+        self.stc.create("DeviceGenEthIIIfParams",
+                        under=device_gen_params,
+                        attributes={"UseDefaultPhyMac": "True"})
+
+        # Configuring Ipv4 interfaces
+        self.stc.create("DeviceGenIpv4IfParams",
+                        under=device_gen_params,
+                        attributes={"Addr": intf_addr, "Gateway": gateway_addr})
+
+        # Create Devices using the Device Wizard
+        self.stc.perform("DeviceGenConfigExpand",
+                         params={"DeleteExisting": "No", "GenParams": device_gen_params})
+
+        return
+
+    def do_test(self):
+        if self.metric == "throughput":
+            self.stc.perform("Rfc2544SetupThroughputTestCommand", self.additional_params)
+        elif self.metric == "backtoback":
+            self.stc.perform("Rfc2544SetupBackToBackTestCommand", self.additional_params)
+        elif self.metric == "frameloss":
+            self.stc.perform("Rfc2544SetupFrameLossTestCommand", self.additional_params)
+        elif self.metric == "latency":
+            self.stc.perform("Rfc2544SetupLatencyTestCommand", self.additional_params)
+        else:
+            raise Exception("invalid rfc2544 test metric.")
+
+        # Save the configuration
+        self.stc.perform("SaveToTcc", params={"Filename": "2544.tcc"})
+
+        # Connect to the hardware...
+        self.stc.perform("AttachPorts",
+                         params={"portList": self.stc.get("system1.project", "children-port"),
+                                 "autoConnect": "TRUE"})
+
+        # Apply configuration.
+        self.stc.apply()
+        self.stc.perform("SequencerStart")
+        self.stc.wait_until_complete()
+
+        return
+
+    def write_query_results_to_csv(self, results_path, csv_results_file_prefix, query_results):
+        filec = os.path.join(results_path, csv_results_file_prefix + ".csv")
+        with open(filec, "wb") as result_file:
+            result_file.write(query_results["Columns"].replace(" ", ",") + "\n")
+            for row in (query_results["Output"].replace("} {", ",").replace("{", "").replace("}", "").split(",")):
+                result_file.write(row.replace(" ", ",") + "\n")
+
+    def format_result(self, metric, original_result_dict):
+        result = {}
+        if metric == 'throughput':
+            columns = original_result_dict["Columns"].split(' ')
+            index_framesize = columns.index("ConfiguredFrameSize")
+            index_result = columns.index("Result")
+            index_throughput = columns.index("Throughput(%)")
+            index_ForwardingRate = columns.index("ForwardingRate(fps)")
+            outputs = \
+                original_result_dict["Output"].replace('} {', ',').replace("{", "").replace("}", "").split(",")
+
+            for row in outputs:
+                output = row.split(' ')
+                result[output[index_framesize]] = {'Result': output[index_result],
+                                                   "Throughput(%)": output[index_throughput],
+                                                   "ForwardingRate(fps)": output[index_ForwardingRate]}
+
+        elif self.metric == "latency":
+            pass
+
+        elif self.metric == "frameloss":
+            pass
+
+        elif self.metric == "backtoback":
+            pass
+
+        return result
+
+    def collect_result(self, local_dir):
+        # Determine what the results database filename is...
+        lab_server_resultsdb = self.stc.get(
+            "system1.project.TestResultSetting", "CurrentResultFileName")
+        self.stc.perform("CSSynchronizeFiles",
+                         params={"DefaultDownloadDir": local_dir})
+
+        resultsdb = local_dir + lab_server_resultsdb.split("/Results")[1]
+
+        if not os.path.exists(resultsdb):
+            resultsdb = lab_server_resultsdb
+            self.logger.info("Failed to create the local summary DB File, using"
+                             " the remote DB file instead.")
+        else:
+            self.logger.info(
+                "The local summary DB file has been saved to %s", resultsdb)
+
+        if self.metric == "throughput":
+            resultsdict = self.stc.perform("QueryResult",
+                                           params={
+                                               "DatabaseConnectionString": lab_server_resultsdb,
+                                               "ResultPath": "RFC2544ThroughputTestResultDetailedSummaryView"})
+        elif self.metric == "backtoback":
+            resultsdict = self.stc.perform("QueryResult",
+                                           params={
+                                               "DatabaseConnectionString": lab_server_resultsdb,
+                                               "ResultPath": "RFC2544Back2BackTestResultDetailedSummaryView"})
+        elif self.metric == "frameloss":
+            resultsdict = self.stc.perform("QueryResult",
+                                           params={
+                                               "DatabaseConnectionString": lab_server_resultsdb,
+                                               "ResultPath": "RFC2544LatencyTestResultDetailedSummaryView"})
+        elif self.metric == "latency":
+            resultsdict = self.stc.perform("QueryResult",
+                                           params={
+                                               "DatabaseConnectionString": lab_server_resultsdb,
+                                               "ResultPath": "RFC2544FrameLossTestResultDetailedSummaryView"})
+        else:
+            raise Exception("invalid rfc2544 test metric.")
+
+        self.write_query_results_to_csv(self.results_dir, self.metric, resultsdict)
+
+        self.result = self.format_result(self.metric, resultsdict)
+
+        return
+
+    def thread_entry(self):
+        self.status = self.TC_STATUS_RUNNING
+        try:
+            # create session on lab server
+            self.sess = StcSession(self.lab_server_ip, session_name=self.name, user_name=self.name)
+            self.stc = self.sess.stc
+
+            # create test result directory
+            self.results_dir = self.RESULT_PATH_PREFIX + self.tc_id + '/'
+            os.makedirs(self.results_dir)
+
+            # Bring up license server
+            self.config_license(self.license_server_ip)
+
+            self.logger.info("config license success, license_server_addr = %s.", self.license_server_ip)
+
+            # Create the root project object and Configure any custom traffic parameters
+            self.create_project()
+
+            self.logger.info("create project success.")
+
+            # configure test port
+            self.config_test_port(self.west_stcv_ip, 1, 1, self.west_stcv_tst_ip, self.east_stcv_tst_ip)
+            self.config_test_port(self.east_stcv_ip, 1, 1, self.east_stcv_tst_ip, self.west_stcv_tst_ip)
+
+            self.logger.info("config test port success, west_chassis_addr = %s, east_chassis_addr = %s.",
+                             self.west_stcv_ip, self.east_stcv_ip)
+
+            # execute test
+            self.do_test()
+
+            self.logger.info("execute test success.")
+
+            # collect test result
+            self.collect_result(self.results_dir)
+
+            self.logger.info("collect result file success, results_dir = %s.", self.results_dir)
+
+            self.status = self.TC_STATUS_FINISHED
+
+        except Exception as err:
+            self.logger.error("Failed to execute Rfc2544 testcase, err: %s", str(err))
+            self.err_reason = str(err)
+            self.status = self.TC_STATUS_ERROR
+
+        finally:
+            if self.sess is not None:
+                self.sess.clean_all_session()
+
+    def execute(self):
+
+        self.executor = threading.Thread(name='rfc2544', target=self.thread_entry())
+        self.executor.start()
+
+    def get_result(self):
+        if self.status != self.TC_STATUS_FINISHED:
+            return {'name': self.name,
+                    'tc_id': self.tc_id,
+                    'status': self.status
+                    }
+
+        return {'name': self.name,
+                'category': 'rfc2544',
+                'id': self.tc_id,
+                'params': {
+                    'metric': self.metric,
+                    'framesizes': self.additional_params.get('FrameSizeList')},
+                'result': self.result}
+
+    def get_status(self):
+        return self.status
+
+    def delete_result(self):
+        shutil.rmtree(self.results_dir)
+        pass
+
+    def cancel_run(self):
+        pass
+
+    def get_err_reason(self):
+        return self.err_reason
+
+
+if __name__ == '__main__':
+
+    lab_server_ip = '192.168.37.122'
+    license_server_ip = '192.168.37.251'
+    west_stcv_admin_ip = '192.168.37.202'
+    west_stcv_tst_ip = '192.168.1.20'
+    east_stcv_admin_ip = '192.168.37.212'
+    east_stcv_tst_ip = '192.168.1.17'
+
+    tc = StcRfc2544Test(name='tc1',
+                        lab_server_ip=lab_server_ip,
+                        license_server_ip=license_server_ip,
+                        west_stcv_admin_ip=west_stcv_admin_ip,
+                        west_stcv_tst_ip=west_stcv_tst_ip,
+                        east_stcv_admin_ip=east_stcv_admin_ip,
+                        east_stcv_tst_ip=east_stcv_tst_ip,
+                        metric="throughput",
+                        framesizes=[64, 128, 256, 512, 1024])
+    tc.execute()
+    status = tc.get_status()
+    while(status != tc.TC_STATUS_FINISHED):
+        if status == tc.TC_STATUS_ERROR:
+            print "tc exectue fail, reason %s" % tc.get_err_reason()
+            break
+        sleep(2)
+    if status == tc.TC_STATUS_FINISHED:
+        print tc.get_result()
+'''
+    tc = StcRfc2544Test(name='tc2',
+                        lab_server_ip=lab_server_ip,
+                        license_server_ip=license_server_ip,
+                        west_stcv_admin_ip=west_stcv_admin_ip,
+                        west_stcv_tst_ip=west_stcv_tst_ip,
+                        east_stcv_admin_ip=east_stcv_admin_ip,
+                        east_stcv_tst_ip=east_stcv_tst_ip,
+                        metric="latency",
+                        framesizes=[64, 128, 256, 512, 1024])
+    tc.execute()
+    status = tc.get_status()
+    while(status != tc.TC_STATUS_FINISHED):
+        if status == tc.TC_STATUS_ERROR:
+            print "tc exectue fail, reason %s" % tc.get_err_reason()
+            break
+        sleep(2)
+    if status == tc.TC_STATUS_FINISHED:
+        print tc.get_result()
+
+    tc = StcRfc2544Test(name='tc3',
+                        lab_server_ip=lab_server_ip,
+                        license_server_ip=license_server_ip,
+                        west_stcv_admin_ip=west_stcv_admin_ip,
+                        west_stcv_tst_ip=west_stcv_tst_ip,
+                        east_stcv_admin_ip=east_stcv_admin_ip,
+                        east_stcv_tst_ip=east_stcv_tst_ip,
+                        metric="backtoback",
+                        framesizes=[64, 128, 256, 512, 1024])
+    tc.execute()
+    status = tc.get_status()
+    while(status != tc.TC_STATUS_FINISHED):
+        if status == tc.TC_STATUS_ERROR:
+            print "tc exectue fail, reason %s" % tc.get_err_reason()
+            break
+        sleep(2)
+    if status == tc.TC_STATUS_FINISHED:
+        print tc.get_result()
+
+    tc = StcRfc2544Test(name='tc4',
+                        lab_server_ip=lab_server_ip,
+                        license_server_ip=license_server_ip,
+                        west_stcv_admin_ip=west_stcv_admin_ip,
+                        west_stcv_tst_ip=west_stcv_tst_ip,
+                        east_stcv_admin_ip=east_stcv_admin_ip,
+                        east_stcv_tst_ip=east_stcv_tst_ip,
+                        metric="frameloss",
+                        framesizes=[64, 128, 256, 512, 1024])
+    tc.execute()
+    status = tc.get_status()
+    while(status != tc.TC_STATUS_FINISHED):
+        if status == tc.TC_STATUS_ERROR:
+            print "tc exectue fail, reason %s" % tc.get_err_reason()
+            break
+        sleep(2)
+    if status == tc.TC_STATUS_FINISHED:
+        print tc.get_result()
+'''
+
+'''
+class Testcase(object):
+
+    def __init__(self, stack):
+        self.stack = stack
+
+    def execute(self):
+        pass
+
+class TestcaseFactory(object):
+
+    def __init__(self):
+
+    def create_tc(self, tc_metadata):
+        self.tc_name = tc_metadata['tc_name']
+        self.tc_id = str(uuid.uuid4())
+        if
+'''
diff --git a/contrib/nettest/nettest/start.sh b/contrib/nettest/nettest/start.sh
new file mode 100644 (file)
index 0000000..12ae3eb
--- /dev/null
@@ -0,0 +1,11 @@
+#!/bin/bash
+##############################################################################
+# Copyright (c) 2018 Spirent Communications and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+exec /usr/bin/python rest_server.py
diff --git a/contrib/nettest/nettest/stcv_stack.py b/contrib/nettest/nettest/stcv_stack.py
new file mode 100644 (file)
index 0000000..6e69f47
--- /dev/null
@@ -0,0 +1,174 @@
+##############################################################################
+# Copyright (c) 2018 Spirent Communications and others.
+#
+# All rights reserved. This program and the accompanying materials
+# are made available under the terms of the Apache License, Version 2.0
+# which accompanies this distribution, and is available at
+# http://www.apache.org/licenses/LICENSE-2.0
+##############################################################################
+
+import logging
+import os
+from time import sleep
+import traceback
+
+import heatclient.client as heatclient
+from keystoneauth1 import loading
+from keystoneauth1 import session
+
+
+class StcvStack(object):
+    STCV_CONFIG_FILE = 'stcv_config_file'
+    STCV_HEAT_FILE = './heat_2stcv.yaml'
+    STCV_USER_DATA = '''#cloud-config
+    spirent:
+        ntp: '''
+
+    def __init__(self, name, **kwargs):
+        self.logger = logging.getLogger(__name__)
+
+        self.name = name
+        self.pub_net_name = kwargs.get('pub_net_name')
+        self.ntp_server_ip = kwargs.get('ntp_server_ip')
+        self.lab_server_ip = kwargs.get('lab_server_ip')
+        self.stcv_image = kwargs.get('stcv_image')
+        self.stcv_flavor = kwargs.get('stcv_flavor')
+        if kwargs.get('stcv_affinity'):
+            self.stcv_affinity = 'affinity'
+        else:
+            self.stcv_affinity = 'anti-affinity'
+
+        self.stack_id = None
+        self._heatc_lient = None
+
+    def _attach_to_openstack(self):
+        creds = {"username": os.environ.get('OS_USERNAME'),
+                 "password": os.environ.get('OS_PASSWORD'),
+                 "auth_url": os.environ.get('OS_AUTH_URL'),
+                 "project_domain_id": os.environ.get('OS_PROJECT_DOMAIN_ID'),
+                 "project_domain_name": os.environ.get('OS_PROJECT_DOMAIN_NAME'),
+                 "project_id": os.environ.get('OS_PROJECT_ID'),
+                 "project_name": os.environ.get('OS_PROJECT_NAME'),
+                 "tenant_name": os.environ.get('OS_TENANT_NAME'),
+                 "tenant_id": os.environ.get("OS_TENANT_ID"),
+                 "user_domain_id": os.environ.get('OS_USER_DOMAIN_ID'),
+                 "user_domain_name": os.environ.get('OS_USER_DOMAIN_NAME')
+                 }
+
+        self.logger.debug("Creds: %s" % creds)
+
+        loader = loading.get_plugin_loader('password')
+        auth = loader.load_from_options(**creds)
+        sess = session.Session(auth)
+        self._heat_client = heatclient.Client("1", session=sess)
+
+    def _make_parameters(self):
+        user_data = self.STCV_USER_DATA + self.ntp_server_ip
+        file_path = os.getcwd() + '/' + self.STCV_CONFIG_FILE
+        fd = open(file_path, 'w')
+        fd.writelines(user_data)
+        fd.close()
+
+        return {
+            'public_net_name': self.pub_net_name,
+            'stcv_image': self.stcv_image,
+            'stcv_flavor': self.stcv_flavor,
+            'stcv_sg_affinity': self.stcv_affinity,
+            'ntp_server_ip': self.ntp_server_ip
+        }
+
+    def acquire_ip_from_stack_output(self, output, key_name):
+        ip = None
+        for item in output:
+            if item['output_key'] == key_name:
+                ip = item['output_value']
+                if isinstance(ip, list):
+                    ip = ip[0]['ip_address']
+                break
+
+        return ip
+
+    def create_stack(self):
+        with open(self.STCV_HEAT_FILE) as fd:
+            template = fd.read()
+
+        self._attach_to_openstack()
+
+        self.logger.debug("Creating stack")
+
+        stack = self._heat_client.stacks.create(
+            stack_name=self.name,
+            template=template,
+            parameters=self._make_parameters())
+
+        self.stack_id = stack['stack']['id']
+
+        while True:
+            stack = self._heat_client.stacks.get(self.stack_id)
+            status = getattr(stack, 'stack_status')
+            self.logger.debug("Stack status=%s" % (status,))
+            if (status == u'CREATE_COMPLETE'):
+                self.stcv1_ip = self.acquire_ip_from_stack_output(stack.outputs, "STCv_1_Mgmt_Ip")
+                self.stcv2_ip = self.acquire_ip_from_stack_output(stack.outputs, "STCv_2_Mgmt_Ip")
+                self.stcv1_tst_ip = self.acquire_ip_from_stack_output(stack.outputs, "STCv_1_Tst_Ip")
+                self.stcv2_tst_ip = self.acquire_ip_from_stack_output(stack.outputs, "STCv_2_Tst_Ip")
+                break
+            if (status == u'DELETE_COMPLETE'):
+                self.stack_id = None
+                break
+            if (status == u'CREATE_FAILED'):
+                self.status_reason = getattr(stack, 'stack_status_reason')
+                sleep(5)
+                self._heat_client.stacks.delete(stack_id=self.stack_id)
+            sleep(2)
+
+    def delete_stack(self):
+        if self.stack_id is None:
+            raise Exception('stack does not exist')
+
+        self._attach_to_openstack()
+        while True:
+            stack = self._heat_client.stacks.get(self.stack_id)
+            status = getattr(stack, 'stack_status')
+            self.logger.debug("Stack status=%s" % (status,))
+            if (status == u'CREATE_COMPLETE'):
+                self._heat_client.stacks.delete(stack_id=self.stack_id)
+            if (status == u'DELETE_COMPLETE'):
+                self.stack_id = None
+                break
+            if (status == u'DELETE_FAILED'):
+                sleep(5)
+                self._heat_client.stacks.delete(stack_id=self.stack_id)
+            sleep(2)
+
+    def get_west_stcv_ip(self):
+        return self.stcv1_ip
+
+    def get_west_stcv_tst_ip(self):
+        return self.stcv1_tst_ip
+
+    def get_east_stcv_ip(self):
+        return self.stcv2_ip
+
+    def get_east_stcv_tst_ip(self):
+        return self.stcv2_tst_ip
+
+
+if __name__ == '__main__':
+    try:
+        stack = StcvStack(name='stack1',
+                          pub_net_name='external',
+                          ntp_server_ip='192.168.37.151',
+                          stcv_image='stcv-4.79',
+                          stcv_flavor='m1.tiny',
+                          affinity=False)
+        stack.create_stack()
+
+        print stack.get_east_stcv_ip()
+        print stack.get_east_stcv_tst_ip()
+        print stack.get_west_stcv_ip()
+        print stack.get_west_stcv_tst_ip()
+
+    except Exception as err:
+        excstr = traceback.format_exc()
+        print excstr