From 15ac21b3a3b72f8a1242e8a72cdec20ab4494186 Mon Sep 17 00:00:00 2001 From: Qiang Dai Date: Fri, 23 Feb 2018 15:54:05 +0800 Subject: support rfc2544 test using spirent virtual test center 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 --- contrib/nettest/Dockerfile | 47 +++ contrib/nettest/README.md | 0 contrib/nettest/nettest/heat_2stcv.yaml | 170 +++++++++ contrib/nettest/nettest/nettest.py | 157 +++++++++ contrib/nettest/nettest/requirements.txt | 9 + contrib/nettest/nettest/rest_server.py | 343 ++++++++++++++++++ contrib/nettest/nettest/rfc2544test.py | 576 +++++++++++++++++++++++++++++++ contrib/nettest/nettest/start.sh | 11 + contrib/nettest/nettest/stcv_stack.py | 174 ++++++++++ 9 files changed, 1487 insertions(+) create mode 100644 contrib/nettest/Dockerfile create mode 100644 contrib/nettest/README.md create mode 100644 contrib/nettest/nettest/heat_2stcv.yaml create mode 100644 contrib/nettest/nettest/nettest.py create mode 100644 contrib/nettest/nettest/requirements.txt create mode 100644 contrib/nettest/nettest/rest_server.py create mode 100644 contrib/nettest/nettest/rfc2544test.py create mode 100644 contrib/nettest/nettest/start.sh create mode 100644 contrib/nettest/nettest/stcv_stack.py diff --git a/contrib/nettest/Dockerfile b/contrib/nettest/Dockerfile new file mode 100644 index 00000000..a0ecabf9 --- /dev/null +++ b/contrib/nettest/Dockerfile @@ -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 index 00000000..e69de29b diff --git a/contrib/nettest/nettest/heat_2stcv.yaml b/contrib/nettest/nettest/heat_2stcv.yaml new file mode 100644 index 00000000..77c6e6e8 --- /dev/null +++ b/contrib/nettest/nettest/heat_2stcv.yaml @@ -0,0 +1,170 @@ +############################################################################## +# 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 +############################################################################## + +heat_template_version: 2016-10-14 + +description: Template for deploying 2 STCv and 1 labserver + +parameters: + public_net_name: {default: external, description: Public network to allocate floating IPs to VMs', type: string} + #public_net_id: {description: public_network id for exernal connectivity,type: string} + mgmt_net_name: {default: admin, description: Name of STCv mgmt network to be created, type: string} + mgmt_net_cidr: {default: 10.10.10.0/24, description: STCv mgmt network CIDR,type: string} + mgmt_net_gw: {default: 10.10.10.1, description: STCv mgmt network gateway address, type: string} + mgmt_net_pool_start: {default: 10.10.10.10, description: Start of mgmt network IP address allocation pool, type: string} + mgmt_net_pool_end: {default: 10.10.10.20, description: End of mgmt network IP address allocation pool, type: string} + tst_net_name: {default: tst, description: Name of STCv private network to be created, type: string} + tst_net_cidr: {default: 192.168.1.0/24, description: STCv private network CIDR,type: string} + tst_net_gw: {default: 192.168.1.1, description: STCv private network gateway address, type: string} + tst_net_pool_start: {default: 192.168.1.10, description: Start of private network IP address allocation pool, type: string} + tst_net_pool_end: {default: 192.168.1.20, description: End of private network IP address allocation pool, type: string} + stcv_image: {default: "stcv-4.79", description: Image name to use for STCv, type: string} + stcv_flavor: {default: "m1.tiny", description: Flavor to use for STCv, type: string} + #stcv_user_data: {default: "", description: user data such as ntp server ip for stcv, type: string} + #stcv_config_file: {default: "stcv_config_file", description: user data such as ntp server ip for stcv, type: string} + ntp_server_ip: {default: "", description: user data such as ntp server ip for stcv, type: string} + stcv_sg_name: {default: stcv_sg, description: server group name, type: string} + stcv_sg_affinity: {default: affinity, description: server group affinity for stcv, type: string} + +resources: + stcv_server_group: + type: OS::Nova::ServerGroup + properties: + name: {get_param: stcv_sg_name} + policies: [{get_param: stcv_sg_affinity}] + mgmt_net: + type: OS::Neutron::Net + properties: + name: {get_param: mgmt_net_name} + mgmt_net_subnet: + type: OS::Neutron::Subnet + properties: + allocation_pools: + - end: {get_param: mgmt_net_pool_end} + start: {get_param: mgmt_net_pool_start} + cidr: {get_param: mgmt_net_cidr} + gateway_ip: {get_param: mgmt_net_gw} + network: {get_resource: mgmt_net} + public_router: + type: OS::Neutron::Router + properties: + external_gateway_info: + network: {get_param: public_net_name} + router_interface: + type: OS::Neutron::RouterInterface + properties: + router: {get_resource: public_router} + subnet: {get_resource: mgmt_net_subnet} + tst_net: + type: OS::Neutron::Net + properties: + name: {get_param: tst_net_name} + tst_subnet: + type: OS::Neutron::Subnet + properties: + allocation_pools: + - end: {get_param: tst_net_pool_end} + start: {get_param: tst_net_pool_start} + cidr: {get_param: tst_net_cidr} + gateway_ip: {get_param: tst_net_gw} + network: {get_resource: tst_net} + stcv_1_port_1: + type: OS::Neutron::Port + properties: + network: {get_resource: mgmt_net} + fixed_ips: + - subnet: {get_resource: mgmt_net_subnet} + floating_ip1: + type: OS::Neutron::FloatingIP + properties: + floating_network: {get_param: public_net_name} + port_id: {get_resource: stcv_1_port_1} + stcv_1_port_2: + type: OS::Neutron::Port + properties: + network: {get_resource: tst_net} + port_security_enabled: False + fixed_ips: + - subnet: {get_resource: tst_subnet} + STCv_1: + type: OS::Nova::Server + properties: + #availability_zone : {get_param: availability_zone_name} + flavor: {get_param: stcv_flavor} + image: {get_param: stcv_image} + name: STCv_1 + user_data: + str_replace: + template: | + #cloud-config + spirent: + ntp: $ntp_server_ip + params: + $ntp_server_ip: {get_param: ntp_server_ip} + user_data_format: RAW + config_drive: True + scheduler_hints: + group: {get_resource: stcv_server_group} + networks: + - port: {get_resource: stcv_1_port_1} + - port: {get_resource: stcv_1_port_2} + stcv_2_port_1: + type: OS::Neutron::Port + properties: + network: {get_resource: mgmt_net} + fixed_ips: + - subnet: {get_resource: mgmt_net_subnet} + floating_ip2: + type: OS::Neutron::FloatingIP + properties: + floating_network: {get_param: public_net_name} + port_id: {get_resource: stcv_2_port_1} + stcv_2_port_2: + type: OS::Neutron::Port + properties: + network: {get_resource: tst_net} + port_security_enabled: False + fixed_ips: + - subnet: {get_resource: tst_subnet} + STCv_2: + type: OS::Nova::Server + properties: + #availability_zone : {get_param: availability_zone_name} + flavor: {get_param: stcv_flavor} + image: {get_param: stcv_image} + name: STCv_2 + user_data: + str_replace: + template: | + #cloud-config + spirent: + ntp: $ntp_server_ip + params: + $ntp_server_ip: {get_param: ntp_server_ip} + user_data_format: RAW + config_drive: True + scheduler_hints: + group: {get_resource: stcv_server_group} + networks: + - port: {get_resource: stcv_2_port_1} + - port: {get_resource: stcv_2_port_2} +outputs: + STCv_1_Mgmt_Ip: + value: {get_attr: [floating_ip1, floating_ip_address]} + description: STCv_1 Mgmt IP + STCv_2_Mgmt_Ip: + value: {get_attr: [floating_ip2, floating_ip_address]} + description: STCv_2 Mgmt IP + STCv_1_Tst_Ip: + value: {get_attr: [stcv_1_port_2, fixed_ips]} + description: STCv_1 Tst IP + STCv_2_Tst_Ip: + value: {get_attr: [stcv_2_port_2, fixed_ips]} + description: STCv_2 Tst IP + diff --git a/contrib/nettest/nettest/nettest.py b/contrib/nettest/nettest/nettest.py new file mode 100644 index 00000000..c5a203e0 --- /dev/null +++ b/contrib/nettest/nettest/nettest.py @@ -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 index 00000000..3efb124b --- /dev/null +++ b/contrib/nettest/nettest/requirements.txt @@ -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 index 00000000..ee13c91b --- /dev/null +++ b/contrib/nettest/nettest/rest_server.py @@ -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/") +def query_testcase(tc_id): + return [] + +@app.route("/stctest/api/v1.0/testcase/", 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 index 00000000..688b4d12 --- /dev/null +++ b/contrib/nettest/nettest/rfc2544test.py @@ -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 index 00000000..12ae3eb0 --- /dev/null +++ b/contrib/nettest/nettest/start.sh @@ -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 index 00000000..6e69f479 --- /dev/null +++ b/contrib/nettest/nettest/stcv_stack.py @@ -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 -- cgit 1.2.3-korg