Fix Errors in v1 Compliance Test and Pin setuptools version

This patch fixes the multiple Tacker side issues in
tacker v1 compliance test.

Additionally, The ft-v1-compliance-sol tox environment
fails while importing the Robot REST library.

This happens because the flex library (used by REST) imports
pkg_resources, which was removed in newer setuptools releases.
Since tox installs the latest setuptools by default, the import
fails and stops the test execution.

Pin setuptools to a compatible version to restore the compliance
test environment.

This is a temporary workaround until the dependency chain is
updated to remove the pkg_resources dependency.

Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2142405
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2126473
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2052585
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2037046
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2116848
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2136980
Closes-Bug: https://bugs.launchpad.net/tacker/+bug/2137529
Depends-On: Ia7a3da88075d3e8af783c38d80c9f11fe5547ca9
Change-Id: Ic1b129f08781eaa929803b45014f65c5a884eb7d
Signed-off-by: Shivam Shukla <shivam.shukla3@india.nec.com>
This commit is contained in:
Shivam Shukla
2026-06-10 03:48:12 +00:00
parent fea54ab7db
commit a01b7f02b3
11 changed files with 189 additions and 83 deletions
+35
View File
@@ -183,6 +183,41 @@ class ViewBuilder(base.BaseViewBuilder):
links = self._get_lcm_op_occs_links(vnf_lcm_op_occs)
vnf_lcm_op_occs_dict.update(links)
# Remove the optional parameters which are having None
if 'grantId' in vnf_lcm_op_occs_dict and \
vnf_lcm_op_occs_dict['grantId'] is None:
vnf_lcm_op_occs_dict.pop('grantId')
if 'resourceChanges' in vnf_lcm_op_occs_dict:
if 'affectedVnfcs' in vnf_lcm_op_occs_dict['resourceChanges']:
for item in vnf_lcm_op_occs_dict['resourceChanges'][
'affectedVnfcs']:
if 'removedStorageResourceIds' in item and \
item['removedStorageResourceIds'] is None:
item.pop('removedStorageResourceIds')
if 'resourceChanges' in vnf_lcm_op_occs_dict:
if 'affectedVirtualLinks' in vnf_lcm_op_occs_dict[
'resourceChanges']:
for item in vnf_lcm_op_occs_dict['resourceChanges'][
'affectedVirtualLinks']:
if 'networkResource' in item and 'vimConnectionId' in \
item['networkResource']:
if item['networkResource']['vimConnectionId'] is None:
item['networkResource'].pop('vimConnectionId')
if 'networkResource' in item and \
'vimLevelResourceType' in item['networkResource']:
if item['networkResource'][
'vimLevelResourceType'] is None:
item['networkResource'].pop('vimLevelResourceType')
# Convert operationParams to dict type as per expected specification
if 'operationParams' in vnf_lcm_op_occs_dict:
if isinstance(vnf_lcm_op_occs_dict['operationParams'], str):
vnf_lcm_op_occs_dict['operationParams'] = json.loads(
vnf_lcm_op_occs_dict['operationParams'])
return vnf_lcm_op_occs_dict
def create(self, vnf_instance):
+8
View File
@@ -45,6 +45,14 @@ class ViewBuilder(base.BaseViewBuilder):
links = self._get_links(vnf_package)
vnf_package_response.update(links)
# Convert containerFormat and diskFormat values in uppercase
# as per ETSI specification
if 'softwareImages' in vnf_package_response:
for item in vnf_package_response['softwareImages']:
if 'containerFormat' in item:
item['containerFormat'] = item['containerFormat'].upper()
if 'diskFormat' in item:
item['diskFormat'] = item['diskFormat'].upper()
return vnf_package_response
def _get_modified_user_data(self, old_user_data, new_user_data):
+4 -2
View File
@@ -253,7 +253,6 @@ class VnfLcmController(wsgi.Controller):
if dep_flavour.instantiation_levels:
inst_levels.update({
dep_flavour.flavour_id: dep_flavour.instantiation_levels})
if req_body['flavour_id'] not in flavour_list:
raise exceptions.FlavourNotFound(flavour_id=req_body['flavour_id'])
@@ -272,7 +271,6 @@ class VnfLcmController(wsgi.Controller):
if req_inst_level_id in inst_level.get('levels').keys():
# Found instantiation level
return
raise exceptions.InstantiationLevelNotFound(
inst_level_id=req_body['instantiation_level_id'])
@@ -1940,6 +1938,10 @@ class VnfLcmController(wsgi.Controller):
all_fields=all_fields, exclude_fields=exclude_fields,
fields=fields, exclude_default=exclude_default)
# Remove grantId if it is None
for ind_op in result:
if 'grantId' in ind_op and ind_op['grantId'] is None:
ind_op.pop('grantId')
res = webob.Response(content_type='application/json')
res.status_int = 200
+29 -2
View File
@@ -523,8 +523,11 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject,
if (self.instantiation_state == fields.VnfInstanceState.INSTANTIATED
and self.instantiated_vnf_info):
data.update({'instantiated_vnf_info':
self.instantiated_vnf_info.to_dict()})
instantiated_vnf_data = self.instantiated_vnf_info.to_dict()
# Replace all null values with empty strings
inst_vnf_data = self._filter_null_values(instantiated_vnf_data)
data.update({'instantiated_vnf_info': inst_vnf_data})
vim_connection_info_list = []
for vim_connection_info in self.vim_connection_info:
@@ -533,6 +536,30 @@ class VnfInstance(base.TackerObject, base.TackerPersistentObject,
return data
def _filter_null_values(self, obj):
"""Replace all null values with empty strings"""
if isinstance(obj, dict):
result = {}
for key, value in obj.items():
if value is None:
# Replace null with empty string
result[key] = ''
elif isinstance(value, (dict, list)):
# Recursively process nested dicts or lists
result[key] = self._filter_null_values(value)
else:
# Keep the value as-is
result[key] = value
return result
elif isinstance(obj, list):
return [
self._filter_null_values(item)
for item in obj
if item is not None
]
return obj
@base.remotable
def update(
self,
+10 -6
View File
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import copy
import json
import os
import time
@@ -24,7 +25,8 @@ import robot
from urllib.parse import urlparse
from tacker.tests.functional import base
from tacker.tests.functional.sol.vnflcm import test_vnf_instance as vnflcmtest
from tacker.tests.functional.sol.vnflcm import base as vnflcm_base
from tacker.tests import utils
VNFPKG_PATH = '/vnfpkgm/v1/vnf_packages/%s'
VNFINSS_PATH = '/vnflcm/v1/vnf_instances'
@@ -202,8 +204,10 @@ class BaseComplTest(base.BaseTackerTest):
def _create_and_upload_vnf_packages(cls, pkgnames):
vnfpkginfos = []
for pkgname in pkgnames:
vnfpkgid, vnfdid = vnflcmtest._create_and_upload_vnf_package(
cls.http_client, pkgname, {})
file_path = utils.test_etc_sample("etsi/nfv", pkgname)
path, uniqueid = utils.create_csar_with_unique_vnfd_id(file_path)
vnfpkgid, vnfdid = vnflcm_base._create_and_upload_vnf_package(
cls.http_client, {}, path)
vnfpkginfos.append(VnfPkgInfo(vnfpkgid, vnfdid))
return vnfpkginfos
@@ -251,7 +255,7 @@ class BaseComplTest(base.BaseTackerTest):
@classmethod
def _instantiate_vnf_instance(cls, vnfid):
body = INSTANTIATION_BODY
body = copy.deepcopy(INSTANTIATION_BODY)
body['extVirtualLinks'][0]['resourceId'] = cls.net0_id
body['vimConnectionInfo'][0]['id'] = uuidutils.generate_uuid()
body['vimConnectionInfo'][0]['vimId'] = cls.vimid
@@ -282,7 +286,7 @@ class BaseComplTest(base.BaseTackerTest):
@classmethod
def _instantiate_vnf_instance_for_scale(cls, vnfid):
body = INSTANTIATION_BODY
body = copy.deepcopy(INSTANTIATION_BODY)
body['flavourId'] = 'default'
body['extVirtualLinks'][0]['resourceId'] = cls.net0_id
body['vimConnectionInfo'][0]['id'] = uuidutils.generate_uuid()
@@ -299,7 +303,7 @@ class BaseComplTest(base.BaseTackerTest):
@classmethod
def _instantiate_error_vnf_instance(cls, vnfid):
body = INSTANTIATION_BODY
body = copy.deepcopy(INSTANTIATION_BODY)
body['flavorId'] = 'sample'
body['extVirtualLinks'][0]['resourceId'] = cls.net0_id
body['vimConnectionInfo'][0]['id'] = uuidutils.generate_uuid()
+44 -35
View File
@@ -13,12 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tacker.tests.compliance import base as rootbase
from tacker.tests.compliance.sol002 import base
LOG = logging.getLogger(__name__)
class BaseVNFLifecycleManagementTest(base.BaseComplSolTest):
@classmethod
def setUpClass(cls):
@@ -115,7 +119,6 @@ class IndividualVNFInstanceTest(BaseVNFLifecycleManagementTest):
variables = ['vnfInstanceId:' + vnf['id']]
rc, output = self._run('DELETE Individual VNFInstance', variables)
# Post-Conditions: VNF instance deleted
self.assertEqual(0, rc)
@@ -133,7 +136,6 @@ class IndividualVNFInstanceTest(BaseVNFLifecycleManagementTest):
rc, output = self._run('PATCH Individual VNFInstance',
variables=variables,
body=jsonutils.dumps(body))
# Post-Conditions:
self._delete_vnf_instance(vnfid)
@@ -150,7 +152,6 @@ class InstantiateVNFTaskTest(BaseVNFLifecycleManagementTest):
def test_post_instantiate_vnfinstance(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
variables = ['vnfInstanceId:' + vnf['id']]
body = rootbase.INSTANTIATION_BODY
@@ -163,7 +164,6 @@ class InstantiateVNFTaskTest(BaseVNFLifecycleManagementTest):
variables=variables,
body=jsonutils.dumps(body),
filename='instantiateVnfRequest.json')
# Post-Conditions:
self._wait_vnf_status(vnf['id'], 'instantiationState', 'INSTANTIATED')
self._terminate_vnf_instance(vnf['id'])
@@ -182,7 +182,6 @@ class TerminateVNFTaskTest(BaseVNFLifecycleManagementTest):
def test_post_terminate_vnfinstance(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
@@ -204,19 +203,54 @@ class ScaleVNFTaskTest(BaseVNFLifecycleManagementScaleTest):
@classmethod
def setUpClass(cls):
cls.resource = 'ScaleVNFTask'
# Cleaup the existing DemoVirtualStorage images
# which are created during scale operation
cls._cleanup_test_images()
super(ScaleVNFTaskTest, cls).setUpClass()
def test_post_expand_by_scaleout(self):
@classmethod
def _cleanup_test_images(cls):
try:
glance = cls.glanceclient()
images = list(glance.images.list(
filters={'name': 'DemoVirtualStorage'}))
if images:
for image in images:
try:
glance.images.delete(image.id)
except Exception as e:
LOG.exception(
"Failed to delete image %s: %s",
image.id, e
)
except Exception as e:
LOG.exception("Error during cleanup: %s", e)
# Both the scale operation (scale-out and scale-in) test functions are
# merged as single test function where it first performs scale-out and
# then perform scale-in operation
def test_post_scale(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance_for_scale(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
body = rootbase.SCALE_BODY
body['type'] = 'SCALE_OUT'
rc, output = self._run('POST Scale a vnfInstance',
variables=variables,
body=jsonutils.dumps(body),
filename='scaleVnfRequest.json')
lcmid = self._get_lcm_op_occs_id(vnf['id'], lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
body['type'] = 'SCALE_IN'
rc, output = self._run('POST Scale a vnfInstance',
variables=variables,
body=jsonutils.dumps(body),
@@ -228,28 +262,8 @@ class ScaleVNFTaskTest(BaseVNFLifecycleManagementScaleTest):
lcmid = self._get_lcm_op_occs_id(vnf['id'], lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
self.assertEqual(0, rc)
def test_post_reduce_by_scalein(self):
vnf_id = self._get_vnf_instance_id()
body = rootbase.SCALE_BODY
body['type'] = 'SCALE_IN'
rc, output = self._run('POST Scale a vnfInstance',
variables=['vnfInstanceId:' + vnf_id],
body=jsonutils.dumps(body),
filename='scaleVnfRequest.json')
res = self._get_responses_from_output(output)
# Post-Conditions:
lcmid = self._get_lcm_op_occs_id(vnf_id, lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
self._terminate_vnf_instance(vnf_id)
self._delete_vnf_instance(vnf_id)
self._terminate_vnf_instance(vnf['id'])
self._delete_vnf_instance(vnf['id'])
self.assertEqual(0, rc)
@@ -326,7 +340,6 @@ class HealVNFTaskTest(BaseVNFLifecycleManagementTest):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
resbody = self._get_vnf_ind_instance(vnf['id'])
body = rootbase.HEAL_BODY
@@ -360,7 +373,6 @@ class RetryOperationTaskTest(BaseVNFLifecycleManagementTest):
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_error_vnf_instance(vnf['id'])
lcmid = self._get_lcm_op_occs_id(vnf['id'])
variables = ['vnfLcmOpOccId:' + lcmid]
rc, output = self._run('Post Retry operation task',
@@ -386,9 +398,7 @@ class ChangeExternalVNFConnectivityTaskTest(BaseVNFLifecycleManagementTest):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
resbody = self._get_vnf_ind_instance(vnf['id'])
@@ -402,7 +412,6 @@ class ChangeExternalVNFConnectivityTaskTest(BaseVNFLifecycleManagementTest):
variables=variables,
body=jsonutils.dumps(body),
filename='changeExtVnfConnectivityRequest.json')
# Post-Conditions:
lcmid = self._get_lcm_op_occs_id(vnf['id'], lcm='CHANGE_EXT_CONN')
res = self._wait_lcm_status(lcmid)
+45 -34
View File
@@ -13,12 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_serialization import jsonutils
from oslo_utils import uuidutils
from tacker.tests.compliance import base as rootbase
from tacker.tests.compliance.sol003 import base
LOG = logging.getLogger(__name__)
class BaseVNFLifecycleManagementTest(base.BaseComplSolTest):
@classmethod
def setUpClass(cls):
@@ -123,6 +127,7 @@ class IndividualVNFInstanceTest(BaseVNFLifecycleManagementTest):
def test_modify_information_about_individual_vnf_instance(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
vnfid = vnf['id']
variables = ['vnfInstanceId:' + vnfid]
@@ -182,7 +187,6 @@ class TerminateVNFTaskTest(BaseVNFLifecycleManagementTest):
def test_post_terminate_vnfinstance(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
@@ -260,14 +264,12 @@ class CreateAndGetSubscriptionsTest(BaseVNFLifecycleManagementTest):
rc, output = self._run('POST Create a new subscription',
body=jsonutils.dumps(body),
filename='lccnSubscriptionRequest.json')
self.assertEqual(0, rc)
def test_get_subscriptions(self):
# Pre-conditions: none
rc, output = self._run('GET Subscriptions')
self.assertEqual(0, rc)
@@ -286,7 +288,6 @@ class IndividualSubscriptionTest(BaseVNFLifecycleManagementTest):
rc, output = self._run('GET Individual Subscription',
variables=variables)
self.assertEqual(0, rc)
def test_remove_individual_subscription(self):
@@ -306,19 +307,54 @@ class ScaleVNFTaskTest(BaseVNFLifecycleManagementScaleTest):
@classmethod
def setUpClass(cls):
cls.resource = 'ScaleVNFTask'
# Cleaup the existing DemoVirtualStorage images
# which are created during scale operation
cls._cleanup_test_images()
super(ScaleVNFTaskTest, cls).setUpClass()
def test_post_expand_by_scaleout(self):
@classmethod
def _cleanup_test_images(cls):
try:
glance = cls.glanceclient()
images = list(glance.images.list(
filters={'name': 'DemoVirtualStorage'}))
if images:
for image in images:
try:
glance.images.delete(image.id)
except Exception as e:
LOG.exception(
"Failed to delete image %s: %s",
image.id, e
)
except Exception as e:
LOG.exception("Error during cleanup: %s", e)
# Both the scale operation (scale-out and scale-in) test functions are
# merged as single test function where it first performs scale-out and
# then perform scale-in operation
def test_post_scale(self):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance_for_scale(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
body = rootbase.SCALE_BODY
body['type'] = 'SCALE_OUT'
rc, output = self._run('POST Scale a vnfInstance',
variables=variables,
body=jsonutils.dumps(body),
filename='scaleVnfRequest.json')
lcmid = self._get_lcm_op_occs_id(vnf['id'], lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
body['type'] = 'SCALE_IN'
rc, output = self._run('POST Scale a vnfInstance',
variables=variables,
body=jsonutils.dumps(body),
@@ -330,28 +366,8 @@ class ScaleVNFTaskTest(BaseVNFLifecycleManagementScaleTest):
lcmid = self._get_lcm_op_occs_id(vnf['id'], lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
self.assertEqual(0, rc)
def test_post_reduce_by_scalein(self):
vnf_id = self._get_vnf_instance_id()
body = rootbase.SCALE_BODY
body['type'] = 'SCALE_IN'
rc, output = self._run('POST Scale a vnfInstance',
variables=['vnfInstanceId:' + vnf_id],
body=jsonutils.dumps(body),
filename='scaleVnfRequest.json')
res = self._get_responses_from_output(output)
# Post-Conditions:
lcmid = self._get_lcm_op_occs_id(vnf_id, lcm='SCALE')
res = self._wait_lcm_status(lcmid)
self.assertEqual(0, res)
self._terminate_vnf_instance(vnf_id)
self._delete_vnf_instance(vnf_id)
self._terminate_vnf_instance(vnf['id'])
self._delete_vnf_instance(vnf['id'])
self.assertEqual(0, rc)
@@ -367,7 +383,6 @@ class HealVNFTaskTest(BaseVNFLifecycleManagementTest):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
resbody = self._get_vnf_ind_instance(vnf['id'])
body = rootbase.HEAL_BODY
@@ -400,9 +415,7 @@ class ChangeExternalVNFConnectivityTaskTest(BaseVNFLifecycleManagementTest):
# Pre-conditions: none
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_vnf_instance(vnf['id'])
variables = ['vnfInstanceId:' + vnf['id']]
resbody = self._get_vnf_ind_instance(vnf['id'])
@@ -439,7 +452,6 @@ class RetryOperationTaskTest(BaseVNFLifecycleManagementTest):
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_error_vnf_instance(vnf['id'])
lcmid = self._get_lcm_op_occs_id(vnf['id'])
variables = ['vnfLcmOpOccId:' + lcmid]
rc, output = self._run('Post Retry operation task',
@@ -466,7 +478,6 @@ class RollbackOperationTaskTest(BaseVNFLifecycleManagementTest):
res, vnf = self._create_vnf_instance(self.vnfpkginfos[0].vnfdid)
self._instantiate_error_vnf_instance(vnf['id'])
lcmid = self._get_lcm_op_occs_id(vnf['id'])
variables = ['vnfLcmOpOccId:' + lcmid]
rc, output = self._run('Post Rollback operation task',
+2 -2
View File
@@ -2122,7 +2122,7 @@ VNFLCMOPOCC_RESPONSE = {
'grantId': 'f26f181d-7891-4720-b022-b074ec1733ef',
'operation': 'MODIFY_INFO',
'isAutomaticInvocation': False,
'operationParams': '{"is_reverse": False, "is_auto": False}',
'operationParams': '{"is_reverse": false, "is_auto": false}',
'error': {
'status': 500,
'detail': "name 'con' is not defined",
@@ -2341,7 +2341,7 @@ def fake_vnf_lcm_op_occs():
'grant_id': constants.UUID,
'operation': 'MODIFY_INFO',
'is_automatic_invocation': False,
'operation_params': '{"is_reverse": False, "is_auto": False}',
'operation_params': '{"is_reverse": false, "is_auto": false}',
'is_cancel_pending': False,
'error': error_obj,
'resource_changes': resource_changes_obj,
@@ -2111,6 +2111,9 @@ class TestController(base.TestCase):
expected_result = fakes.VNFLCMOPOCC_RESPONSE
res_dict = self.controller.show_lcm_op_occs(req, constants.UUID)
if isinstance(res_dict['operationParams'], dict):
res_dict['operationParams'] = json.dumps(
res_dict['operationParams'])
self.assertEqual(expected_result, res_dict)
@mock.patch.object(TackerManager, 'get_service_plugins',
+2 -2
View File
@@ -64,10 +64,10 @@ VNFPACKAGE_RESPONSE = {
'softwareImages': [{
'checksum': {'algorithm': 'fake-algorithm',
'hash': 'fake software image hash'},
'containerFormat': 'bare',
'containerFormat': 'BARE',
'createdAt': datetime.datetime(1900, 1, 1, 1, 1, 1,
tzinfo=datetime.timezone.utc),
'diskFormat': 'qcow2',
'diskFormat': 'QCOW2',
'id': 'fake_software_image_id',
'imagePath': 'fake image path',
'minDisk': 1,
+7
View File
@@ -238,6 +238,13 @@ passenv =
{[testenv]passenv}
*_PROXY
commands_pre =
# TODO(Shivam): This is a temporary workaround for compliance tests failing due
# to the removal of `pkg_resources` from newer setuptools versions.
# The Robot REST library dependency chain (flex -> pkg_resources)
# still relies on this module. Pinning setuptools restores compatibility.
# This should be removed once the TST Robot framework updates its dependencies
# to versions that no longer require pkg_resources.
pip install "setuptools<80"
git clone https://forge.etsi.org/rep/nfv/api-tests.git
git -C api-tests checkout 2.6.1-fix-plu
pip install -U -r{envdir}/api-tests/requirements.txt