Merge "Remove deprecated Javelin CLI utilities from Tempest"
diff --git a/README.rst b/README.rst
index d109a2d..13f4f61 100644
--- a/README.rst
+++ b/README.rst
@@ -223,7 +223,7 @@
$ cd $TEMPEST_ROOT_DIR
$ oslo-config-generator --config-file \
- etc/config-generator.tempest.conf \
+ tempest/cmd/config-generator.tempest.conf \
--output-file etc/tempest.conf
After that, open up the ``etc/tempest.conf`` file and edit the
diff --git a/releasenotes/notes/add-httptimeout-in-restclient-ax78061900e3f3d7.yaml b/releasenotes/notes/add-httptimeout-in-restclient-ax78061900e3f3d7.yaml
new file mode 100644
index 0000000..a360f8e
--- /dev/null
+++ b/releasenotes/notes/add-httptimeout-in-restclient-ax78061900e3f3d7.yaml
@@ -0,0 +1,7 @@
+---
+features:
+ - RestClient now supports setting timeout in urllib3.poolmanager.
+ Clients will use CONF.service_clients.http_timeout for timeout
+ value to wait for http request to response.
+ - KeystoneAuthProvider will accept http_timeout and will use it in
+ get_credentials.
diff --git a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
index b457ddd..092014e 100644
--- a/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
+++ b/releasenotes/notes/add_subunit_describe_calls-5498a37e6cd66c4b.yaml
@@ -1,4 +1,8 @@
---
features:
- - Adds subunit-describe-calls. A parser for subunit streams to determine what
+ - |
+ Adds subunit-describe-calls. A parser for subunit streams to determine what
REST API calls are made inside of a test and in what order they are called.
+
+ * Input can be piped in or a file can be specified
+ * Output is shortened for stdout, the output file has more information
diff --git a/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml b/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml
new file mode 100644
index 0000000..64f729a
--- /dev/null
+++ b/releasenotes/notes/plugin-service-client-registration-00b19a2dd4935ba0.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - A new optional interface `TempestPlugin.get_service_clients`
+ is available to plugins. It allows them to declare
+ any service client they implement. For now this is used by
+ tempest only, for auto-registration of service clients
+ in the new class `ServiceClients`.
+ - A new singleton class `clients.ClientsRegistry` is
+ available. It holds the service clients registration data
+ from all plugins. It is used by `ServiceClients` for
+ auto-registration of the service clients implemented
+ in plugins.
diff --git a/requirements.txt b/requirements.txt
index 058ea00..0f8e94d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -13,7 +13,7 @@
oslo.i18n>=2.1.0 # Apache-2.0
oslo.log>=1.14.0 # Apache-2.0
oslo.serialization>=1.10.0 # Apache-2.0
-oslo.utils>=3.15.0 # Apache-2.0
+oslo.utils>=3.16.0 # Apache-2.0
six>=1.9.0 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
diff --git a/tempest/api/baremetal/admin/base.py b/tempest/api/baremetal/admin/base.py
index f7891dd..2d3f190 100644
--- a/tempest/api/baremetal/admin/base.py
+++ b/tempest/api/baremetal/admin/base.py
@@ -96,7 +96,7 @@
@classmethod
@creates('chassis')
- def create_chassis(cls, description=None, expect_errors=False):
+ def create_chassis(cls, description=None):
"""Wrapper utility for creating test chassis.
:param description: A description of the chassis. if not supplied,
diff --git a/tempest/api/compute/base.py b/tempest/api/compute/base.py
index 37aa5ac..5e75493 100644
--- a/tempest/api/compute/base.py
+++ b/tempest/api/compute/base.py
@@ -359,7 +359,7 @@
for address in addresses:
if address['version'] == CONF.validation.ip_version_for_ssh:
return address['addr']
- raise exceptions.ServerUnreachable()
+ raise exceptions.ServerUnreachable(server_id=server['id'])
else:
raise exceptions.InvalidConfiguration()
diff --git a/tempest/api/compute/security_groups/test_security_groups_negative.py b/tempest/api/compute/security_groups/test_security_groups_negative.py
index 5125e2b..e6abf28 100644
--- a/tempest/api/compute/security_groups/test_security_groups_negative.py
+++ b/tempest/api/compute/security_groups/test_security_groups_negative.py
@@ -44,9 +44,11 @@
security_group_id.append(body[i]['id'])
# Generate a non-existent security group id
while True:
- non_exist_id = data_utils.rand_int_id(start=999)
- if self.neutron_available:
+ if (self.neutron_available and
+ test.is_extension_enabled('security-group', 'network')):
non_exist_id = data_utils.rand_uuid()
+ else:
+ non_exist_id = data_utils.rand_int_id(start=999)
if non_exist_id not in security_group_id:
break
return non_exist_id
diff --git a/tempest/api/compute/servers/test_attach_interfaces.py b/tempest/api/compute/servers/test_attach_interfaces.py
index d02f86f..7c12bf9 100644
--- a/tempest/api/compute/servers/test_attach_interfaces.py
+++ b/tempest/api/compute/servers/test_attach_interfaces.py
@@ -21,6 +21,7 @@
from tempest.common import waiters
from tempest import config
from tempest import exceptions
+from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
from tempest import test
@@ -272,6 +273,7 @@
break
self.servers_client.remove_fixed_ip(server['id'], address=fixed_ip)
+ @decorators.skip_because(bug='1607714')
@test.idempotent_id('2f3a0127-95c7-4977-92d2-bc5aec602fb4')
def test_reassign_port_between_servers(self):
"""Tests the following:
diff --git a/tempest/api/identity/admin/v3/test_projects.py b/tempest/api/identity/admin/v3/test_projects.py
index 60bb314..1879e46 100644
--- a/tempest/api/identity/admin/v3/test_projects.py
+++ b/tempest/api/identity/admin/v3/test_projects.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
diff --git a/tempest/api/identity/admin/v3/test_projects_negative.py b/tempest/api/identity/admin/v3/test_projects_negative.py
index e661f42..c76b9ee 100644
--- a/tempest/api/identity/admin/v3/test_projects_negative.py
+++ b/tempest/api/identity/admin/v3/test_projects_negative.py
@@ -1,4 +1,4 @@
-# Copyright 2013 OpenStack, LLC
+# Copyright 2013 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
diff --git a/tempest/api/identity/v2/test_ec2_credentials.py b/tempest/api/identity/v2/test_ec2_credentials.py
index 3c379f0..8f493aa 100644
--- a/tempest/api/identity/v2/test_ec2_credentials.py
+++ b/tempest/api/identity/v2/test_ec2_credentials.py
@@ -51,7 +51,6 @@
def test_list_ec2_credentials(self):
"""Get the list of user ec2 credentials."""
created_creds = []
- fetched_creds = []
# create first ec2 credentials
creds1 = self.non_admin_users_client.create_user_ec2_credential(
self.creds.user_id,
diff --git a/tempest/api/image/v2/test_images_negative.py b/tempest/api/image/v2/test_images_negative.py
index 14de8fd..f60fb0c 100644
--- a/tempest/api/image/v2/test_images_negative.py
+++ b/tempest/api/image/v2/test_images_negative.py
@@ -29,7 +29,7 @@
** get image with image_id=NULL
** get the deleted image
** delete non-existent image
- ** delete rimage with image_id=NULL
+ ** delete image with image_id=NULL
** delete the deleted image
"""
diff --git a/tempest/api/network/test_networks.py b/tempest/api/network/test_networks.py
index bf80ff5..3825f84 100644
--- a/tempest/api/network/test_networks.py
+++ b/tempest/api/network/test_networks.py
@@ -127,7 +127,7 @@
def _get_allocation_pools_from_gateway(cls, ip_version):
"""Return allocation range for subnet of given gateway"""
gateway = cls._get_gateway_from_tempest_conf(ip_version)
- return [{'start': str(gateway + 2), 'end': str(gateway + 3)}]
+ return [{'start': str(gateway + 2), 'end': str(gateway + 6)}]
def subnet_dict(self, include_keys):
# Return a subnet dict which has include_keys and their corresponding
@@ -559,7 +559,7 @@
# Verifies Subnet GW is set in IPv6
self.assertEqual(subnet1['gateway_ip'], ipv6_gateway)
# Verifies Subnet GW is None in IPv4
- self.assertEqual(subnet2['gateway_ip'], None)
+ self.assertIsNone(subnet2['gateway_ip'])
# Verifies all 2 subnets in the same network
body = self.subnets_client.list_subnets()
subnets = [sub['id'] for sub in body['subnets']
diff --git a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
index be33c94..ccb1b54 100644
--- a/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
+++ b/tempest/api/orchestration/stacks/templates/neutron_basic.yaml
@@ -58,7 +58,7 @@
#!/bin/sh -v
SIGNAL_DATA='{"Status": "SUCCESS", "Reason": "SmokeServerNeutron created", "Data": "Application has completed configuration.", "UniqueId": "00000"}'
- while ! curl --fail -X PUT -H 'Content-Type:' --data-binary "$SIGNAL_DATA" \
+ while ! curl --insecure --fail -X PUT -H 'Content-Type:' --data-binary "$SIGNAL_DATA" \
'wait_handle' ; do sleep 3; done
params:
wait_handle: {get_resource: WaitHandleNeutron}
diff --git a/tempest/api/volume/admin/test_volume_types.py b/tempest/api/volume/admin/test_volume_types.py
index 27f6ccb..8eae085 100644
--- a/tempest/api/volume/admin/test_volume_types.py
+++ b/tempest/api/volume/admin/test_volume_types.py
@@ -88,18 +88,21 @@
# Create/get volume type.
body = {}
name = data_utils.rand_name("volume-type")
+ description = data_utils.rand_name("volume-type-description")
proto = CONF.volume.storage_protocol
vendor = CONF.volume.vendor_name
extra_specs = {"storage_protocol": proto,
"vendor_name": vendor}
- body = self.create_volume_type(
- name=name,
- extra_specs=extra_specs)
+ body = self.create_volume_type(description=description, name=name,
+ extra_specs=extra_specs)
self.assertIn('id', body)
self.assertIn('name', body)
- self.assertEqual(body['name'], name,
+ self.assertEqual(name, body['name'],
"The created volume_type name is not equal "
"to the requested name")
+ self.assertEqual(description, body['description'],
+ "The created volume_type_description name is "
+ "not equal to the requested name")
self.assertTrue(body['id'] is not None,
"Field volume_type id is empty or not found.")
fetched_volume_type = self.admin_volume_types_client.show_volume_type(
diff --git a/tempest/api/volume/admin/test_volumes_backup.py b/tempest/api/volume/admin/test_volumes_backup.py
index b6dc488..b144c7c 100644
--- a/tempest/api/volume/admin/test_volumes_backup.py
+++ b/tempest/api/volume/admin/test_volumes_backup.py
@@ -166,6 +166,23 @@
self.admin_backups_client.wait_for_backup_status(import_backup['id'],
'available')
+ @test.idempotent_id('47a35425-a891-4e13-961c-c45deea21e94')
+ def test_volume_backup_reset_status(self):
+ # Create a backup
+ backup_name = data_utils.rand_name('Backup')
+ backup = self.admin_backups_client.create_backup(
+ volume_id=self.volume['id'], name=backup_name)['backup']
+ self.addCleanup(self.admin_backups_client.delete_backup,
+ backup['id'])
+ self.assertEqual(backup_name, backup['name'])
+ self.admin_backups_client.wait_for_backup_status(backup['id'],
+ 'available')
+ # Reset backup status to error
+ self.admin_backups_client.reset_backup_status(backup_id=backup['id'],
+ status="error")
+ self.admin_backups_client.wait_for_backup_status(backup['id'],
+ 'error')
+
class VolumesBackupsAdminV1Test(VolumesBackupsAdminV2Test):
_api_version = 1
diff --git a/tempest/api/volume/v2/test_volumes_list.py b/tempest/api/volume/v2/test_volumes_list.py
index 5117e6c..1fdcb49 100644
--- a/tempest/api/volume/v2/test_volumes_list.py
+++ b/tempest/api/volume/v2/test_volumes_list.py
@@ -174,9 +174,9 @@
# 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)
+ self.assertEqual([], remaining or [],
+ 'No more pages reported, but still '
+ 'missing ids %s' % remaining)
break
@test.idempotent_id('e9138a2c-f67b-4796-8efa-635c196d01de')
diff --git a/tempest/clients.py b/tempest/clients.py
index fd010f2..e070637 100644
--- a/tempest/clients.py
+++ b/tempest/clients.py
@@ -22,9 +22,6 @@
from tempest import exceptions
from tempest.lib import auth
from tempest.lib import exceptions as lib_exc
-from tempest.lib.services import compute
-from tempest.lib.services import image
-from tempest.lib.services import network
from tempest import service_clients
from tempest.services import baremetal
from tempest.services import data_processing
@@ -127,133 +124,84 @@
return configuration
def _set_network_clients(self):
- params = self.parameters['network']
- self.network_agents_client = network.AgentsClient(
- self.auth_provider, **params)
- self.network_extensions_client = network.ExtensionsClient(
- self.auth_provider, **params)
- self.networks_client = network.NetworksClient(
- self.auth_provider, **params)
- self.subnetpools_client = network.SubnetpoolsClient(
- self.auth_provider, **params)
- self.subnets_client = network.SubnetsClient(
- self.auth_provider, **params)
- self.ports_client = network.PortsClient(
- self.auth_provider, **params)
- self.network_quotas_client = network.QuotasClient(
- self.auth_provider, **params)
- self.floating_ips_client = network.FloatingIPsClient(
- self.auth_provider, **params)
- self.metering_labels_client = network.MeteringLabelsClient(
- self.auth_provider, **params)
- self.metering_label_rules_client = network.MeteringLabelRulesClient(
- self.auth_provider, **params)
- self.routers_client = network.RoutersClient(
- self.auth_provider, **params)
- self.security_group_rules_client = network.SecurityGroupRulesClient(
- self.auth_provider, **params)
- self.security_groups_client = network.SecurityGroupsClient(
- self.auth_provider, **params)
- self.network_versions_client = network.NetworkVersionsClient(
- self.auth_provider, **params)
+ self.network_agents_client = self.network.AgentsClient()
+ self.network_extensions_client = self.network.ExtensionsClient()
+ self.networks_client = self.network.NetworksClient()
+ self.subnetpools_client = self.network.SubnetpoolsClient()
+ self.subnets_client = self.network.SubnetsClient()
+ self.ports_client = self.network.PortsClient()
+ self.network_quotas_client = self.network.QuotasClient()
+ self.floating_ips_client = self.network.FloatingIPsClient()
+ self.metering_labels_client = self.network.MeteringLabelsClient()
+ self.metering_label_rules_client = (
+ self.network.MeteringLabelRulesClient())
+ self.routers_client = self.network.RoutersClient()
+ self.security_group_rules_client = (
+ self.network.SecurityGroupRulesClient())
+ self.security_groups_client = self.network.SecurityGroupsClient()
+ self.network_versions_client = self.network.NetworkVersionsClient()
def _set_image_clients(self):
if CONF.service_available.glance:
- params = self.parameters['image']
- self.image_client = image.v1.ImagesClient(
- self.auth_provider, **params)
- self.image_member_client = image.v1.ImageMembersClient(
- self.auth_provider, **params)
-
- self.image_client_v2 = image.v2.ImagesClient(
- self.auth_provider, **params)
- self.image_member_client_v2 = image.v2.ImageMembersClient(
- self.auth_provider, **params)
- self.namespaces_client = image.v2.NamespacesClient(
- self.auth_provider, **params)
- self.resource_types_client = image.v2.ResourceTypesClient(
- self.auth_provider, **params)
- self.schemas_client = image.v2.SchemasClient(
- self.auth_provider, **params)
+ self.image_client = self.image_v1.ImagesClient()
+ self.image_member_client = self.image_v1.ImageMembersClient()
+ self.image_client_v2 = self.image_v2.ImagesClient()
+ self.image_member_client_v2 = self.image_v2.ImageMembersClient()
+ self.namespaces_client = self.image_v2.NamespacesClient()
+ self.resource_types_client = self.image_v2.ResourceTypesClient()
+ self.schemas_client = self.image_v2.SchemasClient()
def _set_compute_clients(self):
- params = self.parameters['compute']
-
- self.agents_client = compute.AgentsClient(self.auth_provider, **params)
- self.compute_networks_client = compute.NetworksClient(
- self.auth_provider, **params)
- self.migrations_client = compute.MigrationsClient(self.auth_provider,
- **params)
+ self.agents_client = self.compute.AgentsClient()
+ self.compute_networks_client = self.compute.NetworksClient()
+ self.migrations_client = self.compute.MigrationsClient()
self.security_group_default_rules_client = (
- compute.SecurityGroupDefaultRulesClient(self.auth_provider,
- **params))
- self.certificates_client = compute.CertificatesClient(
- self.auth_provider, **params)
- self.servers_client = compute.ServersClient(
- self.auth_provider,
- enable_instance_password=CONF.compute_feature_enabled
- .enable_instance_password,
- **params)
- self.server_groups_client = compute.ServerGroupsClient(
- self.auth_provider, **params)
- self.limits_client = compute.LimitsClient(self.auth_provider, **params)
- self.compute_images_client = compute.ImagesClient(self.auth_provider,
- **params)
- self.keypairs_client = compute.KeyPairsClient(self.auth_provider,
- **params)
- self.quotas_client = compute.QuotasClient(self.auth_provider, **params)
- self.quota_classes_client = compute.QuotaClassesClient(
- self.auth_provider, **params)
- self.flavors_client = compute.FlavorsClient(self.auth_provider,
- **params)
- self.extensions_client = compute.ExtensionsClient(self.auth_provider,
- **params)
- self.floating_ip_pools_client = compute.FloatingIPPoolsClient(
- self.auth_provider, **params)
- self.floating_ips_bulk_client = compute.FloatingIPsBulkClient(
- self.auth_provider, **params)
- self.compute_floating_ips_client = compute.FloatingIPsClient(
- self.auth_provider, **params)
+ self.compute.SecurityGroupDefaultRulesClient())
+ self.certificates_client = self.compute.CertificatesClient()
+ eip = CONF.compute_feature_enabled.enable_instance_password
+ self.servers_client = self.compute.ServersClient(
+ enable_instance_password=eip)
+ self.server_groups_client = self.compute.ServerGroupsClient()
+ self.limits_client = self.compute.LimitsClient()
+ self.compute_images_client = self.compute.ImagesClient()
+ self.keypairs_client = self.compute.KeyPairsClient()
+ self.quotas_client = self.compute.QuotasClient()
+ self.quota_classes_client = self.compute.QuotaClassesClient()
+ self.flavors_client = self.compute.FlavorsClient()
+ self.extensions_client = self.compute.ExtensionsClient()
+ self.floating_ip_pools_client = self.compute.FloatingIPPoolsClient()
+ self.floating_ips_bulk_client = self.compute.FloatingIPsBulkClient()
+ self.compute_floating_ips_client = self.compute.FloatingIPsClient()
self.compute_security_group_rules_client = (
- compute.SecurityGroupRulesClient(self.auth_provider, **params))
- self.compute_security_groups_client = compute.SecurityGroupsClient(
- self.auth_provider, **params)
- self.interfaces_client = compute.InterfacesClient(self.auth_provider,
- **params)
- self.fixed_ips_client = compute.FixedIPsClient(self.auth_provider,
- **params)
- self.availability_zone_client = compute.AvailabilityZoneClient(
- self.auth_provider, **params)
- self.aggregates_client = compute.AggregatesClient(self.auth_provider,
- **params)
- self.services_client = compute.ServicesClient(self.auth_provider,
- **params)
- self.tenant_usages_client = compute.TenantUsagesClient(
- self.auth_provider, **params)
- self.hosts_client = compute.HostsClient(self.auth_provider, **params)
- self.hypervisor_client = compute.HypervisorClient(self.auth_provider,
- **params)
+ self.compute.SecurityGroupRulesClient())
+ self.compute_security_groups_client = (
+ self.compute.SecurityGroupsClient())
+ self.interfaces_client = self.compute.InterfacesClient()
+ self.fixed_ips_client = self.compute.FixedIPsClient()
+ self.availability_zone_client = self.compute.AvailabilityZoneClient()
+ self.aggregates_client = self.compute.AggregatesClient()
+ self.services_client = self.compute.ServicesClient()
+ self.tenant_usages_client = self.compute.TenantUsagesClient()
+ self.hosts_client = self.compute.HostsClient()
+ self.hypervisor_client = self.compute.HypervisorClient()
self.instance_usages_audit_log_client = (
- compute.InstanceUsagesAuditLogClient(self.auth_provider, **params))
- self.tenant_networks_client = compute.TenantNetworksClient(
- self.auth_provider, **params)
- self.baremetal_nodes_client = compute.BaremetalNodesClient(
- self.auth_provider, **params)
+ self.compute.InstanceUsagesAuditLogClient())
+ self.tenant_networks_client = self.compute.TenantNetworksClient()
+ self.baremetal_nodes_client = self.compute.BaremetalNodesClient()
# NOTE: The following client needs special timeout values because
# the API is a proxy for the other component.
- params_volume = copy.deepcopy(params)
- # Optional parameters
+ params_volume = {}
for _key in ('build_interval', 'build_timeout'):
_value = self.parameters['volume'].get(_key)
if _value:
params_volume[_key] = _value
- self.volumes_extensions_client = compute.VolumesClient(
- self.auth_provider, **params_volume)
- self.compute_versions_client = compute.VersionsClient(
- self.auth_provider, **params_volume)
- self.snapshots_extensions_client = compute.SnapshotsClient(
- self.auth_provider, **params_volume)
+ self.volumes_extensions_client = self.compute.VolumesClient(
+ **params_volume)
+ self.compute_versions_client = self.compute.VersionsClient(
+ **params_volume)
+ self.snapshots_extensions_client = self.compute.SnapshotsClient(
+ **params_volume)
def _set_identity_clients(self):
params = self.parameters['identity']
diff --git a/tempest/cmd/subunit_describe_calls.py b/tempest/cmd/subunit_describe_calls.py
index 9391823..0f868a9 100644
--- a/tempest/cmd/subunit_describe_calls.py
+++ b/tempest/cmd/subunit_describe_calls.py
@@ -21,13 +21,14 @@
Runtime Arguments
-----------------
-**--subunit, -s**: (Required) The path to the subunit file being parsed
+**--subunit, -s**: (Optional) The path to the subunit file being parsed,
+defaults to stdin
**--non-subunit-name, -n**: (Optional) The file_name that the logs are being
stored in
-**--output-file, -o**: (Required) The path where the JSON output will be
-written to
+**--output-file, -o**: (Optional) The path where the JSON output will be
+written to. This contains more information than is present in stdout.
**--ports, -p**: (Optional) The path to a JSON file describing the ports being
used by different services
@@ -35,13 +36,14 @@
Usage
-----
-subunit-describe-calls will take in a file path via the --subunit parameter
-which contains either a subunit v1 or v2 stream. This is then parsed checking
-for details contained in the file_bytes of the --non-subunit-name parameter
-(the default is pythonlogging which is what Tempest uses to store logs). By
-default the OpenStack Kilo release port defaults (http://bit.ly/22jpF5P)
-are used unless a file is provided via the --ports option. The resulting output
-is dumped in JSON output to the path provided in the --output-file option.
+subunit-describe-calls will take in either stdin subunit v1 or v2 stream or a
+file path which contains either a subunit v1 or v2 stream passed via the
+--subunit parameter. This is then parsed checking for details contained in the
+file_bytes of the --non-subunit-name parameter (the default is pythonlogging
+which is what Tempest uses to store logs). By default the OpenStack Kilo
+release port defaults (http://bit.ly/22jpF5P) are used unless a file is
+provided via the --ports option. The resulting output is dumped in JSON output
+to the path provided in the --output-file option.
Ports file JSON structure
^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -64,7 +66,11 @@
"verb": "HTTP Verb",
"service": "Name of the service",
"url": "A shortened version of the URL called",
- "status_code": "The status code of the response"
+ "status_code": "The status code of the response",
+ "request_headers": "The headers of the request",
+ "request_body": "The body of the request",
+ "response_headers": "The headers of the response",
+ "response_body": "The body of the response"
}
]
}
@@ -75,6 +81,7 @@
import json
import os
import re
+import sys
import subunit
import testtools
@@ -91,6 +98,9 @@
'(?P<verb>\w*) (?P<url>.*) .*')
port_re = re.compile(r'.*:(?P<port>\d+).*')
path_re = re.compile(r'http[s]?://[^/]*/(?P<path>.*)')
+ request_re = re.compile(r'.* Request - Headers: (?P<headers>.*)')
+ response_re = re.compile(r'.* Response - Headers: (?P<headers>.*)')
+ body_re = re.compile(r'.*Body: (?P<body>.*)')
# Based on mitaka defaults:
# http://docs.openstack.org/mitaka/config-reference/
@@ -151,15 +161,46 @@
calls = []
for _, detail in details.items():
+ in_request = False
+ in_response = False
+ current_call = {}
for line in detail.as_text().split("\n"):
- match = self.url_re.match(line)
- if match is not None:
- calls.append({
- "name": match.group("name"),
- "verb": match.group("verb"),
- "status_code": match.group("code"),
- "service": self.get_service(match.group("url")),
- "url": self.url_path(match.group("url"))})
+ url_match = self.url_re.match(line)
+ request_match = self.request_re.match(line)
+ response_match = self.response_re.match(line)
+ body_match = self.body_re.match(line)
+
+ if url_match is not None:
+ if current_call != {}:
+ calls.append(current_call.copy())
+ current_call = {}
+ in_request, in_response = False, False
+ current_call.update({
+ "name": url_match.group("name"),
+ "verb": url_match.group("verb"),
+ "status_code": url_match.group("code"),
+ "service": self.get_service(url_match.group("url")),
+ "url": self.url_path(url_match.group("url"))})
+ elif request_match is not None:
+ in_request, in_response = True, False
+ current_call.update(
+ {"request_headers": request_match.group("headers")})
+ elif in_request and body_match is not None:
+ in_request = False
+ current_call.update(
+ {"request_body": body_match.group(
+ "body")})
+ elif response_match is not None:
+ in_request, in_response = False, True
+ current_call.update(
+ {"response_headers": response_match.group(
+ "headers")})
+ elif in_response and body_match is not None:
+ in_response = False
+ current_call.update(
+ {"response_body": body_match.group("body")})
+ if current_call != {}:
+ calls.append(current_call.copy())
return calls
@@ -206,8 +247,9 @@
self.prog = "subunit-describe-calls"
self.add_argument(
- "-s", "--subunit", metavar="<subunit file>", required=True,
- default=None, help="The path to the subunit output file.")
+ "-s", "--subunit", metavar="<subunit file>",
+ nargs="?", type=argparse.FileType('rb'), default=sys.stdin,
+ help="The path to the subunit output file.")
self.add_argument(
"-n", "--non-subunit-name", metavar="<non subunit name>",
@@ -216,19 +258,18 @@
self.add_argument(
"-o", "--output-file", metavar="<output file>", default=None,
- help="The output file name for the json.", required=True)
+ help="The output file name for the json.")
self.add_argument(
"-p", "--ports", metavar="<ports file>", default=None,
help="A JSON file describing the ports for each service.")
-def parse(subunit_file, non_subunit_name, ports):
+def parse(stream, non_subunit_name, ports):
if ports is not None and os.path.exists(ports):
ports = json.loads(open(ports).read())
url_parser = UrlParser(ports)
- stream = open(subunit_file, 'rb')
suite = subunit.ByteStreamToStreamResult(
stream, non_subunit_name=non_subunit_name)
result = testtools.StreamToExtendedDecorator(url_parser)
@@ -248,8 +289,21 @@
def output(url_parser, output_file):
- with open(output_file, "w") as outfile:
- outfile.write(json.dumps(url_parser.test_logs))
+ if output_file is not None:
+ with open(output_file, "w") as outfile:
+ outfile.write(json.dumps(url_parser.test_logs))
+ return
+
+ for test_name, items in url_parser.test_logs.iteritems():
+ sys.stdout.write('{0}\n'.format(test_name))
+ if not items:
+ sys.stdout.write('\n')
+ continue
+ for item in items:
+ sys.stdout.write('\t- {0} {1} request for {2} to {3}\n'.format(
+ item.get('status_code'), item.get('verb'),
+ item.get('service'), item.get('url')))
+ sys.stdout.write('\n')
def entry_point():
diff --git a/tempest/common/waiters.py b/tempest/common/waiters.py
index df08e30..073481c 100644
--- a/tempest/common/waiters.py
+++ b/tempest/common/waiters.py
@@ -141,7 +141,7 @@
while int(time.time()) - start < client.build_timeout:
image = show_image(image_id)
# Compute image client returns response wrapped in 'image' element
- # which is not case with Glance image client.
+ # which is not the case with Glance image client.
if 'image' in image:
image = image['image']
diff --git a/tempest/config.py b/tempest/config.py
index 0c2b913..6bae021 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -173,6 +173,16 @@
"a domain scoped token to use admin APIs")
]
+service_clients_group = cfg.OptGroup(name='service-clients',
+ title="Service Clients Options")
+
+ServiceClientsGroup = [
+ cfg.IntOpt('http_timeout',
+ default=60,
+ help='Timeout in seconds to wait for the http request to '
+ 'return'),
+]
+
identity_feature_group = cfg.OptGroup(name='identity-feature-enabled',
title='Enabled Identity Features')
@@ -1119,6 +1129,7 @@
(compute_group, ComputeGroup),
(compute_features_group, ComputeFeaturesGroup),
(identity_group, IdentityGroup),
+ (service_clients_group, ServiceClientsGroup),
(identity_feature_group, IdentityFeatureGroup),
(image_group, ImageGroup),
(image_feature_group, ImageFeaturesGroup),
@@ -1184,6 +1195,7 @@
self.compute = _CONF.compute
self.compute_feature_enabled = _CONF['compute-feature-enabled']
self.identity = _CONF.identity
+ self.service_clients = _CONF['service-clients']
self.identity_feature_enabled = _CONF['identity-feature-enabled']
self.image = _CONF.image
self.image_feature_enabled = _CONF['image-feature-enabled']
@@ -1372,6 +1384,7 @@
* `disable_ssl_certificate_validation`
* `ca_certs`
* `trace_requests`
+ * `http_timeout`
The dict returned by this does not fit a few service clients:
@@ -1393,7 +1406,8 @@
'disable_ssl_certificate_validation':
CONF.identity.disable_ssl_certificate_validation,
'ca_certs': CONF.identity.ca_certificates_file,
- 'trace_requests': CONF.debug.trace_requests
+ 'trace_requests': CONF.debug.trace_requests,
+ 'http_timeout': CONF.service_clients.http_timeout
}
if service_client_name is None:
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 67faad5..272f6e3 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -63,7 +63,8 @@
class ServerUnreachable(exceptions.TempestException):
- message = "The server is not reachable via the configured network"
+ message = ("Server %(server_id)s is not reachable via "
+ "the configured network")
# NOTE(andreaf) This exception is added here to facilitate the migration
diff --git a/tempest/lib/auth.py b/tempest/lib/auth.py
index 54a7002..1857a43 100644
--- a/tempest/lib/auth.py
+++ b/tempest/lib/auth.py
@@ -260,11 +260,13 @@
def __init__(self, credentials, auth_url,
disable_ssl_certificate_validation=None,
- ca_certs=None, trace_requests=None, scope='project'):
+ ca_certs=None, trace_requests=None, scope='project',
+ http_timeout=None):
super(KeystoneAuthProvider, self).__init__(credentials, scope)
self.dscv = disable_ssl_certificate_validation
self.ca_certs = ca_certs
self.trace_requests = trace_requests
+ self.http_timeout = http_timeout
self.auth_url = auth_url
self.auth_client = self._auth_client(auth_url)
@@ -342,7 +344,8 @@
def _auth_client(self, auth_url):
return json_v2id.TokenClient(
auth_url, disable_ssl_certificate_validation=self.dscv,
- ca_certs=self.ca_certs, trace_requests=self.trace_requests)
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ http_timeout=self.http_timeout)
def _auth_params(self):
"""Auth parameters to be passed to the token request
@@ -429,7 +432,8 @@
def _auth_client(self, auth_url):
return json_v3id.V3TokenClient(
auth_url, disable_ssl_certificate_validation=self.dscv,
- ca_certs=self.ca_certs, trace_requests=self.trace_requests)
+ ca_certs=self.ca_certs, trace_requests=self.trace_requests,
+ http_timeout=self.http_timeout)
def _auth_params(self):
"""Auth parameters to be passed to the token request
@@ -595,7 +599,7 @@
def get_credentials(auth_url, fill_in=True, identity_version='v2',
disable_ssl_certificate_validation=None, ca_certs=None,
- trace_requests=None, **kwargs):
+ trace_requests=None, http_timeout=None, **kwargs):
"""Builds a credentials object based on the configured auth_version
:param auth_url (string): Full URI of the OpenStack Identity API(Keystone)
@@ -611,6 +615,8 @@
:param ca_certs: CA certificate bundle for validation of certificates
in SSL API requests to the auth system
:param trace_requests: trace in log API requests to the auth system
+ :param http_timeout: timeout in seconds to wait for the http request to
+ return
:param kwargs (dict): Dict of credential key/value pairs
Examples:
@@ -634,7 +640,8 @@
dscv = disable_ssl_certificate_validation
auth_provider = auth_provider_class(
creds, auth_url, disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs, trace_requests=trace_requests)
+ ca_certs=ca_certs, trace_requests=trace_requests,
+ http_timeout=http_timeout)
creds = auth_provider.fill_credentials()
return creds
diff --git a/tempest/lib/common/http.py b/tempest/lib/common/http.py
index dffc5f9..86ea26e 100644
--- a/tempest/lib/common/http.py
+++ b/tempest/lib/common/http.py
@@ -18,7 +18,7 @@
class ClosingHttp(urllib3.poolmanager.PoolManager):
def __init__(self, disable_ssl_certificate_validation=False,
- ca_certs=None):
+ ca_certs=None, timeout=None):
kwargs = {}
if disable_ssl_certificate_validation:
@@ -29,6 +29,9 @@
kwargs['cert_reqs'] = 'CERT_REQUIRED'
kwargs['ca_certs'] = ca_certs
+ if timeout:
+ kwargs['timeout'] = timeout
+
super(ClosingHttp, self).__init__(**kwargs)
def request(self, url, method, *args, **kwargs):
diff --git a/tempest/lib/common/rest_client.py b/tempest/lib/common/rest_client.py
index 1b0f53a..7e1a442 100644
--- a/tempest/lib/common/rest_client.py
+++ b/tempest/lib/common/rest_client.py
@@ -66,6 +66,8 @@
TLS server cert
:param str trace_request: Regex to use for specifying logging the entirety
of the request and response payload
+ :param str http_timeout: Timeout in seconds to wait for the http request to
+ return
"""
TYPE = "json"
@@ -78,7 +80,7 @@
endpoint_type='publicURL',
build_interval=1, build_timeout=60,
disable_ssl_certificate_validation=False, ca_certs=None,
- trace_requests='', name=None):
+ trace_requests='', name=None, http_timeout=None):
self.auth_provider = auth_provider
self.service = service
self.region = region
@@ -99,7 +101,8 @@
'vary', 'www-authenticate'))
dscv = disable_ssl_certificate_validation
self.http_obj = http.ClosingHttp(
- disable_ssl_certificate_validation=dscv, ca_certs=ca_certs)
+ disable_ssl_certificate_validation=dscv, ca_certs=ca_certs,
+ timeout=http_timeout)
def _get_type(self):
return self.TYPE
diff --git a/tempest/lib/common/utils/data_utils.py b/tempest/lib/common/utils/data_utils.py
index 93382c0..6b6548e 100644
--- a/tempest/lib/common/utils/data_utils.py
+++ b/tempest/lib/common/utils/data_utils.py
@@ -75,7 +75,7 @@
ascii_char = string.ascii_letters
digits = string.digits
digit = random.choice(string.digits)
- puncs = '~!@#$%^&*_=+'
+ puncs = '~!@#%^&*_=+'
punc = random.choice(puncs)
seed = ascii_char + digits + puncs
pre = upper + digit + punc
diff --git a/tempest/lib/exceptions.py b/tempest/lib/exceptions.py
index 5ca78f9..de2d713 100644
--- a/tempest/lib/exceptions.py
+++ b/tempest/lib/exceptions.py
@@ -229,3 +229,13 @@
class UnknownServiceClient(TempestException):
message = "Service clients named %(services)s are not known"
+
+
+class ServiceClientRegistrationException(TempestException):
+ message = ("Error registering module %(name)s in path %(module_path)s, "
+ "with service %(service_version)s and clients "
+ "%(client_names)s: %(detailed_error)s")
+
+
+class PluginRegistrationException(TempestException):
+ message = "Error registering plugin %(name)s: %(detailed_error)s"
diff --git a/tempest/lib/services/clients.py b/tempest/lib/services/clients.py
new file mode 100644
index 0000000..8054e62
--- /dev/null
+++ b/tempest/lib/services/clients.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2016 Hewlett-Packard Enterprise Development Company, L.P.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest.lib.common.utils import misc
+from tempest.lib import exceptions
+
+
+@misc.singleton
+class ClientsRegistry(object):
+ """Registry of all service clients available from plugins"""
+
+ def __init__(self):
+ self._service_clients = {}
+
+ def register_service_client(self, plugin_name, service_client_data):
+ if plugin_name in self._service_clients:
+ detailed_error = 'Clients for plugin %s already registered'
+ raise exceptions.PluginRegistrationException(
+ name=plugin_name,
+ detailed_error=detailed_error % plugin_name)
+ self._service_clients[plugin_name] = service_client_data
+
+ def get_service_clients(self):
+ return self._service_clients
diff --git a/tempest/lib/services/identity/v2/token_client.py b/tempest/lib/services/identity/v2/token_client.py
index 5716027..a5d7c86 100644
--- a/tempest/lib/services/identity/v2/token_client.py
+++ b/tempest/lib/services/identity/v2/token_client.py
@@ -22,11 +22,11 @@
class TokenClient(rest_client.RestClient):
def __init__(self, auth_url, disable_ssl_certificate_validation=None,
- ca_certs=None, trace_requests=None):
+ ca_certs=None, trace_requests=None, **kwargs):
dscv = disable_ssl_certificate_validation
super(TokenClient, self).__init__(
None, None, None, disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs, trace_requests=trace_requests)
+ ca_certs=ca_certs, trace_requests=trace_requests, **kwargs)
if auth_url is None:
raise exceptions.IdentityError("Couldn't determine auth_url")
diff --git a/tempest/lib/services/identity/v3/token_client.py b/tempest/lib/services/identity/v3/token_client.py
index 964d43f..c1f7e7b 100644
--- a/tempest/lib/services/identity/v3/token_client.py
+++ b/tempest/lib/services/identity/v3/token_client.py
@@ -22,11 +22,11 @@
class V3TokenClient(rest_client.RestClient):
def __init__(self, auth_url, disable_ssl_certificate_validation=None,
- ca_certs=None, trace_requests=None):
+ ca_certs=None, trace_requests=None, **kwargs):
dscv = disable_ssl_certificate_validation
super(V3TokenClient, self).__init__(
None, None, None, disable_ssl_certificate_validation=dscv,
- ca_certs=ca_certs, trace_requests=trace_requests)
+ ca_certs=ca_certs, trace_requests=trace_requests, **kwargs)
if auth_url is None:
raise exceptions.IdentityError("Couldn't determine auth_url")
diff --git a/tempest/scenario/manager.py b/tempest/scenario/manager.py
index f889c44..b151375 100644
--- a/tempest/scenario/manager.py
+++ b/tempest/scenario/manager.py
@@ -664,7 +664,7 @@
for address in addresses:
if address['version'] == CONF.validation.ip_version_for_ssh:
return address['addr']
- raise exceptions.ServerUnreachable()
+ raise exceptions.ServerUnreachable(server_id=server['id'])
else:
raise exceptions.InvalidConfiguration()
diff --git a/tempest/service_clients.py b/tempest/service_clients.py
index 386e621..a5bc86e 100644
--- a/tempest/service_clients.py
+++ b/tempest/service_clients.py
@@ -14,8 +14,26 @@
# License for the specific language governing permissions and limitations
# under the License.
+import copy
+import importlib
+import inspect
+import logging
+
from tempest.lib import auth
from tempest.lib import exceptions
+from tempest.lib.services import clients
+from tempest.lib.services import compute
+from tempest.lib.services import image
+from tempest.lib.services import network
+
+LOG = logging.getLogger(__name__)
+
+client_modules_by_service_name = {
+ 'compute': compute,
+ 'image.v1': image.v1,
+ 'image.v2': image.v2,
+ 'network': network
+}
def tempest_modules():
@@ -29,9 +47,129 @@
def available_modules():
- """List of service client modules available in Tempest and plugins"""
- # TODO(andreaf) For now this returns only tempest_modules
- return tempest_modules()
+ """List of service client modules available in Tempest and plugins
+
+ The list of available modules can be used for automatic configuration.
+
+ :raise PluginRegistrationException: if a plugin exposes a service_version
+ already defined by Tempest or another plugin.
+
+ Examples:
+
+ >>> from tempest import config
+ >>> params = {}
+ >>> for service_version in available_modules():
+ >>> service = service_version.split('.')[0]
+ >>> params[service] = config.service_client_config(service)
+ >>> service_clients = ServiceClients(creds, identity_uri,
+ >>> client_parameters=params)
+ """
+ extra_service_versions = set([])
+ plugin_services = clients.ClientsRegistry().get_service_clients()
+ for plugin_name in plugin_services:
+ plug_service_versions = set([x['service_version'] for x in
+ plugin_services[plugin_name]])
+ # If a plugin exposes a duplicate service_version raise an exception
+ if plug_service_versions:
+ if not plug_service_versions.isdisjoint(extra_service_versions):
+ detailed_error = (
+ 'Plugin %s is trying to register a service %s already '
+ 'claimed by another one' % (plugin_name,
+ extra_service_versions &
+ plug_service_versions))
+ raise exceptions.PluginRegistrationException(
+ name=plugin_name, detailed_error=detailed_error)
+ if not plug_service_versions.isdisjoint(tempest_modules()):
+ detailed_error = (
+ 'Plugin %s is trying to register a service %s already '
+ 'claimed by a Tempest one' % (plugin_name,
+ tempest_modules() &
+ plug_service_versions))
+ raise exceptions.PluginRegistrationException(
+ name=plugin_name, detailed_error=detailed_error)
+ extra_service_versions |= plug_service_versions
+ return tempest_modules() | extra_service_versions
+
+
+class ClientsFactory(object):
+ """Builds service clients for a service client module
+
+ This class implements the logic of feeding service client parameters
+ to service clients from a specific module. It allows setting the
+ parameters once and obtaining new instances of the clients without the
+ need of passing any parameter.
+
+ ClientsFactory can be used directly, or consumed via the `ServiceClients`
+ class, which manages the authorization part.
+ """
+
+ def __init__(self, module_path, client_names, auth_provider, **kwargs):
+ """Initialises the client factory
+
+ :param module_path: Path to module that includes all service clients.
+ All service client classes must be exposed by a single module.
+ If they are separated in different modules, defining __all__
+ in the root module can help, similar to what is done by service
+ clients in tempest.
+ :param client_names: List or set of names of the service client
+ classes.
+ :param auth_provider: The auth provider used to initialise client.
+ :param kwargs: Parameters to be passed to all clients. Parameters
+ values can be overwritten when clients are initialised, but
+ parameters cannot be deleted.
+ :raise ImportError if the specified module_path cannot be imported
+
+ Example:
+
+ >>> # Get credentials and an auth_provider
+ >>> clients = ClientsFactory(
+ >>> module_path='my_service.my_service_clients',
+ >>> client_names=['ServiceClient1', 'ServiceClient2'],
+ >>> auth_provider=auth_provider,
+ >>> service='my_service',
+ >>> region='region1')
+ >>> my_api_client = clients.MyApiClient()
+ >>> my_api_client_region2 = clients.MyApiClient(region='region2')
+
+ """
+ # Import the module. If it's not importable, the raised exception
+ # provides good enough information about what happened
+ _module = importlib.import_module(module_path)
+ # If any of the classes is not in the module we fail
+ for class_name in client_names:
+ # TODO(andreaf) This always passes all parameters to all clients.
+ # In future to allow clients to specify the list of parameters
+ # that they accept based out of a list of standard ones.
+
+ # Obtain the class
+ klass = self._get_class(_module, class_name)
+ final_kwargs = copy.copy(kwargs)
+
+ # Set the function as an attribute of the factory
+ setattr(self, class_name, self._get_partial_class(
+ klass, auth_provider, final_kwargs))
+
+ @classmethod
+ def _get_partial_class(cls, klass, auth_provider, kwargs):
+
+ # Define a function that returns a new class instance by
+ # combining default kwargs with extra ones
+ def partial_class(**later_kwargs):
+ kwargs.update(later_kwargs)
+ return klass(auth_provider=auth_provider, **kwargs)
+
+ return partial_class
+
+ @classmethod
+ def _get_class(cls, module, class_name):
+ klass = getattr(module, class_name, None)
+ if not klass:
+ msg = 'Invalid class name, %s is not found in %s'
+ raise AttributeError(msg % (class_name, module))
+ if not inspect.isclass(klass):
+ msg = 'Expected a class, got %s of type %s instead'
+ raise TypeError(msg % (klass, type(klass)))
+ return klass
class ServiceClients(object):
@@ -101,11 +239,11 @@
mandatory parameter, and it will so soon.
:param region: Default value of region for service clients.
:param scope: default scope for tokens produced by the auth provider
- :param disable_ssl_certificate_validation Applies to auth and to all
+ :param disable_ssl_certificate_validation: Applies to auth and to all
service clients.
- :param ca_certs Applies to auth and to all service clients.
- :param trace_requests Applies to auth and to all service clients.
- :param client_parameters Dictionary with parameters for service
+ :param ca_certs: Applies to auth and to all service clients.
+ :param trace_requests: Applies to auth and to all service clients.
+ :param client_parameters: Dictionary with parameters for service
clients. Keys of the dictionary are the service client service
name, as declared in `service_clients.available_modules()` except
for the version. Values are dictionaries of parameters that are
@@ -120,6 +258,7 @@
>>> client_parameters['service_y'] = params_service_y
"""
+ self._registered_services = set([])
self.credentials = credentials
self.identity_uri = identity_uri
if not identity_uri:
@@ -161,6 +300,94 @@
raise exceptions.UnknownServiceClient(
services=list(client_parameters.keys()))
+ # Register service clients owned by tempest
+ for service in tempest_modules():
+ if service in list(client_modules_by_service_name):
+ attribute = service.replace('.', '_')
+ configs = service.split('.')[0]
+ module = client_modules_by_service_name[service]
+ self.register_service_client_module(
+ attribute, service, module.__name__,
+ module.__all__, **self.parameters[configs])
+
+ # Register service clients from plugins
+ clients_registry = clients.ClientsRegistry()
+ plugin_service_clients = clients_registry.get_service_clients()
+ for plugin in plugin_service_clients:
+ service_clients = plugin_service_clients[plugin]
+ # Each plugin returns a list of service client parameters
+ for service_client in service_clients:
+ # NOTE(andreaf) If a plugin cannot register, stop the
+ # registration process, log some details to help
+ # troubleshooting, and re-raise
+ try:
+ self.register_service_client_module(**service_client)
+ except Exception:
+ LOG.exception(
+ 'Failed to register service client from plugin %s '
+ 'with parameters %s' % (plugin, service_client))
+ raise
+
+ def register_service_client_module(self, name, service_version,
+ module_path, client_names, **kwargs):
+ """Register a service client module
+
+ Initiates a client factory for the specified module, using this
+ class auth_provider, and accessible via a `name` attribute in the
+ service client.
+
+ :param name: Name used to access the client
+ :param service_version: Name of the service complete with version.
+ Used to track registered services. When a plugin implements it,
+ it can be used by other plugins to obtain their configuration.
+ :param module_path: Path to module that includes all service clients.
+ All service client classes must be exposed by a single module.
+ If they are separated in different modules, defining __all__
+ in the root module can help, similar to what is done by service
+ clients in tempest.
+ :param client_names: List or set of names of service client classes.
+ :param kwargs: Extra optional parameters to be passed to all clients.
+ ServiceClient provides defaults for region, dscv, ca_certs and
+ trace_requests.
+ :raise ServiceClientRegistrationException: if the provided name is
+ already in use or if service_version is already registered.
+ :raise ImportError: if module_path cannot be imported.
+ """
+ if hasattr(self, name):
+ using_name = getattr(self, name)
+ detailed_error = 'Module name already in use: %s' % using_name
+ raise exceptions.ServiceClientRegistrationException(
+ name=name, service_version=service_version,
+ module_path=module_path, client_names=client_names,
+ detailed_error=detailed_error)
+ if service_version in self.registered_services:
+ detailed_error = 'Service %s already registered.' % service_version
+ raise exceptions.ServiceClientRegistrationException(
+ name=name, service_version=service_version,
+ module_path=module_path, client_names=client_names,
+ detailed_error=detailed_error)
+ params = dict(region=self.region,
+ disable_ssl_certificate_validation=self.dscv,
+ ca_certs=self.ca_certs,
+ trace_requests=self.trace_requests)
+ params.update(kwargs)
+ # Instantiate the client factory
+ _factory = ClientsFactory(module_path=module_path,
+ client_names=client_names,
+ auth_provider=self.auth_provider,
+ **params)
+ # Adds the client factory to the service_client
+ setattr(self, name, _factory)
+ # Add the name of the new service in self.SERVICES for discovery
+ self._registered_services.add(service_version)
+
+ @property
+ def registered_services(self):
+ # TODO(andreaf) Temporary set needed until all services are migrated
+ _non_migrated_services = tempest_modules() - set(
+ client_modules_by_service_name)
+ return self._registered_services | _non_migrated_services
+
def _setup_parameters(self, parameters):
"""Setup default values for client parameters
diff --git a/tempest/services/volume/base/admin/base_hosts_client.py b/tempest/services/volume/base/admin/base_hosts_client.py
deleted file mode 100644
index 382e9a8..0000000
--- a/tempest/services/volume/base/admin/base_hosts_client.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# Copyright 2013 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class BaseHostsClient(rest_client.RestClient):
- """Client class to send CRUD Volume Hosts API requests"""
-
- def list_hosts(self, **params):
- """Lists all hosts."""
-
- url = 'os-hosts'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/admin/base_services_client.py b/tempest/services/volume/base/admin/base_services_client.py
deleted file mode 100644
index 861eb92..0000000
--- a/tempest/services/volume/base/admin/base_services_client.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright 2014 NEC Corporation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from oslo_serialization import jsonutils as json
-from six.moves.urllib import parse as urllib
-
-from tempest.lib.common import rest_client
-
-
-class BaseServicesClient(rest_client.RestClient):
-
- def list_services(self, **params):
- url = 'os-services'
- if params:
- url += '?%s' % urllib.urlencode(params)
-
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_availability_zone_client.py b/tempest/services/volume/base/base_availability_zone_client.py
deleted file mode 100644
index 1c2deba..0000000
--- a/tempest/services/volume/base/base_availability_zone_client.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2014 NEC Corporation.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
-
-
-class BaseAvailabilityZoneClient(rest_client.RestClient):
-
- def list_availability_zones(self):
- resp, body = self.get('os-availability-zone')
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/base/base_backups_client.py b/tempest/services/volume/base/base_backups_client.py
index 3842d66..63c5417 100644
--- a/tempest/services/volume/base/base_backups_client.py
+++ b/tempest/services/volume/base/base_backups_client.py
@@ -89,6 +89,13 @@
self.expected_success(201, resp.status)
return rest_client.ResponseBody(resp, body)
+ def reset_backup_status(self, backup_id, status):
+ """Reset the specified backup's status."""
+ post_body = json.dumps({'os-reset_status': {"status": status}})
+ resp, body = self.post('backups/%s/action' % backup_id, post_body)
+ self.expected_success(202, resp.status)
+ return rest_client.ResponseBody(resp, body)
+
def wait_for_backup_status(self, backup_id, status):
"""Waits for a Backup to reach a given status."""
body = self.show_backup(backup_id)['backup']
@@ -99,7 +106,7 @@
time.sleep(self.build_interval)
body = self.show_backup(backup_id)['backup']
backup_status = body['status']
- if backup_status == 'error':
+ if backup_status == 'error' and backup_status != status:
raise exceptions.VolumeBackupException(backup_id=backup_id)
if int(time.time()) - start >= self.build_timeout:
diff --git a/tempest/services/volume/base/base_extensions_client.py b/tempest/services/volume/base/base_extensions_client.py
deleted file mode 100644
index b90fe94..0000000
--- a/tempest/services/volume/base/base_extensions_client.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Copyright 2012 OpenStack Foundation
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-from oslo_serialization import jsonutils as json
-
-from tempest.lib.common import rest_client
-
-
-class BaseExtensionsClient(rest_client.RestClient):
-
- def list_extensions(self):
- url = 'extensions'
- resp, body = self.get(url)
- body = json.loads(body)
- self.expected_success(200, resp.status)
- return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/admin/hosts_client.py b/tempest/services/volume/v1/json/admin/hosts_client.py
index 3b52968..56ba12c 100644
--- a/tempest/services/volume/v1/json/admin/hosts_client.py
+++ b/tempest/services/volume/v1/json/admin/hosts_client.py
@@ -13,8 +13,23 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base.admin import base_hosts_client
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
-class HostsClient(base_hosts_client.BaseHostsClient):
+class HostsClient(rest_client.RestClient):
"""Client class to send CRUD Volume Host API V1 requests"""
+
+ def list_hosts(self, **params):
+ """Lists all hosts."""
+
+ url = 'os-hosts'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/admin/services_client.py b/tempest/services/volume/v1/json/admin/services_client.py
index 2bffd55..d438a34 100644
--- a/tempest/services/volume/v1/json/admin/services_client.py
+++ b/tempest/services/volume/v1/json/admin/services_client.py
@@ -13,8 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base.admin import base_services_client
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
-class ServicesClient(base_services_client.BaseServicesClient):
+class ServicesClient(rest_client.RestClient):
"""Volume V1 volume services client"""
+
+ def list_services(self, **params):
+ url = 'os-services'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/availability_zone_client.py b/tempest/services/volume/v1/json/availability_zone_client.py
index 3a27027..be4f539 100644
--- a/tempest/services/volume/v1/json/availability_zone_client.py
+++ b/tempest/services/volume/v1/json/availability_zone_client.py
@@ -13,9 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base import base_availability_zone_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
-class AvailabilityZoneClient(
- base_availability_zone_client.BaseAvailabilityZoneClient):
+class AvailabilityZoneClient(rest_client.RestClient):
"""Volume V1 availability zone client."""
+
+ def list_availability_zones(self):
+ resp, body = self.get('os-availability-zone')
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v1/json/extensions_client.py b/tempest/services/volume/v1/json/extensions_client.py
index f99d0f5..7b849a8 100644
--- a/tempest/services/volume/v1/json/extensions_client.py
+++ b/tempest/services/volume/v1/json/extensions_client.py
@@ -13,8 +13,17 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base import base_extensions_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
+class ExtensionsClient(rest_client.RestClient):
"""Volume V1 extensions client."""
+
+ def list_extensions(self):
+ url = 'extensions'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/admin/hosts_client.py b/tempest/services/volume/v2/json/admin/hosts_client.py
index e092c6a..dd7c482 100644
--- a/tempest/services/volume/v2/json/admin/hosts_client.py
+++ b/tempest/services/volume/v2/json/admin/hosts_client.py
@@ -13,9 +13,24 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base.admin import base_hosts_client
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
-class HostsClient(base_hosts_client.BaseHostsClient):
+class HostsClient(rest_client.RestClient):
"""Client class to send CRUD Volume V2 API requests"""
api_version = "v2"
+
+ def list_hosts(self, **params):
+ """Lists all hosts."""
+
+ url = 'os-hosts'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/admin/services_client.py b/tempest/services/volume/v2/json/admin/services_client.py
index db19ba9..bc55469 100644
--- a/tempest/services/volume/v2/json/admin/services_client.py
+++ b/tempest/services/volume/v2/json/admin/services_client.py
@@ -13,9 +13,22 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base.admin import base_services_client
+from oslo_serialization import jsonutils as json
+from six.moves.urllib import parse as urllib
+
+from tempest.lib.common import rest_client
-class ServicesClient(base_services_client.BaseServicesClient):
+class ServicesClient(rest_client.RestClient):
"""Client class to send CRUD Volume V2 API requests"""
api_version = "v2"
+
+ def list_services(self, **params):
+ url = 'os-services'
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/availability_zone_client.py b/tempest/services/volume/v2/json/availability_zone_client.py
index 905ebdc..bb4a357 100644
--- a/tempest/services/volume/v2/json/availability_zone_client.py
+++ b/tempest/services/volume/v2/json/availability_zone_client.py
@@ -13,9 +13,16 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base import base_availability_zone_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
-class AvailabilityZoneClient(
- base_availability_zone_client.BaseAvailabilityZoneClient):
+class AvailabilityZoneClient(rest_client.RestClient):
api_version = "v2"
+
+ def list_availability_zones(self):
+ resp, body = self.get('os-availability-zone')
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/services/volume/v2/json/extensions_client.py b/tempest/services/volume/v2/json/extensions_client.py
index 245906f..09279d5 100644
--- a/tempest/services/volume/v2/json/extensions_client.py
+++ b/tempest/services/volume/v2/json/extensions_client.py
@@ -13,8 +13,18 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest.services.volume.base import base_extensions_client
+from oslo_serialization import jsonutils as json
+
+from tempest.lib.common import rest_client
-class ExtensionsClient(base_extensions_client.BaseExtensionsClient):
+class ExtensionsClient(rest_client.RestClient):
+ """Volume V2 extensions client."""
api_version = "v2"
+
+ def list_extensions(self):
+ url = 'extensions'
+ resp, body = self.get(url)
+ body = json.loads(body)
+ self.expected_success(200, resp.status)
+ return rest_client.ResponseBody(resp, body)
diff --git a/tempest/test_discover/plugins.py b/tempest/test_discover/plugins.py
index d604b28..cfb0c7f 100644
--- a/tempest/test_discover/plugins.py
+++ b/tempest/test_discover/plugins.py
@@ -19,6 +19,7 @@
import stevedore
from tempest.lib.common.utils import misc
+from tempest.lib.services import clients
LOG = logging.getLogger(__name__)
@@ -62,6 +63,54 @@
"""
return []
+ def get_service_clients(self):
+ """Get a list of the service clients for registration
+
+ If the plugin implements service clients for one or more APIs, it
+ may return their details by this method for automatic registration
+ in any ServiceClients object instantiated by tests.
+ The default implementation returns an empty list.
+
+ :return list of dictionaries. Each element of the list represents
+ the service client for an API. Each dict must define all
+ parameters required for the invocation of
+ `service_clients.ServiceClients.register_service_client_module`.
+ :rtype: list
+
+ Example:
+
+ >>> # Example implementation with one service client
+ >>> myservice_config = config.service_client_config('myservice')
+ >>> params = {
+ >>> 'name': 'myservice',
+ >>> 'service_version': 'myservice',
+ >>> 'module_path': 'myservice_tempest_tests.services',
+ >>> 'client_names': ['API1Client', 'API2Client'],
+ >>> }
+ >>> params.update(myservice_config)
+ >>> return [params]
+
+ >>> # Example implementation with two service clients
+ >>> foo1_config = config.service_client_config('foo')
+ >>> params_foo1 = {
+ >>> 'name': 'foo_v1',
+ >>> 'service_version': 'foo.v1',
+ >>> 'module_path': 'bar_tempest_tests.services.foo.v1',
+ >>> 'client_names': ['API1Client', 'API2Client'],
+ >>> }
+ >>> params_foo1.update(foo_config)
+ >>> foo2_config = config.service_client_config('foo')
+ >>> params_foo2 = {
+ >>> 'name': 'foo_v2',
+ >>> 'service_version': 'foo.v2',
+ >>> 'module_path': 'bar_tempest_tests.services.foo.v2',
+ >>> 'client_names': ['API1Client', 'API2Client'],
+ >>> }
+ >>> params_foo2.update(foo2_config)
+ >>> return [params_foo1, params_foo2]
+ """
+ return []
+
@misc.singleton
class TempestTestPluginManager(object):
@@ -75,6 +124,7 @@
'tempest.test_plugins', invoke_on_load=True,
propagate_map_exceptions=True,
on_load_failure_callback=self.failure_hook)
+ self._register_service_clients()
@staticmethod
def failure_hook(_, ep, err):
@@ -102,3 +152,13 @@
if opt_list:
plugin_options.extend(opt_list)
return plugin_options
+
+ def _register_service_clients(self):
+ registry = clients.ClientsRegistry()
+ for plug in self.ext_plugins:
+ try:
+ registry.register_service_client(
+ plug.name, plug.obj.get_service_clients())
+ except Exception:
+ LOG.exception('Plugin %s raised an exception trying to run '
+ 'get_service_clients' % plug.name)
diff --git a/tempest/tests/cmd/test_subunit_describe_calls.py b/tempest/tests/cmd/test_subunit_describe_calls.py
index 43b417a..1c24c37 100644
--- a/tempest/tests/cmd/test_subunit_describe_calls.py
+++ b/tempest/tests/cmd/test_subunit_describe_calls.py
@@ -38,46 +38,159 @@
os.path.dirname(os.path.abspath(__file__)),
'sample_streams/calls.subunit')
parser = subunit_describe_calls.parse(
- subunit_file, "pythonlogging", None)
+ open(subunit_file), "pythonlogging", None)
expected_result = {
- 'bar': [{'name': 'AgentsAdminTestJSON:setUp',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:test_create_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:tearDown',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/1',
- 'verb': 'DELETE'},
- {'name': 'AgentsAdminTestJSON:_run_cleanups',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/2',
- 'verb': 'DELETE'}],
- 'foo': [{'name': 'AgentsAdminTestJSON:setUp',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'POST'},
- {'name': 'AgentsAdminTestJSON:test_delete_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents/3',
- 'verb': 'DELETE'},
- {'name': 'AgentsAdminTestJSON:test_delete_agent',
- 'service': 'Nova',
- 'status_code': '200',
- 'url': 'v2.1/<id>/os-agents',
- 'verb': 'GET'},
- {'name': 'AgentsAdminTestJSON:tearDown',
- 'service': 'Nova',
- 'status_code': '404',
- 'url': 'v2.1/<id>/os-agents/3',
- 'verb': 'DELETE'}]}
+ 'bar': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-424013832", "os": "linux", '
+ '"agent_id": 1}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-25ddaae2-0ef1-40d1-8228-59bd64a7e75b', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_create_agent',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "kvm", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86-252246646", "os": "win", '
+ '"agent_id": 2}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'195', 'x-compute-request-id': "
+ "'req-b4136f06-c015-4e7e-995f-c43831e3ecce', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-ee905fd6-a5b5-4da4-8c37-5363cb25bd9d', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/1',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:_run_cleanups',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-e912cac0-63e0-4679-a68a-b6d18ddca074', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:00 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/2',
+ 'verb': 'DELETE'}],
+ 'foo': [{
+ 'name': 'AgentsAdminTestJSON:setUp',
+ 'request_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux"}}',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agent": {"url": "xxx://xxxx/xxx/xxx", '
+ '"hypervisor": "common", "md5hash": '
+ '"add6bb58e139be103324d04d82d8f545", "version": "7.0", '
+ '"architecture": "tempest-x86_64-948635295", "os": "linux", '
+ '"agent_id": 3}}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'203', 'x-compute-request-id': "
+ "'req-ccd2116d-04b1-4ffe-ae32-fb623f68bf1c', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'POST'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'0', 'x-compute-request-id': "
+ "'req-6e7fa28f-ae61-4388-9a78-947c58bc0588', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}, {
+ 'name': 'AgentsAdminTestJSON:test_delete_agent',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_body': '{"agents": []}',
+ 'response_headers': "{'status': '200', 'content-length': "
+ "'14', 'content-location': "
+ "'http://23.253.76.97:8774/v2.1/"
+ "cf6b1933fe5b476fbbabb876f6d1b924/os-agents', "
+ "'x-compute-request-id': "
+ "'req-e41aa9b4-41a6-4138-ae04-220b768eb644', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:01 GMT', 'content-type': "
+ "'application/json'}",
+ 'service': 'Nova',
+ 'status_code': '200',
+ 'url': 'v2.1/<id>/os-agents',
+ 'verb': 'GET'}, {
+ 'name': 'AgentsAdminTestJSON:tearDown',
+ 'request_body': 'None',
+ 'request_headers': "{'Content-Type': 'application/json', "
+ "'Accept': 'application/json', 'X-Auth-Token': '<omitted>'}",
+ 'response_headers': "{'status': '404', 'content-length': "
+ "'82', 'x-compute-request-id': "
+ "'req-e297aeea-91cf-4f26-b49c-8f46b1b7a926', 'vary': "
+ "'X-OpenStack-Nova-API-Version', 'connection': 'close', "
+ "'x-openstack-nova-api-version': '2.1', 'date': "
+ "'Tue, 02 Feb 2016 03:27:02 GMT', 'content-type': "
+ "'application/json; charset=UTF-8'}",
+ 'service': 'Nova',
+ 'status_code': '404',
+ 'url': 'v2.1/<id>/os-agents/3',
+ 'verb': 'DELETE'}]}
+
self.assertEqual(expected_result, parser.test_logs)
diff --git a/tempest/tests/fake_tempest_plugin.py b/tempest/tests/fake_tempest_plugin.py
index f718d0b..56aae1e 100644
--- a/tempest/tests/fake_tempest_plugin.py
+++ b/tempest/tests/fake_tempest_plugin.py
@@ -18,6 +18,7 @@
class FakePlugin(plugins.TempestPlugin):
expected_load_test = ["my/test/path", "/home/dir"]
+ expected_service_clients = [{'foo': 'bar'}]
def load_tests(self):
return self.expected_load_test
@@ -28,6 +29,9 @@
def get_opt_lists(self):
return []
+ def get_service_clients(self):
+ return self.expected_service_clients
+
class FakeStevedoreObj(object):
obj = FakePlugin()
@@ -38,3 +42,26 @@
def __init__(self, name='Test1'):
self._name = name
+
+
+class FakePluginNoServiceClients(plugins.TempestPlugin):
+
+ def load_tests(self):
+ return []
+
+ def register_opts(self, conf):
+ return
+
+ def get_opt_lists(self):
+ return []
+
+
+class FakeStevedoreObjNoServiceClients(object):
+ obj = FakePluginNoServiceClients()
+
+ @property
+ def name(self):
+ return self._name
+
+ def __init__(self, name='Test2'):
+ self._name = name
diff --git a/tempest/tests/lib/test_auth.py b/tempest/tests/lib/test_auth.py
index 12590a3..6da7e41 100644
--- a/tempest/tests/lib/test_auth.py
+++ b/tempest/tests/lib/test_auth.py
@@ -244,7 +244,7 @@
# The original headers where empty
self.assertNotEqual(url, self.target_url)
self.assertIsNone(headers)
- self.assertEqual(body, None)
+ self.assertIsNone(body)
def _test_request_with_alt_part_without_alt_data_no_change(self, body):
"""Test empty alternate auth data with no effect
diff --git a/tempest/tests/negative/test_negative_generators.py b/tempest/tests/negative/test_negative_generators.py
index 78fd80d..2e45ef7 100644
--- a/tempest/tests/negative/test_negative_generators.py
+++ b/tempest/tests/negative/test_negative_generators.py
@@ -146,5 +146,5 @@
schema_under_test = copy.copy(valid_schema)
expected_result = \
self.generator.generate_payload(test, schema_under_test)
- self.assertEqual(expected_result, None)
+ self.assertIsNone(expected_result)
self._validate_result(valid_schema, schema_under_test)
diff --git a/tempest/tests/test_service_clients.py b/tempest/tests/test_service_clients.py
index a559086..3d8b360 100644
--- a/tempest/tests/test_service_clients.py
+++ b/tempest/tests/test_service_clients.py
@@ -13,15 +13,154 @@
# the License.
import fixtures
+import mock
import testtools
+import types
from tempest.lib import auth
from tempest.lib import exceptions
from tempest import service_clients
from tempest.tests import base
+from tempest.tests.lib import fake_auth_provider
from tempest.tests.lib import fake_credentials
+has_attribute = testtools.matchers.MatchesPredicateWithParams(
+ lambda x, y: hasattr(x, y), '{0} does not have an attribute {1}')
+
+
+class TestClientsFactory(base.TestCase):
+
+ def setUp(self):
+ super(TestClientsFactory, self).setUp()
+ self.classes = []
+
+ def _setup_fake_module(self, class_names=None, extra_dict=None):
+ class_names = class_names or []
+ fake_module = types.ModuleType('fake_service_client')
+ _dict = {}
+ # Add fake classes to the fake module
+ for name in class_names:
+ _dict[name] = type(name, (object,), {})
+ # Store it for assertions
+ self.classes.append(_dict[name])
+ if extra_dict:
+ _dict[extra_dict] = extra_dict
+ fake_module.__dict__.update(_dict)
+ fixture_importlib = self.useFixture(fixtures.MockPatch(
+ 'importlib.import_module', return_value=fake_module))
+ return fixture_importlib.mock
+
+ def test___init___one_class(self):
+ fake_partial = 'fake_partial'
+ partial_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory._get_partial_class',
+ return_value=fake_partial)).mock
+ class_names = ['FakeServiceClient1']
+ mock_importlib = self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = service_clients.ClientsFactory('fake_path', class_names,
+ auth_provider, **params)
+ # Assert module has been imported
+ mock_importlib.assert_called_once_with('fake_path')
+ # All attributes have been created
+ for client in class_names:
+ self.assertThat(factory, has_attribute(client))
+ # Partial have been invoked correctly
+ partial_mock.assert_called_once_with(
+ self.classes[0], auth_provider, params)
+ # Get the clients
+ for name in class_names:
+ self.assertEqual(fake_partial, getattr(factory, name))
+
+ def test___init___two_classes(self):
+ fake_partial = 'fake_partial'
+ partial_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory._get_partial_class',
+ return_value=fake_partial)).mock
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ mock_importlib = self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = service_clients.ClientsFactory('fake_path', class_names,
+ auth_provider, **params)
+ # Assert module has been imported
+ mock_importlib.assert_called_once_with('fake_path')
+ # All attributes have been created
+ for client in class_names:
+ self.assertThat(factory, has_attribute(client))
+ # Partial have been invoked the right number of times
+ partial_mock.call_count = len(class_names)
+ # Get the clients
+ for name in class_names:
+ self.assertEqual(fake_partial, getattr(factory, name))
+
+ def test___init___no_module(self):
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ with testtools.ExpectedException(ImportError, '.*fake_module.*'):
+ service_clients.ClientsFactory('fake_module', class_names,
+ auth_provider)
+
+ def test___init___not_a_class(self):
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ extended_class_names = class_names + ['not_really_a_class']
+ self._setup_fake_module(
+ class_names=class_names, extra_dict='not_really_a_class')
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ expected_msg = '.*not_really_a_class.*str.*'
+ with testtools.ExpectedException(TypeError, expected_msg):
+ service_clients.ClientsFactory('fake_module', extended_class_names,
+ auth_provider)
+
+ def test___init___class_not_found(self):
+ class_names = ['FakeServiceClient1', 'FakeServiceClient2']
+ extended_class_names = class_names + ['not_really_a_class']
+ self._setup_fake_module(class_names=class_names)
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ expected_msg = '.*not_really_a_class.*fake_service_client.*'
+ with testtools.ExpectedException(AttributeError, expected_msg):
+ service_clients.ClientsFactory('fake_module', extended_class_names,
+ auth_provider)
+
+ def test__get_partial_class_no_later_kwargs(self):
+ expected_fake_client = 'not_really_a_client'
+ self._setup_fake_module(class_names=[])
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ factory = service_clients.ClientsFactory(
+ 'fake_path', [], auth_provider, **params)
+ klass_mock = mock.Mock(return_value=expected_fake_client)
+ partial = factory._get_partial_class(klass_mock, auth_provider, params)
+ # Class has not be initialised yet
+ klass_mock.assert_not_called()
+ # Use partial and assert on parameters
+ client = partial()
+ self.assertEqual(expected_fake_client, client)
+ klass_mock.assert_called_once_with(auth_provider=auth_provider,
+ **params)
+
+ def test__get_partial_class_later_kwargs(self):
+ expected_fake_client = 'not_really_a_client'
+ self._setup_fake_module(class_names=[])
+ auth_provider = fake_auth_provider.FakeAuthProvider()
+ params = {'k1': 'v1', 'k2': 'v2'}
+ later_params = {'k2': 'v4', 'k3': 'v3'}
+ factory = service_clients.ClientsFactory(
+ 'fake_path', [], auth_provider, **params)
+ klass_mock = mock.Mock(return_value=expected_fake_client)
+ partial = factory._get_partial_class(klass_mock, auth_provider, params)
+ # Class has not be initialised yet
+ klass_mock.assert_not_called()
+ # Use partial and assert on parameters
+ client = partial(**later_params)
+ params.update(later_params)
+ self.assertEqual(expected_fake_client, client)
+ klass_mock.assert_called_once_with(auth_provider=auth_provider,
+ **params)
+
+
class TestServiceClients(base.TestCase):
def setUp(self):
@@ -124,3 +263,84 @@
for _key in _params.keys():
self.assertEqual(expected_params[_key],
_params[_key])
+
+ def test_register_service_client_module(self):
+ expected_params = {'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2'}
+ _manager = self._get_manager(init_region='fake_region_default')
+ # Mock after the _manager is setup to preserve the call count
+ factory_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory')).mock
+ _manager.register_service_client_module(
+ name='fake_module',
+ service_version='fake_service',
+ module_path='fake.path.to.module',
+ client_names=[],
+ **expected_params)
+ self.assertThat(_manager, has_attribute('fake_module'))
+ # Assert called once, without check for exact parameters
+ self.assertTrue(factory_mock.called)
+ self.assertEqual(1, factory_mock.call_count)
+ # Assert expected params are in with their values
+ actual_kwargs = factory_mock.call_args[1]
+ self.assertIn('region', actual_kwargs)
+ self.assertEqual('fake_region_default', actual_kwargs['region'])
+ for param in expected_params:
+ self.assertIn(param, actual_kwargs)
+ self.assertEqual(expected_params[param], actual_kwargs[param])
+ # Assert the new service is registered
+ self.assertIn('fake_service', _manager._registered_services)
+
+ def test_register_service_client_module_override_default(self):
+ new_region = 'new_region'
+ expected_params = {'fake_param1': 'fake_value1',
+ 'fake_param2': 'fake_value2',
+ 'region': new_region}
+ _manager = self._get_manager(init_region='fake_region_default')
+ # Mock after the _manager is setup to preserve the call count
+ factory_mock = self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory')).mock
+ _manager.register_service_client_module(
+ name='fake_module',
+ service_version='fake_service',
+ module_path='fake.path.to.module',
+ client_names=[],
+ **expected_params)
+ self.assertThat(_manager, has_attribute('fake_module'))
+ # Assert called once, without check for exact parameters
+ self.assertTrue(factory_mock.called)
+ self.assertEqual(1, factory_mock.call_count)
+ # Assert expected params are in with their values
+ actual_kwargs = factory_mock.call_args[1]
+ self.assertIn('region', actual_kwargs)
+ self.assertEqual(new_region, actual_kwargs['region'])
+ for param in expected_params:
+ self.assertIn(param, actual_kwargs)
+ self.assertEqual(expected_params[param], actual_kwargs[param])
+ # Assert the new service is registered
+ self.assertIn('fake_service', _manager._registered_services)
+
+ def test_register_service_client_module_duplicate_name(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory'))
+ _manager = self._get_manager()
+ name_owner = 'this_is_a_string'
+ setattr(_manager, 'fake_module', name_owner)
+ expected_error = '.*' + name_owner
+ with testtools.ExpectedException(
+ exceptions.ServiceClientRegistrationException, expected_error):
+ _manager.register_service_client_module(
+ name='fake_module', module_path='fake.path.to.module',
+ service_version='fake_service', client_names=[])
+
+ def test_register_service_client_module_duplicate_service(self):
+ self.useFixture(fixtures.MockPatch(
+ 'tempest.service_clients.ClientsFactory'))
+ _manager = self._get_manager()
+ duplicate_service = 'fake_service1'
+ expected_error = '.*' + duplicate_service
+ with testtools.ExpectedException(
+ exceptions.ServiceClientRegistrationException, expected_error):
+ _manager.register_service_client_module(
+ name='fake_module', module_path='fake.path.to.module',
+ service_version=duplicate_service, client_names=[])
diff --git a/tempest/tests/test_tempest_plugin.py b/tempest/tests/test_tempest_plugin.py
index c07e98c..dd50125 100644
--- a/tempest/tests/test_tempest_plugin.py
+++ b/tempest/tests/test_tempest_plugin.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.lib.services import clients
from tempest.test_discover import plugins
from tempest.tests import base
from tempest.tests import fake_tempest_plugin as fake_plugin
@@ -42,3 +43,39 @@
result['fake01'])
self.assertEqual(fake_plugin.FakePlugin.expected_load_test,
result['fake02'])
+
+ def test__register_service_clients_with_one_plugin(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ fake_obj = fake_plugin.FakeStevedoreObj()
+ manager.ext_plugins = [fake_obj]
+ manager._register_service_clients()
+ expected_result = fake_plugin.FakePlugin.expected_service_clients
+ registered_clients = registry.get_service_clients()
+ self.assertIn(fake_obj.name, registered_clients)
+ self.assertEqual(expected_result, registered_clients[fake_obj.name])
+
+ def test__get_service_clients_with_two_plugins(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ obj1 = fake_plugin.FakeStevedoreObj('fake01')
+ obj2 = fake_plugin.FakeStevedoreObj('fake02')
+ manager.ext_plugins = [obj1, obj2]
+ manager._register_service_clients()
+ expected_result = fake_plugin.FakePlugin.expected_service_clients
+ registered_clients = registry.get_service_clients()
+ self.assertIn('fake01', registered_clients)
+ self.assertIn('fake02', registered_clients)
+ self.assertEqual(expected_result, registered_clients['fake01'])
+ self.assertEqual(expected_result, registered_clients['fake02'])
+
+ def test__register_service_clients_one_plugin_no_service_clients(self):
+ registry = clients.ClientsRegistry()
+ manager = plugins.TempestTestPluginManager()
+ fake_obj = fake_plugin.FakeStevedoreObjNoServiceClients()
+ manager.ext_plugins = [fake_obj]
+ manager._register_service_clients()
+ expected_result = []
+ registered_clients = registry.get_service_clients()
+ self.assertIn(fake_obj.name, registered_clients)
+ self.assertEqual(expected_result, registered_clients[fake_obj.name])