Merge "Provide a full path top level to test discovery"
diff --git a/openstack-common.conf b/openstack-common.conf
index 1920295..16ba6a7 100644
--- a/openstack-common.conf
+++ b/openstack-common.conf
@@ -3,6 +3,8 @@
# The list of modules to copy from openstack-common
module=install_venv_common
module=versionutils
+module=with_venv
+module=install_venv
# The base module to hold the copy of openstack.common
base=tempest
diff --git a/run_tempest.sh b/run_tempest.sh
index 5a9b742..0f32045 100755
--- a/run_tempest.sh
+++ b/run_tempest.sh
@@ -20,7 +20,7 @@
}
testrargs=""
-venv=.venv
+venv=${VENV:-.venv}
with_venv=tools/with_venv.sh
serial=0
always_venv=0
diff --git a/run_tests.sh b/run_tests.sh
index 971f89b..9a158e4 100755
--- a/run_tests.sh
+++ b/run_tests.sh
@@ -19,7 +19,7 @@
testrargs=""
just_pep8=0
-venv=.venv
+venv=${VENV:-.venv}
with_venv=tools/with_venv.sh
serial=0
always_venv=0
diff --git a/tempest/api/compute/admin/test_servers.py b/tempest/api/compute/admin/test_servers.py
index 0062c1e..51a03f5 100644
--- a/tempest/api/compute/admin/test_servers.py
+++ b/tempest/api/compute/admin/test_servers.py
@@ -52,7 +52,7 @@
@test.idempotent_id('51717b38-bdc1-458b-b636-1cf82d99f62f')
def test_list_servers_by_admin(self):
# Listing servers by admin user returns empty list by default
- body = self.client.list_servers_with_detail()
+ body = self.client.list_servers(detail=True)
servers = body['servers']
self.assertEqual([], servers)
@@ -61,7 +61,7 @@
# Filter the list of servers by server error status
params = {'status': 'error'}
self.client.reset_state(self.s1_id, state='error')
- body = self.non_admin_client.list_servers(params)
+ body = self.non_admin_client.list_servers(**params)
# Reset server's state to 'active'
self.client.reset_state(self.s1_id, state='active')
# Verify server's state
@@ -77,7 +77,7 @@
# Listing servers by admin user with all tenants parameter
# Here should be listed all servers
params = {'all_tenants': ''}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
servers_name = map(lambda x: x['name'], servers)
@@ -91,14 +91,14 @@
# List the primary tenant but get nothing due to odd specified behavior
tenant_id = self.non_admin_client.tenant_id
params = {'tenant_id': tenant_id}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
self.assertEqual([], servers)
# List the admin tenant which has no servers
admin_tenant_id = self.client.tenant_id
params = {'all_tenants': '', 'tenant_id': admin_tenant_id}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
self.assertEqual([], servers)
@@ -118,10 +118,10 @@
self.assertEqual(server['status'], 'ACTIVE')
hostname = server[self._host_key]
params = {'host': hostname}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
nonexistent_params = {'host': 'nonexistent_host'}
- nonexistent_body = self.client.list_servers(nonexistent_params)
+ nonexistent_body = self.client.list_servers(**nonexistent_params)
nonexistent_servers = nonexistent_body['servers']
self.assertIn(test_server['id'], map(lambda x: x['id'], servers))
self.assertNotIn(test_server['id'],
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 04b909a..2b36180 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -20,6 +20,7 @@
from tempest.common import compute
from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
from tempest import exceptions
import tempest.test
@@ -280,8 +281,8 @@
cls.images.append(image_id)
if 'wait_until' in kwargs:
- cls.images_client.wait_for_image_status(image_id,
- kwargs['wait_until'])
+ waiters.wait_for_image_status(cls.images_client,
+ image_id, kwargs['wait_until'])
image = cls.images_client.show_image(image_id)
if kwargs['wait_until'] == 'ACTIVE':
diff --git a/tempest/api/compute/images/test_image_metadata.py b/tempest/api/compute/images/test_image_metadata.py
index b307b59..ab82d91 100644
--- a/tempest/api/compute/images/test_image_metadata.py
+++ b/tempest/api/compute/images/test_image_metadata.py
@@ -17,6 +17,7 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
from tempest import test
@@ -52,7 +53,7 @@
cls.images.append(cls.image_id)
image_file = six.StringIO(('*' * 1024))
cls.glance_client.update_image(cls.image_id, data=image_file)
- cls.client.wait_for_image_status(cls.image_id, 'ACTIVE')
+ waiters.wait_for_image_status(cls.client, cls.image_id, 'ACTIVE')
def setUp(self):
super(ImagesMetadataTestJSON, self).setUp()
diff --git a/tempest/api/compute/images/test_images_oneserver.py b/tempest/api/compute/images/test_images_oneserver.py
index 38fed25..1741e86 100644
--- a/tempest/api/compute/images/test_images_oneserver.py
+++ b/tempest/api/compute/images/test_images_oneserver.py
@@ -17,6 +17,7 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
from tempest import test
@@ -81,7 +82,7 @@
meta = {'image_type': 'test'}
body = self.client.create_image(self.server_id, name, meta)
image_id = data_utils.parse_image_id(body.response['location'])
- self.client.wait_for_image_status(image_id, 'ACTIVE')
+ waiters.wait_for_image_status(self.client, image_id, 'ACTIVE')
# Verify the image was created correctly
image = self.client.show_image(image_id)
diff --git a/tempest/api/compute/images/test_list_image_filters.py b/tempest/api/compute/images/test_list_image_filters.py
index cc59084..b0bbc91 100644
--- a/tempest/api/compute/images/test_list_image_filters.py
+++ b/tempest/api/compute/images/test_list_image_filters.py
@@ -21,6 +21,7 @@
from tempest.api.compute import base
from tempest.common.utils import data_utils
+from tempest.common import waiters
from tempest import config
from tempest import test
@@ -61,7 +62,7 @@
time.sleep(1)
image_file = six.StringIO(('*' * 1024))
cls.glance_client.update_image(image_id, data=image_file)
- cls.client.wait_for_image_status(image_id, 'ACTIVE')
+ waiters.wait_for_image_status(cls.client, image_id, 'ACTIVE')
body = cls.client.show_image(image_id)
return body
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index c3c50d6..396327b 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -46,6 +46,28 @@
super(AttachInterfacesTestJSON, cls).setup_clients()
cls.client = cls.os.interfaces_client
+ def wait_for_interface_status(self, server, port_id, status):
+ """Waits for a interface to reach a given status."""
+ body = self.client.show_interface(server, port_id)
+ interface_status = body['port_state']
+ start = int(time.time())
+
+ while(interface_status != status):
+ time.sleep(self.build_interval)
+ body = self.client.show_interface(server, port_id)
+ interface_status = body['port_state']
+
+ timed_out = int(time.time()) - start >= self.build_timeout
+
+ if interface_status != status and timed_out:
+ message = ('Interface %s failed to reach %s status '
+ '(current %s) within the required time (%s s).' %
+ (port_id, status, interface_status,
+ self.build_timeout))
+ raise exceptions.TimeoutException(message)
+
+ return body
+
def _check_interface(self, iface, port_id=None, network_id=None,
fixed_ip=None, mac_addr=None):
self.assertIn('port_state', iface)
@@ -61,14 +83,14 @@
def _create_server_get_interfaces(self):
server = self.create_test_server(wait_until='ACTIVE')
ifs = self.client.list_interfaces(server['id'])
- body = self.client.wait_for_interface_status(
+ body = self.wait_for_interface_status(
server['id'], ifs[0]['port_id'], 'ACTIVE')
ifs[0]['port_state'] = body['port_state']
return server, ifs
def _test_create_interface(self, server):
iface = self.client.create_interface(server['id'])
- iface = self.client.wait_for_interface_status(
+ iface = self.wait_for_interface_status(
server['id'], iface['port_id'], 'ACTIVE')
self._check_interface(iface)
return iface
@@ -77,7 +99,7 @@
network_id = ifs[0]['net_id']
iface = self.client.create_interface(server['id'],
network_id=network_id)
- iface = self.client.wait_for_interface_status(
+ iface = self.wait_for_interface_status(
server['id'], iface['port_id'], 'ACTIVE')
self._check_interface(iface, network_id=network_id)
return iface
diff --git a/tempest/api/compute/servers/test_create_server.py b/tempest/api/compute/servers/test_create_server.py
index 8b0e1a4..ea5f142 100644
--- a/tempest/api/compute/servers/test_create_server.py
+++ b/tempest/api/compute/servers/test_create_server.py
@@ -91,7 +91,7 @@
@test.idempotent_id('585e934c-448e-43c4-acbf-d06a9b899997')
def test_list_servers_with_detail(self):
# The created server should be in the detailed list of all servers
- body = self.client.list_servers_with_detail()
+ body = self.client.list_servers(detail=True)
servers = body['servers']
found = any([i for i in servers if i['id'] == self.server['id']])
self.assertTrue(found)
diff --git a/tempest/api/compute/servers/test_list_server_filters.py b/tempest/api/compute/servers/test_list_server_filters.py
index 326334c..6d546d8 100644
--- a/tempest/api/compute/servers/test_list_server_filters.py
+++ b/tempest/api/compute/servers/test_list_server_filters.py
@@ -92,7 +92,7 @@
def test_list_servers_filter_by_image(self):
# Filter the list of servers by image
params = {'image': self.image_ref}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1['id'], map(lambda x: x['id'], servers))
@@ -103,7 +103,7 @@
def test_list_servers_filter_by_flavor(self):
# Filter the list of servers by flavor
params = {'flavor': self.flavor_ref_alt}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertNotIn(self.s1['id'], map(lambda x: x['id'], servers))
@@ -114,7 +114,7 @@
def test_list_servers_filter_by_server_name(self):
# Filter the list of servers by server name
params = {'name': self.s1_name}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -125,7 +125,7 @@
def test_list_servers_filter_by_server_status(self):
# Filter the list of servers by server status
params = {'status': 'active'}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1['id'], map(lambda x: x['id'], servers))
@@ -139,7 +139,7 @@
self.client.stop(self.s1['id'])
self.client.wait_for_server_status(self.s1['id'],
'SHUTOFF')
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
self.client.start(self.s1['id'])
self.client.wait_for_server_status(self.s1['id'],
'ACTIVE')
@@ -153,21 +153,21 @@
def test_list_servers_filter_by_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 1}
- servers = self.client.list_servers(params)
+ servers = self.client.list_servers(**params)
self.assertEqual(1, len([x for x in servers['servers'] if 'id' in x]))
@test.idempotent_id('b1495414-2d93-414c-8019-849afe8d319e')
def test_list_servers_filter_by_zero_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 0}
- servers = self.client.list_servers(params)
+ servers = self.client.list_servers(**params)
self.assertEqual(0, len(servers['servers']))
@test.idempotent_id('37791bbd-90c0-4de0-831e-5f38cba9c6b3')
def test_list_servers_filter_by_exceed_limit(self):
# Verify only the expected number of servers are returned
params = {'limit': 100000}
- servers = self.client.list_servers(params)
+ servers = self.client.list_servers(**params)
all_servers = self.client.list_servers()
self.assertEqual(len([x for x in all_servers['servers'] if 'id' in x]),
len([x for x in servers['servers'] if 'id' in x]))
@@ -177,7 +177,7 @@
def test_list_servers_detailed_filter_by_image(self):
# Filter the detailed list of servers by image
params = {'image': self.image_ref}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
self.assertIn(self.s1['id'], map(lambda x: x['id'], servers))
@@ -188,7 +188,7 @@
def test_list_servers_detailed_filter_by_flavor(self):
# Filter the detailed list of servers by flavor
params = {'flavor': self.flavor_ref_alt}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
self.assertNotIn(self.s1['id'], map(lambda x: x['id'], servers))
@@ -199,7 +199,7 @@
def test_list_servers_detailed_filter_by_server_name(self):
# Filter the detailed list of servers by server name
params = {'name': self.s1_name}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -210,7 +210,7 @@
def test_list_servers_detailed_filter_by_server_status(self):
# Filter the detailed list of servers by server status
params = {'status': 'active'}
- body = self.client.list_servers_with_detail(params)
+ body = self.client.list_servers(detail=True, **params)
servers = body['servers']
test_ids = [s['id'] for s in (self.s1, self.s2, self.s3)]
@@ -224,7 +224,7 @@
def test_list_servers_filtered_by_name_wildcard(self):
# List all servers that contains '-instance' in name
params = {'name': '-instance'}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -235,7 +235,7 @@
part_name = self.s1_name[6:-1]
params = {'name': part_name}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -248,7 +248,7 @@
regexes = ['^.*\-instance\-[0-9]+$', '^.*\-instance\-.*$']
for regex in regexes:
params = {'name': regex}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -259,7 +259,7 @@
part_name = self.s1_name[-10:]
params = {'name': part_name}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -282,7 +282,7 @@
else:
msg = "Skipped until bug 1450859 is resolved"
raise self.skipException(msg)
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -304,7 +304,7 @@
params = {'ip': ip}
else:
params = {'ip6': ip}
- body = self.client.list_servers(params)
+ body = self.client.list_servers(**params)
servers = body['servers']
self.assertIn(self.s1_name, map(lambda x: x['name'], servers))
@@ -315,5 +315,5 @@
def test_list_servers_detailed_limit_results(self):
# Verify only the expected number of detailed results are returned
params = {'limit': 1}
- servers = self.client.list_servers_with_detail(params)
+ servers = self.client.list_servers(detail=True, **params)
self.assertEqual(1, len(servers['servers']))
diff --git a/tempest/api/compute/servers/test_list_servers_negative.py b/tempest/api/compute/servers/test_list_servers_negative.py
index fd4d902..def6cf5 100644
--- a/tempest/api/compute/servers/test_list_servers_negative.py
+++ b/tempest/api/compute/servers/test_list_servers_negative.py
@@ -68,7 +68,7 @@
def test_list_servers_by_non_existing_image(self):
# Listing servers for a non existing image returns empty list
non_existing_image = '1234abcd-zzz0-aaa9-ppp3-0987654abcde'
- body = self.client.list_servers(dict(image=non_existing_image))
+ body = self.client.list_servers(image=non_existing_image)
servers = body['servers']
self.assertEqual([], servers)
@@ -77,7 +77,7 @@
def test_list_servers_by_non_existing_flavor(self):
# Listing servers by non existing flavor returns empty list
non_existing_flavor = 1234
- body = self.client.list_servers(dict(flavor=non_existing_flavor))
+ body = self.client.list_servers(flavor=non_existing_flavor)
servers = body['servers']
self.assertEqual([], servers)
@@ -86,7 +86,7 @@
def test_list_servers_by_non_existing_server_name(self):
# Listing servers for a non existent server name returns empty list
non_existing_name = 'junk_server_1234'
- body = self.client.list_servers(dict(name=non_existing_name))
+ body = self.client.list_servers(name=non_existing_name)
servers = body['servers']
self.assertEqual([], servers)
@@ -95,21 +95,21 @@
def test_list_servers_status_non_existing(self):
# Return an empty list when invalid status is specified
non_existing_status = 'BALONEY'
- body = self.client.list_servers(dict(status=non_existing_status))
+ body = self.client.list_servers(status=non_existing_status)
servers = body['servers']
self.assertEqual([], servers)
@test.idempotent_id('12c80a9f-2dec-480e-882b-98ba15757659')
def test_list_servers_by_limits(self):
# List servers by specifying limits
- body = self.client.list_servers({'limit': 1})
+ body = self.client.list_servers(limit=1)
self.assertEqual(1, len([x for x in body['servers'] if 'id' in x]))
@test.attr(type=['negative'])
@test.idempotent_id('d47c17fb-eebd-4287-8e95-f20a7e627b18')
def test_list_servers_by_limits_greater_than_actual_count(self):
# List servers by specifying a greater value for limit
- body = self.client.list_servers({'limit': 100})
+ body = self.client.list_servers(limit=100)
self.assertEqual(len(self.existing_fixtures), len(body['servers']))
@test.attr(type=['negative'])
@@ -117,28 +117,29 @@
def test_list_servers_by_limits_pass_string(self):
# Return an error if a string value is passed for limit
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
- {'limit': 'testing'})
+ limit='testing')
@test.attr(type=['negative'])
@test.idempotent_id('62610dd9-4713-4ee0-8beb-fd2c1aa7f950')
def test_list_servers_by_limits_pass_negative_value(self):
# Return an error if a negative value for limit is passed
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
- {'limit': -1})
+ limit=-1)
@test.attr(type=['negative'])
@test.idempotent_id('87d12517-e20a-4c9c-97b6-dd1628d6d6c9')
def test_list_servers_by_changes_since_invalid_date(self):
# Return an error when invalid date format is passed
+ params = {'changes-since': '2011/01/01'}
self.assertRaises(lib_exc.BadRequest, self.client.list_servers,
- {'changes-since': '2011/01/01'})
+ **params)
@test.attr(type=['negative'])
@test.idempotent_id('74745ad8-b346-45b5-b9b8-509d7447fc1f')
def test_list_servers_by_changes_since_future_date(self):
# Return an empty list when a date in the future is passed
changes_since = {'changes-since': '2051-01-01T12:34:00Z'}
- body = self.client.list_servers(changes_since)
+ body = self.client.list_servers(**changes_since)
self.assertEqual(0, len(body['servers']))
@test.attr(type=['negative'])
@@ -146,7 +147,7 @@
def test_list_servers_detail_server_is_deleted(self):
# Server details are not listed for a deleted server
deleted_ids = [s['id'] for s in self.deleted_fixtures]
- body = self.client.list_servers_with_detail()
+ body = self.client.list_servers(detail=True)
servers = body['servers']
actual = [srv for srv in servers
if srv['id'] in deleted_ids]
diff --git a/tempest/api/image/v2/test_images_member.py b/tempest/api/image/v2/test_images_member.py
index d497005..d89803d 100644
--- a/tempest/api/image/v2/test_images_member.py
+++ b/tempest/api/image/v2/test_images_member.py
@@ -89,3 +89,19 @@
def test_get_image_members_schema(self):
body = self.os_img_client.show_schema("members")
self.assertEqual("members", body['name'])
+
+ @test.idempotent_id('cb961424-3f68-4d21-8e36-30ad66fb6bfb')
+ def test_get_private_image(self):
+ image_id = self._create_image()
+ member = self.os_img_client.add_image_member(image_id,
+ self.alt_tenant_id)
+ self.assertEqual(member['member_id'], self.alt_tenant_id)
+ self.assertEqual(member['image_id'], image_id)
+ self.assertEqual(member['status'], 'pending')
+ self.assertNotIn(image_id, self._list_image_ids_as_alt())
+ self.alt_img_client.update_image_member(image_id,
+ self.alt_tenant_id,
+ {'status': 'accepted'})
+ self.assertIn(image_id, self._list_image_ids_as_alt())
+ self.os_img_client.remove_image_member(image_id, self.alt_tenant_id)
+ self.assertNotIn(image_id, self._list_image_ids_as_alt())
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
index 4c774da..d1eb694 100644
--- a/tempest/api/volume/v2/test_volumes_list.py
+++ b/tempest/api/volume/v2/test_volumes_list.py
@@ -14,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+from six.moves.urllib import parse
+
from tempest.api.volume import base
from tempest import test
@@ -87,3 +89,101 @@
_list_details_with_multiple_params()
_list_details_with_multiple_params(sort_dir='desc')
+
+ def _test_pagination(self, resource, ids=None, limit=1, **kwargs):
+ """Check list pagination functionality for a resource.
+
+ This method requests the list of resources and follows pagination
+ links.
+
+ If an iterable is supplied in ids it will check that all ids are
+ retrieved and that only those are listed, that we will get a next
+ link for an empty page if the number of items is divisible by used
+ limit (this is expected behavior).
+
+ We can specify number of items per request using limit argument.
+ """
+
+ # Get list method for the type of resource from the client
+ client = getattr(self, resource + '_client')
+ method = getattr(client, 'list_' + resource)
+
+ # Include limit in params for list request
+ params = kwargs.pop('params', {})
+ params['limit'] = limit
+
+ # Store remaining items we are expecting from list
+ if ids is not None:
+ remaining = list(ids)
+ else:
+ remaining = None
+
+ # Mark that we are not comming from a next link
+ next = None
+
+ while True:
+ # Get a list page
+ response = method(return_body=True, params=params, **kwargs)
+
+ # If we have to check ids
+ if remaining is not None:
+ # Confirm we receive expected number of elements
+ num_expected = min(len(remaining), limit)
+ self.assertEqual(num_expected, len(response[resource]),
+ 'Requested %(#expect)d but got %(#received)d '
+ % {'#expect': num_expected,
+ '#received': len(response[resource])})
+
+ # For each received element
+ for element in response[resource]:
+ element_id = element['id']
+ # Check it's one of expected ids
+ self.assertIn(element_id,
+ ids,
+ 'Id %(id)s is not in expected ids %(ids)s' %
+ {'id': element_id, 'ids': ids})
+ # If not in remaining, we have received it twice
+ self.assertIn(element_id,
+ remaining,
+ 'Id %s was received twice' % element_id)
+ # We no longer expect it
+ remaining.remove(element_id)
+
+ # If we come from a next link check that absolute url is the same
+ # as the one used for this request
+ if next:
+ self.assertEqual(next, response.response['content-location'])
+
+ # Get next from response
+ next = None
+ for link in response.get(resource + '_links', ()):
+ if link['rel'] == 'next':
+ next = link['href']
+ break
+
+ # Check if we have next and we shouldn't or the other way around
+ if remaining is not None:
+ if remaining or (num_expected and len(ids) % limit == 0):
+ self.assertIsNotNone(next, 'Missing link to next page')
+ else:
+ self.assertIsNone(next, 'Unexpected link to next page')
+
+ # If we can follow to the next page, get params from url to make
+ # request in the form of a relative URL
+ if next:
+ params = parse.urlparse(next).query
+
+ # If cannot follow make sure it's because we have finished
+ else:
+ self.assertListEqual([], remaining or [],
+ 'No more pages reported, but still '
+ 'missing ids %s' % remaining)
+ break
+
+ @test.idempotent_id('e9138a2c-f67b-4796-8efa-635c196d01de')
+ def test_volume_list_details_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=True)
+
+ @test.idempotent_id('af55e775-8e4b-4feb-8719-215c43b0238c')
+ def test_volume_list_pagination(self):
+ self._test_pagination('volumes', ids=self.volume_id_list, detail=False)
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index bdbd6bc..85a03cf 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -132,6 +132,27 @@
raise exceptions.TimeoutException(message)
+def wait_for_volume_status(client, volume_id, status):
+ """Waits for a Volume to reach a given status."""
+ body = client.show_volume(volume_id)
+ volume_status = body['status']
+ start = int(time.time())
+
+ while volume_status != status:
+ time.sleep(client.build_interval)
+ body = client.show_volume(volume_id)
+ volume_status = body['status']
+ if volume_status == 'error':
+ raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
+
+ if int(time.time()) - start >= client.build_timeout:
+ message = ('Volume %s failed to reach %s status (current %s) '
+ 'within the required time (%s s).' %
+ (volume_id, status, volume_status,
+ client.build_timeout))
+ raise exceptions.TimeoutException(message)
+
+
def wait_for_bm_node_status(client, node_id, attr, status):
"""Waits for a baremetal node attribute to reach given status.
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index 8f0b4dc..95a8356 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -559,27 +559,27 @@
def _list_networks(self, *args, **kwargs):
"""List networks using admin creds """
- return self._admin_lister('networks')(*args, **kwargs)
+ networks_list = self.admin_manager.network_client.list_networks(
+ *args, **kwargs)
+ return networks_list['networks']
def _list_subnets(self, *args, **kwargs):
"""List subnets using admin creds """
- return self._admin_lister('subnets')(*args, **kwargs)
+ subnets_list = self.admin_manager.network_client.list_subnets(
+ *args, **kwargs)
+ return subnets_list['subnets']
def _list_routers(self, *args, **kwargs):
"""List routers using admin creds """
- return self._admin_lister('routers')(*args, **kwargs)
+ routers_list = self.admin_manager.network_client.list_routers(
+ *args, **kwargs)
+ return routers_list['routers']
def _list_ports(self, *args, **kwargs):
"""List ports using admin creds """
- return self._admin_lister('ports')(*args, **kwargs)
-
- def _admin_lister(self, resource_type):
- def temp(*args, **kwargs):
- temp_method = self.admin_manager.network_client.__getattr__(
- 'list_%s' % resource_type)
- resource_list = temp_method(*args, **kwargs)
- return resource_list[resource_type]
- return temp
+ ports_list = self.admin_manager.network_client.list_ports(
+ *args, **kwargs)
+ return ports_list['ports']
def _create_subnet(self, network, client=None, namestart='subnet-smoke',
**kwargs):
diff --git a/tempest/scenario/test_large_ops.py b/tempest/scenario/test_large_ops.py
index ffb35fb..1a18be5 100644
--- a/tempest/scenario/test_large_ops.py
+++ b/tempest/scenario/test_large_ops.py
@@ -102,7 +102,7 @@
**create_kwargs)
# needed because of bug 1199788
params = {'name': name}
- server_list = self.servers_client.list_servers(params)
+ server_list = self.servers_client.list_servers(**params)
self.servers = server_list['servers']
for server in self.servers:
# after deleting all servers - wait for all servers to clear
diff --git a/tempest/services/compute/json/extensions_client.py b/tempest/services/compute/json/extensions_client.py
index cd7ecbc..ec60872 100644
--- a/tempest/services/compute/json/extensions_client.py
+++ b/tempest/services/compute/json/extensions_client.py
@@ -28,11 +28,6 @@
self.validate_response(schema.list_extensions, resp, body)
return service_client.ResponseBodyList(resp, body['extensions'])
- def is_enabled(self, extension):
- extensions = self.list_extensions()
- exts = extensions['extensions']
- return any([e for e in exts if e['name'] == extension])
-
def show_extension(self, extension_alias):
resp, body = self.get('extensions/%s' % extension_alias)
body = json.loads(body)
diff --git a/tempest/services/compute/json/images_client.py b/tempest/services/compute/json/images_client.py
index 9ce2eef..172bbe2 100644
--- a/tempest/services/compute/json/images_client.py
+++ b/tempest/services/compute/json/images_client.py
@@ -20,7 +20,6 @@
from tempest.api_schema.response.compute.v2_1 import images as schema
from tempest.common import service_client
-from tempest.common import waiters
class ImagesClient(service_client.ServiceClient):
@@ -73,10 +72,6 @@
self.validate_response(schema.delete, resp, body)
return service_client.ResponseBody(resp, body)
- def wait_for_image_status(self, image_id, status):
- """Waits for an image to reach a given status."""
- waiters.wait_for_image_status(self, image_id, status)
-
def list_image_metadata(self, image_id):
"""Lists all metadata items for an image."""
resp, body = self.get("images/%s/metadata" % image_id)
diff --git a/tempest/services/compute/json/interfaces_client.py b/tempest/services/compute/json/interfaces_client.py
index 9d6c9d4..0e0ff47 100644
--- a/tempest/services/compute/json/interfaces_client.py
+++ b/tempest/services/compute/json/interfaces_client.py
@@ -14,12 +14,10 @@
# under the License.
import json
-import time
from tempest.api_schema.response.compute.v2_1 import interfaces as schema
from tempest.api_schema.response.compute.v2_1 import servers as servers_schema
from tempest.common import service_client
-from tempest import exceptions
class InterfacesClient(service_client.ServiceClient):
@@ -60,28 +58,6 @@
self.validate_response(schema.delete_interface, resp, body)
return service_client.ResponseBody(resp, body)
- def wait_for_interface_status(self, server, port_id, status):
- """Waits for a interface to reach a given status."""
- body = self.show_interface(server, port_id)
- interface_status = body['port_state']
- start = int(time.time())
-
- while(interface_status != status):
- time.sleep(self.build_interval)
- body = self.show_interface(server, port_id)
- interface_status = body['port_state']
-
- timed_out = int(time.time()) - start >= self.build_timeout
-
- if interface_status != status and timed_out:
- message = ('Interface %s failed to reach %s status '
- '(current %s) within the required time (%s s).' %
- (port_id, status, interface_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
-
- return body
-
def add_fixed_ip(self, server_id, network_id):
"""Add a fixed IP to input server instance."""
post_body = json.dumps({
diff --git a/tempest/services/compute/json/servers_client.py b/tempest/services/compute/json/servers_client.py
index 2ecb2d4..7263014 100644
--- a/tempest/services/compute/json/servers_client.py
+++ b/tempest/services/compute/json/servers_client.py
@@ -149,28 +149,21 @@
self.validate_response(schema.delete_server, resp, body)
return service_client.ResponseBody(resp, body)
- def list_servers(self, params=None):
+ def list_servers(self, detail=False, **params):
"""Lists all servers for a user."""
url = 'servers'
+ _schema = schema.list_servers
+
+ if detail:
+ url += '/detail'
+ _schema = schema.list_servers_detail
if params:
url += '?%s' % urllib.urlencode(params)
resp, body = self.get(url)
body = json.loads(body)
- self.validate_response(schema.list_servers, resp, body)
- return service_client.ResponseBody(resp, body)
-
- def list_servers_with_detail(self, params=None):
- """Lists all servers in detail for a user."""
-
- url = 'servers/detail'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.validate_response(schema.list_servers_detail, resp, body)
+ self.validate_response(_schema, resp, body)
return service_client.ResponseBody(resp, body)
def wait_for_server_status(self, server_id, status, extra_timeout=0,
diff --git a/tempest/services/compute/json/volumes_extensions_client.py b/tempest/services/compute/json/volumes_extensions_client.py
index 08bb4bc..985038d 100644
--- a/tempest/services/compute/json/volumes_extensions_client.py
+++ b/tempest/services/compute/json/volumes_extensions_client.py
@@ -14,14 +14,13 @@
# under the License.
import json
-import time
from six.moves.urllib import parse as urllib
from tempest_lib import exceptions as lib_exc
from tempest.api_schema.response.compute.v2_1 import volumes as schema
from tempest.common import service_client
-from tempest import exceptions
+from tempest.common import waiters
class VolumesExtensionsClient(service_client.ServiceClient):
@@ -83,23 +82,7 @@
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status."""
- body = self.show_volume(volume_id)
- volume_status = body['status']
- start = int(time.time())
-
- while volume_status != status:
- time.sleep(self.build_interval)
- body = self.show_volume(volume_id)
- volume_status = body['status']
- if volume_status == 'error':
- raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
-
- if int(time.time()) - start >= self.build_timeout:
- message = ('Volume %s failed to reach %s status (current %s) '
- 'within the required time (%s s).' %
- (volume_id, status, volume_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
+ waiters.wait_for_volume_status(self, volume_id, status)
def is_resource_deleted(self, id):
try:
diff --git a/tempest/services/network/json/network_client.py b/tempest/services/network/json/network_client.py
index 13449ea..b3663fc 100644
--- a/tempest/services/network/json/network_client.py
+++ b/tempest/services/network/json/network_client.py
@@ -73,17 +73,14 @@
}
return resource_plural_map.get(resource_name, resource_name + 's')
- def _lister(self, plural_name):
- def _list(**filters):
- uri = self.get_uri(plural_name)
- if filters:
- uri += '?' + urllib.urlencode(filters, doseq=1)
- resp, body = self.get(uri)
- result = {plural_name: self.deserialize_list(body)}
- self.expected_success(200, resp.status)
- return service_client.ResponseBody(resp, result)
-
- return _list
+ def _list_resources(self, uri, **filters):
+ req_uri = self.uri_prefix + uri
+ if filters:
+ req_uri += '?' + urllib.urlencode(filters, doseq=1)
+ resp, body = self.get(req_uri)
+ body = self.deserialize_list(body)
+ self.expected_success(200, resp.status)
+ return service_client.ResponseBody(resp, body)
def _delete_resource(self, uri):
req_uri = self.uri_prefix + uri
@@ -119,15 +116,6 @@
self.expected_success(200, resp.status)
return service_client.ResponseBody(resp, body)
- def __getattr__(self, name):
- method_prefixes = ["list_"]
- method_functors = [self._lister]
- for index, prefix in enumerate(method_prefixes):
- prefix_len = len(prefix)
- if name[:prefix_len] == prefix:
- return method_functors[index](name[prefix_len:])
- raise AttributeError(name)
-
def create_network(self, **kwargs):
uri = '/networks'
post_data = {'network': kwargs}
@@ -146,6 +134,10 @@
uri = '/networks/%s' % network_id
return self._delete_resource(uri)
+ def list_networks(self, **filters):
+ uri = '/networks'
+ return self._list_resources(uri, **filters)
+
def create_subnet(self, **kwargs):
uri = '/subnets'
post_data = {'subnet': kwargs}
@@ -164,6 +156,10 @@
uri = '/subnets/%s' % subnet_id
return self._delete_resource(uri)
+ def list_subnets(self, **filters):
+ uri = '/subnets'
+ return self._list_resources(uri, **filters)
+
def create_port(self, **kwargs):
uri = '/ports'
post_data = {'port': kwargs}
@@ -182,6 +178,10 @@
uri = '/ports/%s' % port_id
return self._delete_resource(uri)
+ def list_ports(self, **filters):
+ uri = '/ports'
+ return self._list_resources(uri, **filters)
+
def create_floatingip(self, **kwargs):
uri = '/floatingips'
post_data = {'floatingip': kwargs}
@@ -200,6 +200,10 @@
uri = '/floatingips/%s' % floatingip_id
return self._delete_resource(uri)
+ def list_floatingips(self, **filters):
+ uri = '/floatingips'
+ return self._list_resources(uri, **filters)
+
def create_metering_label(self, **kwargs):
uri = '/metering/metering-labels'
post_data = {'metering_label': kwargs}
@@ -213,6 +217,10 @@
uri = '/metering/metering-labels/%s' % metering_label_id
return self._delete_resource(uri)
+ def list_metering_labels(self, **filters):
+ uri = '/metering/metering-labels'
+ return self._list_resources(uri, **filters)
+
def create_metering_label_rule(self, **kwargs):
uri = '/metering/metering-label-rules'
post_data = {'metering_label_rule': kwargs}
@@ -226,6 +234,10 @@
uri = '/metering/metering-label-rules/%s' % metering_label_rule_id
return self._delete_resource(uri)
+ def list_metering_label_rules(self, **filters):
+ uri = '/metering/metering-label-rules'
+ return self._list_resources(uri, **filters)
+
def create_security_group(self, **kwargs):
uri = '/security-groups'
post_data = {'security_group': kwargs}
@@ -244,6 +256,10 @@
uri = '/security-groups/%s' % security_group_id
return self._delete_resource(uri)
+ def list_security_groups(self, **filters):
+ uri = '/security-groups'
+ return self._list_resources(uri, **filters)
+
def create_security_group_rule(self, **kwargs):
uri = '/security-group-rules'
post_data = {'security_group_rule': kwargs}
@@ -257,10 +273,18 @@
uri = '/security-group-rules/%s' % security_group_rule_id
return self._delete_resource(uri)
+ def list_security_group_rules(self, **filters):
+ uri = '/security-group-rules'
+ return self._list_resources(uri, **filters)
+
def show_extension(self, ext_alias, **fields):
uri = '/extensions/%s' % ext_alias
return self._show_resource(uri, **fields)
+ def list_extensions(self, **filters):
+ uri = '/extensions'
+ return self._list_resources(uri, **filters)
+
# Common methods that are hard to automate
def create_bulk_network(self, names):
network_list = [{'name': name} for name in names]
@@ -268,7 +292,7 @@
body = self.serialize_list(post_data, "networks", "network")
uri = self.get_uri("networks")
resp, body = self.post(uri, body)
- body = {'networks': self.deserialize_list(body)}
+ body = self.deserialize_list(body)
self.expected_success(201, resp.status)
return service_client.ResponseBody(resp, body)
@@ -277,7 +301,7 @@
body = self.serialize_list(post_data, 'subnets', 'subnet')
uri = self.get_uri('subnets')
resp, body = self.post(uri, body)
- body = {'subnets': self.deserialize_list(body)}
+ body = self.deserialize_list(body)
self.expected_success(201, resp.status)
return service_client.ResponseBody(resp, body)
@@ -286,7 +310,7 @@
body = self.serialize_list(post_data, 'ports', 'port')
uri = self.get_uri('ports')
resp, body = self.post(uri, body)
- body = {'ports': self.deserialize_list(body)}
+ body = self.deserialize_list(body)
self.expected_success(201, resp.status)
return service_client.ResponseBody(resp, body)
@@ -351,14 +375,7 @@
return json.loads(body)
def deserialize_list(self, body):
- res = json.loads(body)
- # expecting response in form
- # {'resources': [ res1, res2] } => when pagination disabled
- # {'resources': [..], 'resources_links': {}} => if pagination enabled
- for k in res.keys():
- if k.endswith("_links"):
- continue
- return res[k]
+ return json.loads(body)
def serialize(self, data):
return json.dumps(data)
@@ -385,6 +402,10 @@
uri = '/quotas/%s' % tenant_id
return self._show_resource(uri, **fields)
+ def list_quotas(self, **filters):
+ uri = '/quotas'
+ return self._list_resources(uri, **filters)
+
def create_router(self, name, admin_state_up=True, **kwargs):
post_body = {'router': kwargs}
post_body['router']['name'] = name
@@ -442,6 +463,10 @@
uri = '/routers/%s' % router_id
return self._delete_resource(uri)
+ def list_routers(self, **filters):
+ uri = '/routers'
+ return self._list_resources(uri, **filters)
+
def update_router_with_snat_gw_info(self, router_id, **kwargs):
"""Update a router passing also the enable_snat attribute.
@@ -514,6 +539,10 @@
uri = '/agents/%s' % agent_id
return self._show_resource(uri, **fields)
+ def list_agents(self, **filters):
+ uri = '/agents'
+ return self._list_resources(uri, **filters)
+
def list_routers_on_l3_agent(self, agent_id):
uri = '%s/agents/%s/l3-routers' % (self.uri_prefix, agent_id)
resp, body = self.get(uri)
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 1f8548e..b8e3464 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -14,13 +14,13 @@
# under the License.
import json
-import time
+import six
from six.moves.urllib import parse as urllib
from tempest_lib import exceptions as lib_exc
from tempest.common import service_client
-from tempest import exceptions
+from tempest.common import waiters
class BaseVolumesClient(service_client.ServiceClient):
@@ -40,18 +40,57 @@
"""Return the element 'attachment' from input volumes."""
return volume['attachments'][0]
- def list_volumes(self, detail=False, params=None):
- """List all the volumes created."""
+ def _ext_get(self, url, key=None, status=200):
+ """Extended get method.
+
+ Retrieves requested url, checks that status is expected status and
+ return a ResponseBody, ResponseBodyList or ResponseBodyData depending
+ on received data's key entry.
+
+ If key is not specified or is None we will return the whole body in a
+ ResponseBody class.
+ """
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(status, resp.status)
+
+ if not key:
+ return service_client.ResponseBody(resp, body)
+ elif isinstance(body[key], dict):
+ return service_client.ResponseBody(resp, body[key])
+ elif isinstance(body[key], list):
+ return service_client.ResponseBodyList(resp, body[key])
+
+ return service_client.ResponseBodyData(resp, body[key])
+
+ def _prepare_params(self, params):
+ """Prepares params for use in get or _ext_get methods.
+
+ If params is a string it will be left as it is, but if it's not it will
+ be urlencoded.
+ """
+ if isinstance(params, six.string_types):
+ return params
+ return urllib.urlencode(params)
+
+ def list_volumes(self, detail=False, params=None, return_body=False):
+ """List all the volumes created.
+
+ Params can be a string (must be urlencoded) or a dictionary.
+ If return_body is True then we will return the whole response body in
+ a ResponseBody class, it it's False or has not been specified we will
+ return only the list of volumes in a ResponseBodyList (inherits from
+ list).
+ """
url = 'volumes'
if detail:
url += '/detail'
if params:
- url += '?%s' % urllib.urlencode(params)
+ url += '?%s' % self._prepare_params(params)
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return service_client.ResponseBodyList(resp, body['volumes'])
+ key = None if return_body else 'volumes'
+ return self._ext_get(url, key)
def show_volume(self, volume_id):
"""Returns the details of a single volume."""
@@ -161,25 +200,7 @@
def wait_for_volume_status(self, volume_id, status):
"""Waits for a Volume to reach a given status."""
- body = self.show_volume(volume_id)
- volume_status = body['status']
- start = int(time.time())
-
- while volume_status != status:
- time.sleep(self.build_interval)
- body = self.show_volume(volume_id)
- volume_status = body['status']
- if volume_status == 'error':
- raise exceptions.VolumeBuildErrorException(volume_id=volume_id)
-
- if int(time.time()) - start >= self.build_timeout:
- message = ('Volume %s failed to reach %s status (current: %s) '
- 'within the required time '
- '(%s s).' % (volume_id,
- status,
- volume_status,
- self.build_timeout))
- raise exceptions.TimeoutException(message)
+ waiters.wait_for_volume_status(self, volume_id, status)
def is_resource_deleted(self, id):
try:
diff --git a/tempest/stress/cleanup.py b/tempest/stress/cleanup.py
index 29c4401..d9b430e 100644
--- a/tempest/stress/cleanup.py
+++ b/tempest/stress/cleanup.py
@@ -24,7 +24,7 @@
def cleanup():
admin_manager = clients.AdminManager()
- body = admin_manager.servers_client.list_servers({"all_tenants": True})
+ body = admin_manager.servers_client.list_servers(all_tenants=True)
LOG.info("Cleanup::remove %s servers" % len(body['servers']))
for s in body['servers']:
try:
diff --git a/tempest/tests/test_glance_http.py b/tempest/tests/test_glance_http.py
index 7850ee4..71aa395 100644
--- a/tempest/tests/test_glance_http.py
+++ b/tempest/tests/test_glance_http.py
@@ -137,7 +137,8 @@
resp, body = self.client.raw_request('PUT', '/images', body=req_body)
self.assertEqual(200, resp.status)
self.assertEqual('fake_response_body', body.read())
- httplib.HTTPConnection.send.assert_call_count(req_body.tell())
+ call_count = httplib.HTTPConnection.send.call_count
+ self.assertEqual(call_count - 1, req_body.tell())
def test_get_connection_class_for_https(self):
conn_class = self.client.get_connection_class('https')
diff --git a/test-requirements.txt b/test-requirements.txt
index 2b3607d..7115168 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -7,6 +7,7 @@
python-subunit>=0.0.18
oslosphinx>=2.5.0 # Apache-2.0
mox>=0.5.3
-mock>=1.0
+mock>=1.1;python_version!='2.6'
+mock==1.0.1;python_version=='2.6'
coverage>=3.6
oslotest>=1.5.1 # Apache-2.0
diff --git a/tools/install_venv.py b/tools/install_venv.py
index 96b8279..d6d9c8e 100644
--- a/tools/install_venv.py
+++ b/tools/install_venv.py
@@ -48,11 +48,11 @@
def main(argv):
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
- if os.environ.get('tools_path'):
- root = os.environ['tools_path']
+ if os.environ.get('TOOLS_PATH'):
+ root = os.environ['TOOLS_PATH']
venv = os.path.join(root, '.venv')
- if os.environ.get('venv'):
- venv = os.environ['venv']
+ if os.environ.get('VENV'):
+ venv = os.environ['VENV']
pip_requires = os.path.join(root, 'requirements.txt')
test_requires = os.path.join(root, 'test-requirements.txt')
diff --git a/tools/install_venv_common.py b/tools/install_venv_common.py
index 743b59d..e279159 100644
--- a/tools/install_venv_common.py
+++ b/tools/install_venv_common.py
@@ -101,6 +101,7 @@
print('done.')
else:
print("venv already exists...")
+ pass
def pip_install(self, *args):
self.run_command(['tools/with_venv.sh',
@@ -124,7 +125,7 @@
parser.add_option('-n', '--no-site-packages',
action='store_true',
help="Do not inherit packages from global Python "
- "install")
+ "install.")
return parser.parse_args(argv[1:])[0]
diff --git a/tools/with_venv.sh b/tools/with_venv.sh
index 550c477..165c883 100755
--- a/tools/with_venv.sh
+++ b/tools/with_venv.sh
@@ -1,4 +1,6 @@
#!/bin/bash
-TOOLS=`dirname $0`
-VENV=$TOOLS/../.venv
-source $VENV/bin/activate && "$@"
+TOOLS_PATH=${TOOLS_PATH:-$(dirname $0)/../}
+VENV_PATH=${VENV_PATH:-${TOOLS_PATH}}
+VENV_DIR=${VENV_DIR:-/.venv}
+VENV=${VENV:-${VENV_PATH}/${VENV_DIR}}
+source ${VENV}/bin/activate && "$@"