aboutsummaryrefslogtreecommitdiffstats
path: root/functest/opnfv_tests/vnf/ims/ixia/utils
diff options
context:
space:
mode:
authorCedric Ollivier <cedric.ollivier@orange.com>2017-09-14 21:26:15 +0000
committerGerrit Code Review <gerrit@opnfv.org>2017-09-14 21:26:16 +0000
commitf45b9587c5568c0b64cfedb94e4241e485ffce47 (patch)
tree09cfe848259df5bf3c6617a4c85a2bef6ccd1f5e /functest/opnfv_tests/vnf/ims/ixia/utils
parent890076da4b4d80c392c7e952282e9c721508b0d6 (diff)
parent2f9219dfa44982779990e13c177a703f2239b488 (diff)
Merge "New testcase creation named "cloudify_ims_perf""
Diffstat (limited to 'functest/opnfv_tests/vnf/ims/ixia/utils')
-rw-r--r--functest/opnfv_tests/vnf/ims/ixia/utils/IxChassisUtils.py150
-rw-r--r--functest/opnfv_tests/vnf/ims/ixia/utils/IxLoadUtils.py397
-rw-r--r--functest/opnfv_tests/vnf/ims/ixia/utils/IxRestUtils.py192
-rw-r--r--functest/opnfv_tests/vnf/ims/ixia/utils/__init__.py0
4 files changed, 739 insertions, 0 deletions
diff --git a/functest/opnfv_tests/vnf/ims/ixia/utils/IxChassisUtils.py b/functest/opnfv_tests/vnf/ims/ixia/utils/IxChassisUtils.py
new file mode 100644
index 00000000..973e0264
--- /dev/null
+++ b/functest/opnfv_tests/vnf/ims/ixia/utils/IxChassisUtils.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017 IXIA 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 httplib2
+import json
+import logging
+
+
+okStates = [200, 201, 202]
+states = [
+ 'Queued',
+ 'In Progress',
+ 'Manual Step Required',
+ 'Error',
+ 'Finished',
+ 'Aborted',
+ 'Retried',
+ 'IRebooting',
+ 'Force Continue',
+ 'Pending',
+ ]
+notStartedState = 'Not_Started'
+errorStates = ['Error', 'Aborted', 'Force Continue']
+finishedStates = ['Manual Step Required', 'Finished']
+
+logger = logging.getLogger(__name__)
+
+
+class TestFailedError(Exception):
+ pass
+
+
+class ChassisRestAPI:
+ @staticmethod
+ def postWithPayload(loginUrl, payload=None):
+ urlHeadersJson = {'content-type': 'application/json'}
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ if payload is None:
+ logger.debug('POST: ' + loginUrl)
+ (response, content) = h.request(loginUrl, 'POST', '',
+ urlHeadersJson)
+ logger.debug(content)
+ else:
+ logger.debug('POST: ' + loginUrl + ' <- Data: ' + str(payload))
+ (response, content) = h.request(loginUrl, 'POST',
+ body=payload,
+ headers=urlHeadersJson)
+ logger.debug(response)
+ logger.debug(content)
+ except Exception, e:
+ raise Exception('Got an error code: ', e)
+ return content
+
+ @staticmethod
+ def postWithPayloadAndHeaders(loginUrl, urlHeadersJson,
+ payload=None):
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ if payload is None:
+ logger.debug('POST: ' + loginUrl)
+ (response, content) = h.request(loginUrl, 'POST', '',
+ urlHeadersJson)
+ else:
+ logger.debug('POST: ' + loginUrl + ' <- Data: ' + str(payload))
+ (response, content) = h.request(loginUrl, 'POST',
+ body=payload,
+ headers=urlHeadersJson)
+ except Exception, e:
+ raise Exception('Got an error code: ', e)
+ return content
+
+ @staticmethod
+ def postOperation(url, apiKey, payload=''):
+ urlHeadersJson = {'content-type': 'application/json',
+ 'X-Api-Key': '%s' % str(apiKey)}
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ if payload is None:
+ logger.debug('POST: ' + url)
+ (response, content) = h.request(url, 'POST',
+ json.dumps(payload),
+ urlHeadersJson)
+ else:
+ logger.debug('POST: ' + url + ' <- Data: ' + str(payload))
+ (response, content) = h.request(url, 'POST',
+ json.dumps(payload),
+ headers=urlHeadersJson)
+ except Exception, e:
+ raise Exception('Got an error code: ', e)
+ return content
+
+ @staticmethod
+ def patch(url, payload, apiKey):
+ urlHeadersJson = {'content-type': 'application/json',
+ 'X-Api-Key': '%s' % str(apiKey)}
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ logger.debug('PATCH: ' + url + ' <-- Attribute: ' +
+ str(payload))
+ (response, content) = h.request(url, 'PATCH',
+ json.dumps(payload),
+ urlHeadersJson)
+ except Exception, e:
+
+ # print (response, content)
+
+ raise Exception('Got an error code: ', e)
+ return content
+
+ @staticmethod
+ def delete(url, apiKey):
+ urlHeadersJson = {'content-type': 'application/json',
+ 'X-Api-Key': '%s' % str(apiKey)}
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ (response, content) = h.request(url, 'DELETE', '', urlHeadersJson)
+ logger.debug('DELETE: ' + url)
+ except Exception, e:
+ raise Exception('Got an error code: ', e)
+ if response.status not in okStates:
+ raise TestFailedError(json.loads(content)['error'])
+ return json.loads(content)
+
+ @staticmethod
+ def getWithHeaders(url, apiKey):
+ urlHeadersJson = {'content-type': 'application/json',
+ 'X-Api-Key': '%s' % str(apiKey)}
+ try:
+ h = httplib2.Http('.cache',
+ disable_ssl_certificate_validation=True)
+ logger.debug('GET: ' + url)
+ (response, content) = h.request(url, 'GET', '', urlHeadersJson)
+ except Exception, e:
+ raise Exception('Got an error code: ', e)
+ if response.status not in okStates:
+ raise TestFailedError(json.loads(content)['error'])
+ output = json.loads(content)
+ return output
diff --git a/functest/opnfv_tests/vnf/ims/ixia/utils/IxLoadUtils.py b/functest/opnfv_tests/vnf/ims/ixia/utils/IxLoadUtils.py
new file mode 100644
index 00000000..d8003e46
--- /dev/null
+++ b/functest/opnfv_tests/vnf/ims/ixia/utils/IxLoadUtils.py
@@ -0,0 +1,397 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017 IXIA 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 requests
+import sys
+import time
+import logging
+from IxRestUtils import formatDictToJSONPayload
+
+kActionStateFinished = 'finished'
+kActionStatusSuccessful = 'Successful'
+kActionStatusError = 'Error'
+kTestStateUnconfigured = 'Unconfigured'
+
+logger = logging.getLogger(__name__)
+
+
+def stripApiAndVersionFromURL(url):
+
+ # remove the slash (if any) at the beginning of the url
+
+ if url[0] == '/':
+ url = url[1:]
+
+ urlElements = url.split('/')
+ if 'api' in url:
+
+ # strip the api/v0 part of the url
+
+ urlElements = urlElements[2:]
+
+ return '/'.join(urlElements)
+
+
+def waitForActionToFinish(connection, replyObj, actionUrl):
+ """
+ This method waits for an action to finish executing. after a POST request
+ is sent in order to start an action, The HTTP reply will contain,
+ in the header, a 'location' field, that contains an URL.
+ The action URL contains the status of the action. we perform a GET on that
+ URL every 0.5 seconds until the action finishes with a success.
+ If the action fails, we will throw an error and
+ print the action's error message.
+ """
+
+ actionResultURL = replyObj.headers.get('location')
+ if actionResultURL:
+ actionResultURL = stripApiAndVersionFromURL(actionResultURL)
+ actionFinished = False
+
+ while not actionFinished:
+ actionStatusObj = connection.httpGet(actionResultURL)
+
+ if actionStatusObj.state == kActionStateFinished:
+ if actionStatusObj.status == kActionStatusSuccessful:
+ actionFinished = True
+ else:
+ errorMsg = "Error while executing action '%s'." \
+ % actionUrl
+
+ if actionStatusObj.status == kActionStatusError:
+ errorMsg += actionStatusObj.error
+
+ print errorMsg
+
+ sys.exit(1)
+ else:
+ time.sleep(0.1)
+
+
+def performGenericOperation(connection, url, payloadDict):
+ """
+ This will perform a generic operation on the given url,
+ it will wait for it to finish.
+ """
+
+ data = formatDictToJSONPayload(payloadDict)
+ reply = connection.httpPost(url=url, data=data)
+
+ waitForActionToFinish(connection, reply, url)
+
+ return reply
+
+
+def performGenericPost(connection, listUrl, payloadDict):
+ """
+ This will perform a generic POST method on a given url
+ """
+
+ data = formatDictToJSONPayload(payloadDict)
+
+ reply = connection.httpPost(url=listUrl, data=data)
+ try:
+ newObjPath = reply.headers['location']
+ except:
+ raise Exception('Location header is not present. \
+ Please check if the action was created successfully.')
+
+ newObjID = newObjPath.split('/')[-1]
+ return newObjID
+
+
+def performGenericDelete(connection, listUrl, payloadDict):
+ """
+ This will perform a generic DELETE method on a given url
+ """
+
+ data = formatDictToJSONPayload(payloadDict)
+
+ reply = connection.httpDelete(url=listUrl, data=data)
+ return reply
+
+
+def performGenericPatch(connection, url, payloadDict):
+ """
+ This will perform a generic PATCH method on a given url
+ """
+
+ data = formatDictToJSONPayload(payloadDict)
+
+ reply = connection.httpPatch(url=url, data=data)
+ return reply
+
+
+def createSession(connection, ixLoadVersion):
+ """
+ This method is used to create a new session.
+ It will return the url of the newly created session
+ """
+
+ sessionsUrl = 'sessions'
+ data = {'ixLoadVersion': ixLoadVersion}
+
+ sessionId = performGenericPost(connection, sessionsUrl, data)
+
+ newSessionUrl = '%s/%s' % (sessionsUrl, sessionId)
+ startSessionUrl = '%s/operations/start' % newSessionUrl
+
+ # start the session
+
+ performGenericOperation(connection, startSessionUrl, {})
+
+ logger.debug('Created session no %s' % sessionId)
+
+ return newSessionUrl
+
+
+def deleteSession(connection, sessionUrl):
+ """
+ This method is used to delete an existing session.
+ """
+
+ deleteParams = {}
+ performGenericDelete(connection, sessionUrl, deleteParams)
+
+
+def uploadFile(connection, url, fileName, uploadPath, overwrite=True):
+ headers = {'Content-Type': 'multipart/form-data'}
+ params = {'overwrite': overwrite, 'uploadPath': uploadPath}
+
+ logger.debug('Uploading...')
+ try:
+ with open(fileName, 'rb') as f:
+ resp = requests.post(url, data=f, params=params,
+ headers=headers)
+ except requests.exceptions.ConnectionError, e:
+ raise Exception('Upload file failed. Received connection error. \
+ One common cause for this error is the size of the \
+ file to be uploaded.The web server sets a limit of 1GB\
+ for the uploaded file size. \
+ Received the following error: %s' % str(e))
+ except IOError, e:
+ raise Exception('Upload file failed. Received IO error: %s'
+ % str(e))
+ except Exception:
+ raise Exception('Upload file failed. Received the following error: %s'
+ % str(e))
+ else:
+ logger.debug('Upload file finished.')
+ logger.debug('Response status code %s' % resp.status_code)
+ logger.debug('Response text %s' % resp.text)
+
+
+def loadRepository(connection, sessionUrl, rxfFilePath):
+ """
+ This method will perform a POST request to load a repository.
+ """
+
+ loadTestUrl = '%s/ixload/test/operations/loadTest' % sessionUrl
+ data = {'fullPath': rxfFilePath}
+
+ performGenericOperation(connection, loadTestUrl, data)
+
+
+def saveRxf(connection, sessionUrl, rxfFilePath):
+ """
+ This method saves the current rxf to the disk of the machine on
+ which the IxLoad instance is running.
+ """
+
+ saveRxfUrl = '%s/ixload/test/operations/saveAs' % sessionUrl
+ rxfFilePath = rxfFilePath.replace('\\', '\\\\')
+ data = {'fullPath': rxfFilePath, 'overWrite': 1}
+
+ performGenericOperation(connection, saveRxfUrl, data)
+
+
+def runTest(connection, sessionUrl):
+ """
+ This method is used to start the currently loaded test.
+ After starting the 'Start Test' action, wait for the action to complete.
+ """
+
+ startRunUrl = '%s/ixload/test/operations/runTest' % sessionUrl
+ data = {}
+
+ performGenericOperation(connection, startRunUrl, data)
+
+
+def getTestCurrentState(connection, sessionUrl):
+ """
+ This method gets the test current state.
+ (for example - running, unconfigured, ..)
+ """
+
+ activeTestUrl = '%s/ixload/test/activeTest' % sessionUrl
+ testObj = connection.httpGet(activeTestUrl)
+
+ return testObj.currentState
+
+
+def getTestRunError(connection, sessionUrl):
+ """
+ This method gets the error that appeared during the last test run.
+ If no error appeared (the test ran successfully),
+ the return value will be 'None'.
+ """
+
+ activeTestUrl = '%s/ixload/test/activeTest' % sessionUrl
+ testObj = connection.httpGet(activeTestUrl)
+
+ return testObj.testRunError
+
+
+def waitForTestToReachUnconfiguredState(connection, sessionUrl):
+ """
+ This method waits for the current test to reach the 'Unconfigured' state.
+ """
+
+ while getTestCurrentState(connection, sessionUrl) \
+ != kTestStateUnconfigured:
+ time.sleep(0.1)
+
+
+def pollStats(connection, sessionUrl, watchedStatsDict, pollingInterval=4):
+ """
+ This method is used to poll the stats.
+ Polling stats is per request but this method does a continuous poll.
+ """
+
+ statSourceList = watchedStatsDict.keys()
+ statsDict = {}
+
+ collectedTimestamps = {}
+ testIsRunning = True
+
+ # check stat sources
+
+ for statSource in statSourceList[:]:
+ statSourceUrl = '%s/ixload/stats/%s/values' % (sessionUrl, statSource)
+ statSourceReply = connection.httpRequest('GET', statSourceUrl)
+ if statSourceReply.status_code != 200:
+ logger.debug("Warning - Stat source '%s' does not exist. \
+ Will ignore it." % statSource)
+ statSourceList.remove(statSource)
+
+ # check the test state, and poll stats while the test is still running
+
+ while testIsRunning:
+
+ # the polling interval is configurable.
+ # by default, it's set to 4 seconds
+
+ time.sleep(pollingInterval)
+
+ for statSource in statSourceList:
+ valuesUrl = '%s/ixload/stats/%s/values' % (sessionUrl, statSource)
+
+ valuesObj = connection.httpGet(valuesUrl)
+ valuesDict = valuesObj.getOptions()
+
+ # get just the new timestamps - that were not previously
+ # retrieved in another stats polling iteration
+
+ newTimestamps = [int(timestamp) for timestamp in
+ valuesDict.keys() if timestamp
+ not in collectedTimestamps.get(statSource,
+ [])]
+ newTimestamps.sort()
+
+ for timestamp in newTimestamps:
+ timeStampStr = str(timestamp)
+
+ collectedTimestamps.setdefault(statSource, []).append(
+ timeStampStr)
+
+ timestampDict = statsDict.setdefault(statSource,
+ {}).setdefault(
+ timestamp, {})
+
+ # save the values for the current timestamp,
+ # and later print them
+
+ logger.info(' -- ')
+ for (caption, value) in \
+ valuesDict[timeStampStr].getOptions().items():
+ if caption in watchedStatsDict[statSource]:
+ logger.info(' %s -> %s' % (caption, value))
+ timestampDict[caption] = value
+
+ testIsRunning = getTestCurrentState(connection, sessionUrl) \
+ == 'Running'
+
+ logger.debug('Stopped receiving stats.')
+ return timestampDict
+
+
+def clearChassisList(connection, sessionUrl):
+ """
+ This method is used to clear the chassis list.
+ After execution no chassis should be available in the chassisListself.
+ """
+
+ chassisListUrl = '%s/ixload/chassischain/chassisList' % sessionUrl
+ deleteParams = {}
+ performGenericDelete(connection, chassisListUrl, deleteParams)
+
+
+def configureLicenseServer(connection, sessionUrl, licenseServerIp):
+ """
+ This method is used to clear the chassis list.
+ After execution no chassis should be available in the chassisList.
+ """
+
+ chassisListUrl = '%s/ixload/preferences' % sessionUrl
+ patchParams = {'licenseServer': licenseServerIp}
+ performGenericPatch(connection, chassisListUrl, patchParams)
+
+
+def addChassisList(connection, sessionUrl, chassisList):
+ """
+ This method is used to add one or more chassis to the chassis list.
+ """
+
+ chassisListUrl = '%s/ixload/chassisChain/chassisList' % sessionUrl
+
+ for chassisName in chassisList:
+ data = {'name': chassisName}
+ chassisId = performGenericPost(connection, chassisListUrl, data)
+
+ # refresh the chassis
+
+ refreshConnectionUrl = '%s/%s/operations/refreshConnection' \
+ % (chassisListUrl, chassisId)
+ performGenericOperation(connection, refreshConnectionUrl, {})
+
+
+def assignPorts(connection, sessionUrl, portListPerCommunity):
+ """
+ This method is used to assign ports from a connected chassis
+ to the required NetTraffics.
+ """
+
+ communtiyListUrl = '%s/ixload/test/activeTest/communityList' \
+ % sessionUrl
+
+ communityList = connection.httpGet(url=communtiyListUrl)
+
+ for community in communityList:
+ portListForCommunity = portListPerCommunity.get(community.name)
+
+ portListUrl = '%s/%s/network/portList' % (communtiyListUrl,
+ community.objectID)
+
+ if portListForCommunity:
+ for portTuple in portListForCommunity:
+ (chassisId, cardId, portId) = portTuple
+ paramDict = {'chassisId': chassisId, 'cardId': cardId,
+ 'portId': portId}
+
+ performGenericPost(connection, portListUrl, paramDict)
diff --git a/functest/opnfv_tests/vnf/ims/ixia/utils/IxRestUtils.py b/functest/opnfv_tests/vnf/ims/ixia/utils/IxRestUtils.py
new file mode 100644
index 00000000..63db403b
--- /dev/null
+++ b/functest/opnfv_tests/vnf/ims/ixia/utils/IxRestUtils.py
@@ -0,0 +1,192 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2017 IXIA 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
+
+from urlparse import urljoin
+import requests
+
+
+def getConnection(server, port):
+ """
+ Gets a Connection instance, that will be used to
+ make the HTTP requests to the application
+ """
+ connectionUrl = 'http://%s:%s/' % (server, port)
+
+ conn = Connection(connectionUrl, 'v0')
+ return conn
+
+
+def formatDictToJSONPayload(dictionary):
+ """
+ Converts a given python dict instance to a string
+ JSON payload that can be sent to a REST API.
+ """
+ jsonPayload = '{'
+ optionsList = []
+ for (key, val) in dictionary.items():
+ valStr = str(val)
+ if type(val) is str:
+ valStr = '"%s"' % val
+ if type(val) is bool:
+ valStr = valStr.lower()
+ optionsList.append('"%s":%s' % (key, valStr))
+
+ jsonPayload += ','.join(optionsList)
+ jsonPayload += '}'
+
+ return jsonPayload
+
+
+class Connection(object):
+
+ """
+ Class that executes the HTTP requests to the application instance.
+ It handles creating the HTTP session and executing HTTP methods.
+ """
+
+ kHeaderContentType = 'content-type'
+ kContentJson = 'application/json'
+
+ def __init__(self, siteUrl, apiVersion):
+ self.httpSession = None
+
+ self.url = Connection.urljoin(siteUrl, 'api')
+ self.url = Connection.urljoin(self.url, apiVersion)
+
+ def _getHttpSession(self):
+ """
+ This is a lazy initializer for the HTTP session.
+ It does not need to be active until it is required.
+ """
+
+ if self.httpSession is None:
+ self.httpSession = requests.Session()
+ return self.httpSession
+
+ @classmethod
+ def urljoin(cls, base, end):
+ """ Join two URLs. If the second URL is absolute, the base is ignored.
+
+ Use this instead of urlparse.urljoin directly so that we can customize
+ its behavior if necessary.
+ Currently differs in that it
+ 1. appends a / to base if not present.
+ 2. casts end to a str as a convenience
+ """
+
+ if base and not base.endswith('/'):
+ base = base + '/'
+ return urljoin(base, str(end))
+
+ def httpRequest(self, method, url='', data='', params={}, headers={}):
+ """
+ Method for making a HTTP request.
+ The method type (GET, POST, PATCH, DELETE) will be sent as a parameter.
+ Along with the url and request data. The HTTP response is returned
+ """
+
+ headers[Connection.kHeaderContentType] = Connection.kContentJson
+
+ absUrl = Connection.urljoin(self.url, url)
+ result = self._getHttpSession().request(method,
+ absUrl,
+ data=str(data),
+ params=params,
+ headers=headers)
+ return result
+
+ def httpGet(self, url='', data='', params={}, headers={}):
+ """
+ Method for calling HTTP GET.
+ This will return a WebObject that has the fields returned
+ in JSON format by the GET operation.
+ """
+
+ reply = self.httpRequest('GET', url, data, params, headers)
+ return _WebObject(reply.json())
+
+ def httpPost(self, url='', data='', params={}, headers={}):
+ """
+ Method for calling HTTP POST. Will return the HTTP reply.
+ """
+
+ return self.httpRequest('POST', url, data, params, headers)
+
+ def httpPatch(self, url='', data='', params={}, headers={}):
+ """
+ Method for calling HTTP PATCH. Will return the HTTP reply.
+ """
+
+ return self.httpRequest('PATCH', url, data, params, headers)
+
+ def httpDelete(self, url='', data='', params={}, headers={}):
+ """
+ Method for calling HTTP DELETE. Will return the HTTP reply.
+ """
+
+ return self.httpRequest('DELETE', url, data, params, headers)
+
+
+def _WebObject(value):
+ """
+ Method used for creating a wrapper object corresponding to the JSON string
+ received on a GET request.
+ """
+
+ if isinstance(value, dict):
+ result = WebObject(**value)
+ elif isinstance(value, list):
+ result = WebList(entries=value)
+ else:
+ result = value
+ return result
+
+
+class WebList(list):
+
+ """
+ Using this class a JSON list will be transformed
+ in a list of WebObject instances.
+ """
+
+ def __init__(self, entries=[]):
+ """
+ Create a WebList from a list of items that
+ are processed by the _WebObject function
+ """
+
+ for item in entries:
+ self.append(_WebObject(item))
+
+
+class WebObject(object):
+
+ """
+ A WebObject instance will have its fields set to correspond to
+ the JSON format received on a GET request.
+ """
+
+ def __init__(self, **entries):
+ """
+ Create a WebObject instance by providing a dict having a
+ property - value structure.
+ """
+
+ self.jsonOptions = {}
+ for (key, value) in entries.iteritems():
+ webObj = _WebObject(value)
+ self.jsonOptions[key] = webObj
+ self.__dict__[key] = webObj
+
+ def getOptions(self):
+ '''
+ Get the JSON dictionary which represents the WebObject Instance
+ '''
+
+ return self.jsonOptions
diff --git a/functest/opnfv_tests/vnf/ims/ixia/utils/__init__.py b/functest/opnfv_tests/vnf/ims/ixia/utils/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/functest/opnfv_tests/vnf/ims/ixia/utils/__init__.py