Merge "Adds client API and tests for volume attachments"
diff --git a/tempest/common/ssh.py b/tempest/common/ssh.py
index 085fce3..faf182a 100644
--- a/tempest/common/ssh.py
+++ b/tempest/common/ssh.py
@@ -18,6 +18,7 @@
import time
import socket
import warnings
+import select
from tempest import exceptions
@@ -37,7 +38,8 @@
self.look_for_keys = look_for_keys
self.key_filename = key_filename
self.timeout = int(timeout)
- self.channel_timeout = int(channel_timeout)
+ self.channel_timeout = float(channel_timeout)
+ self.buf_size = 1024
def _get_ssh_connection(self):
"""Returns an ssh connection to the specified host"""
@@ -85,24 +87,48 @@
return
def exec_command(self, cmd):
- """Execute the specified command on the server.
+ """
+ Execute the specified command on the server.
- :returns: data read from standard output of the command
+ Note that this method is reading whole command outputs to memory, thus
+ shouldn't be used for large outputs.
+ :returns: data read from standard output of the command.
+ :raises: SSHExecCommandFailed if command returns nonzero
+ status. The exception contains command status stderr content.
"""
ssh = self._get_ssh_connection()
- stdin, stdout, stderr = ssh.exec_command(cmd)
- stdin.flush()
- stdin.channel.shutdown_write()
- stdout.channel.settimeout(self.channel_timeout)
- status = stdout.channel.recv_exit_status()
- try:
- output = stdout.read()
- except socket.timeout:
- if status == 0:
- return None, status
- ssh.close()
- return status, output
+ transport = ssh.get_transport()
+ channel = transport.open_session()
+ channel.exec_command(cmd)
+ channel.shutdown_write()
+ out_data = []
+ err_data = []
+
+ select_params = [channel], [], [], self.channel_timeout
+ while True:
+ ready = select.select(*select_params)
+ if not any(ready):
+ raise exceptions.TimeoutException(
+ "Command: '{0}' executed on host '{1}'.".format(
+ cmd, self.host))
+ if not ready[0]: # If there is nothing to read.
+ continue
+ out_chunk = err_chunk = None
+ if channel.recv_ready():
+ out_chunk = channel.recv(self.buf_size)
+ out_data += out_chunk,
+ if channel.recv_stderr_ready():
+ err_chunk = channel.recv_stderr(self.buf_size)
+ err_data += err_chunk,
+ if channel.closed and not err_chunk and not out_chunk:
+ break
+ exit_status = channel.recv_exit_status()
+ if 0 != exit_status:
+ raise exceptions.SSHExecCommandFailed(
+ command=cmd, exit_status=exit_status,
+ strerror=''.join(err_data))
+ return ''.join(out_data)
def test_connection_auth(self):
""" Returns true if ssh can connect to server"""
diff --git a/tempest/common/utils/data_utils.py b/tempest/common/utils/data_utils.py
index 752bc10..fc7c112 100644
--- a/tempest/common/utils/data_utils.py
+++ b/tempest/common/utils/data_utils.py
@@ -5,7 +5,7 @@
def rand_name(name='test'):
- return name + str(random.randint(1, 99999999999))
+ return name + str(random.randint(1, 999999))
def build_url(host, port, api_version=None, path=None,
diff --git a/tempest/exceptions.py b/tempest/exceptions.py
index 03cf163..7154b80 100644
--- a/tempest/exceptions.py
+++ b/tempest/exceptions.py
@@ -91,7 +91,13 @@
class SSHTimeout(TempestException):
message = ("Connection to the %(host)s via SSH timed out.\n"
- "User: %(user)s, Password: %(password)s")
+ "User: %(user)s, Password: %(password)s")
+
+
+class SSHExecCommandFailed(TempestException):
+ ''' Raised when remotely executed command returns nonzero status. '''
+ message = ("Command '%(command)s', exit status: %(exit_status)d, "
+ "Error:\n%(strerror)s")
class ServerUnreachable(TempestException):
diff --git a/tempest/manager.py b/tempest/manager.py
index 228c3b9..bab7ea7 100644
--- a/tempest/manager.py
+++ b/tempest/manager.py
@@ -40,16 +40,16 @@
from tempest.services.nova.json import console_output_client
NetworkClient = network_client.NetworkClient
-ImagesClient = images_client.ImagesClient
+ImagesClient = images_client.ImagesClientJSON
FlavorsClient = flavors_client.FlavorsClientJSON
ServersClient = servers_client.ServersClientJSON
LimitsClient = limits_client.LimitsClientJSON
ExtensionsClient = extensions_client.ExtensionsClientJSON
SecurityGroupsClient = security_groups_client.SecurityGroupsClient
-FloatingIPsClient = floating_ips_client.FloatingIPsClient
+FloatingIPsClient = floating_ips_client.FloatingIPsClientJSON
KeyPairsClient = keypairs_client.KeyPairsClientJSON
-VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClient
-VolumesClient = volumes_client.VolumesClient
+VolumesExtensionsClient = volumes_extensions_client.VolumesExtensionsClientJSON
+VolumesClient = volumes_client.VolumesClientJSON
ConsoleOutputsClient = console_output_client.ConsoleOutputsClient
LOG = logging.getLogger(__name__)
diff --git a/tempest/openstack.py b/tempest/openstack.py
index 27ae6c0..d9abb64 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -19,30 +19,47 @@
from tempest import config
from tempest import exceptions
+from tempest.services.identity.json.admin_client import AdminClientJSON
+from tempest.services.identity.json.admin_client import TokenClientJSON
+from tempest.services.identity.xml.admin_client import AdminClientXML
+from tempest.services.identity.xml.admin_client import TokenClientXML
from tempest.services.image import service as image_service
from tempest.services.network.json.network_client import NetworkClient
from tempest.services.nova.json.extensions_client import ExtensionsClientJSON
from tempest.services.nova.json.flavors_client import FlavorsClientJSON
-from tempest.services.volume.json.volumes_client import VolumesClient
-from tempest.services.nova.json.images_client import ImagesClient
+from tempest.services.nova.json.floating_ips_client import \
+FloatingIPsClientJSON
+from tempest.services.nova.json.images_client import ImagesClientJSON
from tempest.services.nova.json.limits_client import LimitsClientJSON
from tempest.services.nova.json.servers_client import ServersClientJSON
from tempest.services.nova.json.security_groups_client \
import SecurityGroupsClient
-from tempest.services.nova.json.floating_ips_client import FloatingIPsClient
from tempest.services.nova.json.keypairs_client import KeyPairsClientJSON
from tempest.services.nova.json.volumes_extensions_client \
-import VolumesExtensionsClient
+import VolumesExtensionsClientJSON
from tempest.services.nova.json.console_output_client \
import ConsoleOutputsClient
from tempest.services.nova.xml.extensions_client import ExtensionsClientXML
from tempest.services.nova.xml.flavors_client import FlavorsClientXML
+from tempest.services.nova.xml.floating_ips_client import \
+FloatingIPsClientXML
+from tempest.services.nova.xml.images_client import ImagesClientXML
from tempest.services.nova.xml.keypairs_client import KeyPairsClientXML
from tempest.services.nova.xml.limits_client import LimitsClientXML
from tempest.services.nova.xml.servers_client import ServersClientXML
+from tempest.services.nova.xml.volumes_extensions_client \
+import VolumesExtensionsClientXML
+from tempest.services.volume.json.volumes_client import VolumesClientJSON
+from tempest.services.volume.xml.volumes_client import VolumesClientXML
+
LOG = logging.getLogger(__name__)
+IMAGES_CLIENTS = {
+ "json": ImagesClientJSON,
+ "xml": ImagesClientXML,
+}
+
KEYPAIRS_CLIENTS = {
"json": KeyPairsClientJSON,
"xml": KeyPairsClientXML,
@@ -68,6 +85,32 @@
"xml": ExtensionsClientXML
}
+VOLUMES_EXTENSIONS_CLIENTS = {
+ "json": VolumesExtensionsClientJSON,
+ "xml": VolumesExtensionsClientXML,
+}
+
+FLOAT_CLIENTS = {
+ "json": FloatingIPsClientJSON,
+ "xml": FloatingIPsClientXML,
+}
+
+VOLUMES_CLIENTS = {
+ "json": VolumesClientJSON,
+ "xml": VolumesClientXML,
+}
+
+
+ADMIN_CLIENT = {
+ "json": AdminClientJSON,
+ "xml": AdminClientXML,
+}
+
+TOKEN_CLIENT = {
+ "json": TokenClientJSON,
+ "xml": TokenClientXML,
+}
+
class Manager(object):
@@ -112,20 +155,23 @@
try:
self.servers_client = SERVERS_CLIENTS[interface](*client_args)
self.limits_client = LIMITS_CLIENTS[interface](*client_args)
+ self.images_client = IMAGES_CLIENTS[interface](*client_args)
self.keypairs_client = KEYPAIRS_CLIENTS[interface](*client_args)
self.flavors_client = FLAVORS_CLIENTS[interface](*client_args)
self.extensions_client = \
EXTENSIONS_CLIENTS[interface](*client_args)
+ self.volumes_extensions_client = \
+ VOLUMES_EXTENSIONS_CLIENTS[interface](*client_args)
+ self.floating_ips_client = FLOAT_CLIENTS[interface](*client_args)
+ self.volumes_client = VOLUMES_CLIENTS[interface](*client_args)
+ self.admin_client = ADMIN_CLIENT[interface](*client_args)
+ self.token_client = TOKEN_CLIENT[interface](self.config)
except KeyError:
msg = "Unsupported interface type `%s'" % interface
raise exceptions.InvalidConfiguration(msg)
- self.images_client = ImagesClient(*client_args)
self.security_groups_client = SecurityGroupsClient(*client_args)
- self.floating_ips_client = FloatingIPsClient(*client_args)
self.console_outputs_client = ConsoleOutputsClient(*client_args)
self.network_client = NetworkClient(*client_args)
- self.volumes_extensions_client = VolumesExtensionsClient(*client_args)
- self.volumes_client = VolumesClient(*client_args)
class AltManager(Manager):
@@ -168,3 +214,33 @@
self.services = {}
self.services['image'] = image_service.Service(self.config)
self.images = self.services['image']
+
+
+class IdentityManager(Manager):
+
+ """
+ Manager object that uses the alt_XXX credentials for its
+ managed client objects
+ """
+
+ def __init__(self, interface='json'):
+ conf = config.TempestConfig()
+ super(IdentityManager, self).__init__(conf.identity_admin.username,
+ conf.identity_admin.password,
+ conf.identity_admin.tenant_name,
+ interface)
+
+
+class IdentityNaManager(Manager):
+
+ """
+ Manager object that uses the alt_XXX credentials for its
+ managed client objects
+ """
+
+ def __init__(self, interface='json'):
+ conf = config.TempestConfig()
+ super(IdentityNaManager, self).__init__(conf.compute.username,
+ conf.compute.password,
+ conf.compute.tenant_name,
+ interface)
diff --git a/tempest/services/identity/json/admin_client.py b/tempest/services/identity/json/admin_client.py
index cb9c10b..a42a690 100644
--- a/tempest/services/identity/json/admin_client.py
+++ b/tempest/services/identity/json/admin_client.py
@@ -4,10 +4,10 @@
import json
-class AdminClient(RestClient):
+class AdminClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(AdminClient, self).__init__(config, username, password,
+ super(AdminClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.identity.catalog_type
self.endpoint_url = 'adminURL'
@@ -204,7 +204,7 @@
return self.delete(url)
-class TokenClient(RestClient):
+class TokenClientJSON(RestClient):
def __init__(self, config):
self.auth_url = config.identity.auth_url
diff --git a/tempest/services/identity/xml/__init__.py b/tempest/services/identity/xml/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/identity/xml/__init__.py
diff --git a/tempest/services/identity/xml/admin_client.py b/tempest/services/identity/xml/admin_client.py
new file mode 100644
index 0000000..948c643
--- /dev/null
+++ b/tempest/services/identity/xml/admin_client.py
@@ -0,0 +1,267 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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.
+
+import logging
+from lxml import etree
+from tempest.common.rest_client import RestClient
+from tempest.common.rest_client import RestClientXML
+from tempest.services.nova.xml.common import Document
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import xml_to_json
+from tempest import exceptions
+import httplib2
+import json
+
+XMLNS = "http://docs.openstack.org/identity/api/v2.0"
+
+
+class AdminClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(AdminClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ def _parse_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ def _parse_body(self, body):
+ json = xml_to_json(body)
+ return json
+
+ def has_admin_extensions(self):
+ """
+ Returns True if the KSADM Admin Extensions are supported
+ False otherwise
+ """
+ if hasattr(self, '_has_admin_extensions'):
+ return self._has_admin_extensions
+ resp, body = self.list_roles()
+ self._has_admin_extensions = ('status' in resp and resp.status != 503)
+ return self._has_admin_extensions
+
+ def create_role(self, name):
+ """Create a role"""
+ create_role = Element("role",
+ xmlns=XMLNS,
+ name=name)
+ resp, body = self.post('OS-KSADM/roles', str(Document(create_role)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def create_tenant(self, name, **kwargs):
+ """
+ Create a tenant
+ name (required): New tenant name
+ description: Description of new tenant (default is none)
+ enabled <true|false>: Initial tenant status (default is true)
+ """
+ en = kwargs.get('enabled', 'true')
+ create_tenant = Element("tenant",
+ xmlns=XMLNS,
+ name=name,
+ description=kwargs.get('description', ''),
+ enabled=str(en).lower())
+ resp, body = self.post('tenants', str(Document(create_tenant)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_role(self, role_id):
+ """Delete a role"""
+ resp, body = self.delete('OS-KSADM/roles/%s' % str(role_id),
+ self.headers)
+ return resp, body
+
+ def list_user_roles(self, tenant_id, user_id):
+ """Returns a list of roles assigned to a user for a tenant"""
+ url = '/tenants/%s/users/%s/roles' % (tenant_id, user_id)
+ resp, body = self.get(url, self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def assign_user_role(self, tenant_id, user_id, role_id):
+ """Add roles to a user on a tenant"""
+ resp, body = self.put('/tenants/%s/users/%s/roles/OS-KSADM/%s'
+ % (tenant_id, user_id, role_id),
+ '', self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def remove_user_role(self, tenant_id, user_id, role_id):
+ """Removes a role assignment for a user on a tenant"""
+ return self.delete('/tenants/%s/users/%s/roles/OS-KSADM/%s'
+ % (tenant_id, user_id, role_id), self.headers)
+
+ def delete_tenant(self, tenant_id):
+ """Delete a tenant"""
+ resp, body = self.delete('tenants/%s' % str(tenant_id), self.headers)
+ return resp, body
+
+ def get_tenant(self, tenant_id):
+ """Get tenant details"""
+ resp, body = self.get('tenants/%s' % str(tenant_id), self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def list_roles(self):
+ """Returns roles"""
+ resp, body = self.get('OS-KSADM/roles', self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def list_tenants(self):
+ """Returns tenants"""
+ resp, body = self.get('tenants', self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def update_tenant(self, tenant_id, **kwargs):
+ """Updates a tenant"""
+ resp, body = self.get_tenant(tenant_id)
+ name = kwargs.get('name', body['name'])
+ desc = kwargs.get('description', body['description'])
+ en = kwargs.get('enabled', body['enabled'])
+ update_tenant = Element("tenant",
+ xmlns=XMLNS,
+ id=tenant_id,
+ name=name,
+ description=desc,
+ enabled=str(en).lower())
+
+ resp, body = self.post('tenants/%s' % tenant_id,
+ str(Document(update_tenant)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def create_user(self, name, password, tenant_id, email):
+ """Create a user"""
+ create_user = Element("user",
+ xmlns=XMLNS,
+ name=name,
+ password=password,
+ tenantId=tenant_id,
+ email=email)
+ resp, body = self.post('users', str(Document(create_user)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_user(self, user_id):
+ """Delete a user"""
+ resp, body = self.delete("users/%s" % user_id, self.headers)
+ return resp, body
+
+ def get_users(self):
+ """Get the list of users"""
+ resp, body = self.get("users", self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def enable_disable_user(self, user_id, enabled):
+ """Enables or disables a user"""
+ enable_user = Element("user",
+ enabled=str(enabled).lower())
+ resp, body = self.put('users/%s/enabled' % user_id,
+ str(Document(enable_user)), self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def delete_token(self, token_id):
+ """Delete a token"""
+ resp, body = self.delete("tokens/%s" % token_id, self.headers)
+ return resp, body
+
+ def list_users_for_tenant(self, tenant_id):
+ """List users for a Tenant"""
+ resp, body = self.get('/tenants/%s/users' % tenant_id, self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def create_service(self, name, type, **kwargs):
+ """Create a service"""
+ OS_KSADM = "http://docs.openstack.org/identity/api/ext/OS-KSADM/v1.0"
+ create_service = Element("service",
+ xmlns=OS_KSADM,
+ name=name,
+ type=type,
+ description=kwargs.get('description'))
+ resp, body = self.post('OS-KSADM/services',
+ str(Document(create_service)),
+ self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def get_service(self, service_id):
+ """Get Service"""
+ url = '/OS-KSADM/services/%s' % service_id
+ resp, body = self.get(url, self.headers)
+ body = self._parse_body(etree.fromstring(body))
+ return resp, body
+
+ def delete_service(self, service_id):
+ """Delete Service"""
+ url = '/OS-KSADM/services/%s' % service_id
+ return self.delete(url, self.headers)
+
+
+class TokenClientXML(RestClientXML):
+
+ def __init__(self, config):
+ self.auth_url = config.identity.auth_url
+
+ def auth(self, user, password, tenant):
+ passwordCreds = Element("passwordCredentials",
+ username=user,
+ password=password)
+ auth = Element("auth",
+ tenantName=tenant)
+ auth.append(passwordCreds)
+ headers = {'Content-Type': 'application/xml'}
+ resp, body = self.post(self.auth_url, headers=headers,
+ body=str(Document(auth)))
+ return resp, body
+
+ def request(self, method, url, headers=None, body=None):
+ """A simple HTTP request interface."""
+ self.http_obj = httplib2.Http()
+ if headers == None:
+ headers = {}
+
+ resp, resp_body = self.http_obj.request(url, method,
+ headers=headers, body=body)
+
+ if resp.status in (401, 403):
+ resp_body = json.loads(resp_body)
+ raise exceptions.Unauthorized(resp_body['error']['message'])
+
+ return resp, resp_body
+
+ def get_token(self, user, password, tenant):
+ resp, body = self.auth(user, password, tenant)
+ if resp['status'] != '202':
+ body = json.loads(body)
+ access = body['access']
+ token = access['token']
+ return token['id']
diff --git a/tempest/services/nova/json/floating_ips_client.py b/tempest/services/nova/json/floating_ips_client.py
index 9f382ff..231b61a 100644
--- a/tempest/services/nova/json/floating_ips_client.py
+++ b/tempest/services/nova/json/floating_ips_client.py
@@ -3,9 +3,9 @@
import json
-class FloatingIPsClient(RestClient):
+class FloatingIPsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(FloatingIPsClient, self).__init__(config, username, password,
+ super(FloatingIPsClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.compute.catalog_type
@@ -15,8 +15,8 @@
if params != None:
param_list = []
for param, value in params.iteritems():
- param_list.append("%s=%s&" % (param, value))
- url += '?' + ' '.join(param_list)
+ param_list.append("%s=%s" % (param, value))
+ url += '?' + ' &'.join(param_list)
resp, body = self.get(url)
body = json.loads(body)
return resp, body['floating_ips']
diff --git a/tempest/services/nova/json/images_client.py b/tempest/services/nova/json/images_client.py
index 87cb403..7a29b2f 100644
--- a/tempest/services/nova/json/images_client.py
+++ b/tempest/services/nova/json/images_client.py
@@ -4,11 +4,11 @@
import time
-class ImagesClient(RestClient):
+class ImagesClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(ImagesClient, self).__init__(config, username, password,
- auth_url, tenant_name)
+ super(ImagesClientJSON, self).__init__(config, username, password,
+ auth_url, tenant_name)
self.service = self.config.compute.catalog_type
self.build_interval = self.config.compute.build_interval
self.build_timeout = self.config.compute.build_timeout
diff --git a/tempest/services/nova/json/volumes_extensions_client.py b/tempest/services/nova/json/volumes_extensions_client.py
index 9b3590b..ed71f71 100644
--- a/tempest/services/nova/json/volumes_extensions_client.py
+++ b/tempest/services/nova/json/volumes_extensions_client.py
@@ -4,10 +4,10 @@
import time
-class VolumesExtensionsClient(RestClient):
+class VolumesExtensionsClientJSON(RestClient):
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(VolumesExtensionsClient, self).__init__(config, username,
+ super(VolumesExtensionsClientJSON, self).__init__(config, username,
password, auth_url,
tenant_name)
self.service = self.config.compute.catalog_type
diff --git a/tempest/services/nova/xml/floating_ips_client.py b/tempest/services/nova/xml/floating_ips_client.py
new file mode 100644
index 0000000..4b91abb
--- /dev/null
+++ b/tempest/services/nova/xml/floating_ips_client.py
@@ -0,0 +1,103 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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 lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import exceptions
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import Document
+from tempest.services.nova.xml.common import Element
+
+
+class FloatingIPsClientXML(RestClientXML):
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(FloatingIPsClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+
+ def _parse_array(self, node):
+ array = []
+ for child in node.getchildren():
+ array.append(xml_to_json(child))
+ return array
+
+ def _parse_floating_ip(self, body):
+ json = xml_to_json(body)
+ return json
+
+ def list_floating_ips(self, params=None):
+ """Returns a list of all floating IPs filtered by any parameters"""
+ url = 'os-floating-ips'
+ if params != None:
+ param_list = []
+ for param, value in params.iteritems():
+ param_list.append("%s=%s" % (param, value))
+ url += "?" + "&".join(param_list)
+
+ resp, body = self.get(url, self.headers)
+ body = self._parse_array(etree.fromstring(body))
+ return resp, body
+
+ def get_floating_ip_details(self, floating_ip_id):
+ """Get the details of a floating IP"""
+ url = "os-floating-ips/%s" % str(floating_ip_id)
+ resp, body = self.get(url, self.headers)
+ body = self._parse_floating_ip(etree.fromstring(body))
+ if resp.status == 404:
+ raise exceptions.NotFound(body)
+ return resp, body
+
+ def create_floating_ip(self):
+ """Allocate a floating IP to the project"""
+ url = 'os-floating-ips'
+ resp, body = self.post(url, None, self.headers)
+ body = self._parse_floating_ip(etree.fromstring(body))
+ return resp, body
+
+ def delete_floating_ip(self, floating_ip_id):
+ """Deletes the provided floating IP from the project"""
+ url = "os-floating-ips/%s" % str(floating_ip_id)
+ resp, body = self.delete(url, self.headers)
+ return resp, body
+
+ def associate_floating_ip_to_server(self, floating_ip, server_id):
+ """Associate the provided floating IP to a specific server"""
+ url = "servers/%s/action" % str(server_id)
+ doc = Document()
+ server = Element("addFloatingIp")
+ doc.append(server)
+ server.add_attr("address", floating_ip)
+ resp, body = self.post(url, str(doc), self.headers)
+ return resp, body
+
+ def disassociate_floating_ip_from_server(self, floating_ip, server_id):
+ """Disassociate the provided floating IP from a specific server"""
+ url = "servers/%s/action" % str(server_id)
+ doc = Document()
+ server = Element("removeFloatingIp")
+ doc.append(server)
+ server.add_attr("address", floating_ip)
+ resp, body = self.post(url, str(doc), self.headers)
+ return resp, body
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_floating_ip_details(id)
+ except exceptions.NotFound:
+ return True
+ return False
diff --git a/tempest/services/nova/xml/images_client.py b/tempest/services/nova/xml/images_client.py
new file mode 100644
index 0000000..0df8dfc
--- /dev/null
+++ b/tempest/services/nova/xml/images_client.py
@@ -0,0 +1,198 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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.
+
+import time
+import urllib
+
+from lxml import etree
+
+from tempest import exceptions
+from tempest.common.rest_client import RestClientXML
+from tempest.services.nova.xml.common import Document
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import XMLNS_11
+
+
+class ImagesClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(ImagesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_server(self, node):
+ json = xml_to_json(node)
+ return self._parse_links(node, json)
+
+ def _parse_image(self, node):
+ """Parses detailed XML image information into dictionary"""
+ json = xml_to_json(node)
+
+ self._parse_links(node, json)
+
+ # parse all metadata
+ if 'metadata' in json:
+ tag = node.find('{%s}metadata' % XMLNS_11)
+ json['metadata'] = dict((x.get('key'), x.text)
+ for x in tag.getchildren())
+
+ # parse server information
+ if 'server' in json:
+ tag = node.find('{%s}server' % XMLNS_11)
+ json['server'] = self._parse_server(tag)
+ return json
+
+ def _parse_links(self, node, json):
+ """Append multiple links under a list"""
+ # look for links
+ if 'link' in json:
+ # remove single link element
+ del json['link']
+ json['links'] = [xml_to_json(x) for x in
+ node.findall('{http://www.w3.org/2005/Atom}link')]
+ return json
+
+ def create_image(self, server_id, name, meta=None):
+ """Creates an image of the original server"""
+ post_body = Element('createImage', name=name)
+
+ if meta:
+ metadata = Element('metadata')
+ post_body.append(metadata)
+ for k, v in meta.items():
+ data = Element('meta', key=k)
+ data.append(Text(v))
+ metadata.append(data)
+ resp, body = self.post('servers/%s/action' % str(server_id),
+ str(Document(post_body)), self.headers)
+ return resp, body
+
+ def list_images(self, params=None):
+ """Returns a list of all images filtered by any parameters"""
+ url = 'images'
+ if params:
+ param_list = urllib.urlencode(params)
+ url += "?" + param_list
+
+ resp, body = self.get(url, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['images']
+
+ def list_images_with_detail(self, params=None):
+ """Returns a detailed list of images filtered by any parameters"""
+ url = 'images/detail'
+ if params:
+ param_list = urllib.urlencode(params)
+
+ url = "images/detail?" + param_list
+
+ resp, body = self.get(url, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['images']
+
+ def get_image(self, image_id):
+ """Returns the details of a single image"""
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+ body = self._parse_image(etree.fromstring(body))
+ return resp, body
+
+ def delete_image(self, image_id):
+ """Deletes the provided image"""
+ return self.delete("images/%s" % str(image_id), self.headers)
+
+ def wait_for_image_resp_code(self, image_id, code):
+ """
+ Waits until the HTTP response code for the request matches the
+ expected value
+ """
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+ start = int(time.time())
+
+ while resp.status != code:
+ time.sleep(self.build_interval)
+ resp, body = self.get("images/%s" % str(image_id), self.headers)
+
+ if int(time.time()) - start >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ def wait_for_image_status(self, image_id, status):
+ """Waits for an image to reach a given status."""
+ resp, image = self.get_image(image_id)
+ start = int(time.time())
+
+ while image['status'] != status:
+ time.sleep(self.build_interval)
+ resp, image = self.get_image(image_id)
+ if image['status'] == 'ERROR':
+ raise exceptions.AddImageException(image_id=image_id)
+
+ if int(time.time()) - start >= self.build_timeout:
+ raise exceptions.TimeoutException
+
+ def list_image_metadata(self, image_id):
+ """Lists all metadata items for an image"""
+ resp, body = self.get("images/%s/metadata" % str(image_id),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def set_image_metadata(self, image_id, meta):
+ """Sets the metadata for an image"""
+ post_body = json.dumps({'metadata': meta})
+ resp, body = self.put('images/%s/metadata' % str(image_id),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def update_image_metadata(self, image_id, meta):
+ """Updates the metadata for an image"""
+ post_body = Element('metadata', meta)
+ for k, v in meta:
+ metadata = Element('meta', key=k)
+ text = Text(v)
+ metadata.append(text)
+ post_body.append(metadata)
+
+ resp, body = self.post('images/%s/metadata' % str(image_id),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['metadata']
+
+ def get_image_metadata_item(self, image_id, key):
+ """Returns the value for a specific image metadata key"""
+ resp, body = self.get("images/%s/metadata/%s.xml" %
+ (str(image_id), key), self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['meta']
+
+ def set_image_metadata_item(self, image_id, key, meta):
+ """Sets the value for a specific image metadata key"""
+ post_body = json.dumps({'meta': meta})
+ resp, body = self.put('images/%s/metadata/%s' % (str(image_id), key),
+ post_body, self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body['meta']
+
+ def delete_image_metadata_item(self, image_id, key):
+ """Deletes a single image metadata key/value pair"""
+ resp, body = self.delete("images/%s/metadata/%s" % (str(image_id), key,
+ self.headers))
+ return resp, body
diff --git a/tempest/services/nova/xml/volumes_extensions_client.py b/tempest/services/nova/xml/volumes_extensions_client.py
new file mode 100644
index 0000000..fffea44
--- /dev/null
+++ b/tempest/services/nova/xml/volumes_extensions_client.py
@@ -0,0 +1,146 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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.
+
+
+import time
+from lxml import etree
+
+from tempest import exceptions
+from tempest.common.rest_client import RestClientXML
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import XMLNS_11
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import Document
+
+
+class VolumesExtensionsClientXML(RestClientXML):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(VolumesExtensionsClientXML, self).__init__(config,
+ username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_volume(self, body):
+ vol = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ if tag == 'metadata':
+ vol['metadata'] = dict((meta.get('key'),
+ meta.text) for meta in list(child))
+ else:
+ vol[tag] = xml_to_json(child)
+ return vol
+
+ def list_volumes(self, params=None):
+ """List all the volumes created"""
+ url = 'os-volumes'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ return resp, volumes
+
+ def list_volumes_with_detail(self, params=None):
+ """List all the details of volumes"""
+ url = 'os-volumes/detail'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ return resp, volumes
+
+ def get_volume(self, volume_id):
+ """Returns the details of a single volume"""
+ url = "os-volumes/%s" % str(volume_id)
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume(body)
+
+ def create_volume(self, size, display_name=None, metadata=None):
+ """Creates a new Volume.
+
+ :param size: Size of volume in GB. (Required)
+ :param display_name: Optional Volume Name.
+ :param metadata: An optional dictionary of values for metadata.
+ """
+ volume = Element("volume",
+ xmlns=XMLNS_11,
+ size=size)
+ if display_name:
+ volume.add_attr('display_name', display_name)
+
+ if metadata:
+ _metadata = Element('metadata')
+ volume.append(_metadata)
+ for key, value in metadata.items():
+ meta = Element('meta')
+ meta.add_attr('key', key)
+ meta.append(Text(value))
+ _metadata.append(meta)
+
+ resp, body = self.post('os-volumes', str(Document(volume)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume(self, volume_id):
+ """Deletes the Specified Volume"""
+ return self.delete("os-volumes/%s" % str(volume_id))
+
+ def wait_for_volume_status(self, volume_id, status):
+ """Waits for a Volume to reach a given status"""
+ resp, body = self.get_volume(volume_id)
+ volume_name = body['displayName']
+ volume_status = body['status']
+ start = int(time.time())
+
+ while volume_status != status:
+ time.sleep(self.build_interval)
+ resp, body = self.get_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 within '\
+ 'the required time (%s s).' % (volume_name, status,
+ self.build_timeout)
+ raise exceptions.TimeoutException(message)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume(id)
+ except exceptions.NotFound:
+ return True
+ return False
diff --git a/tempest/services/volume/json/volumes_client.py b/tempest/services/volume/json/volumes_client.py
index 863d9a8..3a42d51 100644
--- a/tempest/services/volume/json/volumes_client.py
+++ b/tempest/services/volume/json/volumes_client.py
@@ -22,13 +22,13 @@
from tempest import exceptions
-class VolumesClient(RestClient):
+class VolumesClientJSON(RestClient):
"""
Client class to send CRUD Volume API requests to a Cinder endpoint
"""
def __init__(self, config, username, password, auth_url, tenant_name=None):
- super(VolumesClient, self).__init__(config, username, password,
+ super(VolumesClientJSON, self).__init__(config, username, password,
auth_url, tenant_name)
self.service = self.config.volume.catalog_type
diff --git a/tempest/services/volume/xml/__init__.py b/tempest/services/volume/xml/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/volume/xml/__init__.py
diff --git a/tempest/services/volume/xml/volumes_client.py b/tempest/services/volume/xml/volumes_client.py
new file mode 100644
index 0000000..8bb8bff
--- /dev/null
+++ b/tempest/services/volume/xml/volumes_client.py
@@ -0,0 +1,148 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+# Copyright 2012 IBM
+# 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.
+
+import time
+
+from lxml import etree
+
+from tempest.common.rest_client import RestClientXML
+from tempest import exceptions
+from tempest.services.nova.xml.common import xml_to_json
+from tempest.services.nova.xml.common import XMLNS_11
+from tempest.services.nova.xml.common import Element
+from tempest.services.nova.xml.common import Text
+from tempest.services.nova.xml.common import Document
+
+
+class VolumesClientXML(RestClientXML):
+ """
+ Client class to send CRUD Volume API requests to a Cinder endpoint
+ """
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(VolumesClientXML, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.compute.catalog_type
+ self.build_interval = self.config.compute.build_interval
+ self.build_timeout = self.config.compute.build_timeout
+
+ def _parse_volume(self, body):
+ vol = dict((attr, body.get(attr)) for attr in body.keys())
+
+ for child in body.getchildren():
+ tag = child.tag
+ if tag.startswith("{"):
+ ns, tag = tag.split("}", 1)
+ if tag == 'metadata':
+ vol['metadata'] = dict((meta.get('key'),
+ meta.text) for meta in list(child))
+ else:
+ vol[tag] = xml_to_json(child)
+ return vol
+
+ def list_volumes(self, params=None):
+ """List all the volumes created"""
+ url = 'volumes'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ return resp, volumes
+
+ def list_volumes_with_detail(self, params=None):
+ """List all the details of volumes"""
+ url = 'volumes/detail'
+
+ if params:
+ url += '?%s' % urllib.urlencode(params)
+
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ volumes = []
+ if body is not None:
+ volumes += [self._parse_volume(vol) for vol in list(body)]
+ return resp, volumes
+
+ def get_volume(self, volume_id):
+ """Returns the details of a single volume"""
+ url = "volumes/%s" % str(volume_id)
+ resp, body = self.get(url, self.headers)
+ body = etree.fromstring(body)
+ return resp, self._parse_volume(body)
+
+ def create_volume(self, size, display_name=None, metadata=None):
+ """Creates a new Volume.
+
+ :param size: Size of volume in GB. (Required)
+ :param display_name: Optional Volume Name.
+ :param metadata: An optional dictionary of values for metadata.
+ """
+ volume = Element("volume",
+ xmlns=XMLNS_11,
+ size=size)
+ if display_name:
+ volume.add_attr('display_name', display_name)
+
+ if metadata:
+ _metadata = Element('metadata')
+ volume.append(_metadata)
+ for key, value in metadata.items():
+ meta = Element('meta')
+ meta.add_attr('key', key)
+ meta.append(Text(value))
+ _metadata.append(meta)
+
+ resp, body = self.post('volumes', str(Document(volume)),
+ self.headers)
+ body = xml_to_json(etree.fromstring(body))
+ return resp, body
+
+ def delete_volume(self, volume_id):
+ """Deletes the Specified Volume"""
+ return self.delete("volumes/%s" % str(volume_id))
+
+ def wait_for_volume_status(self, volume_id, status):
+ """Waits for a Volume to reach a given status"""
+ resp, body = self.get_volume(volume_id)
+ volume_name = body['displayName']
+ volume_status = body['status']
+ start = int(time.time())
+
+ while volume_status != status:
+ time.sleep(self.build_interval)
+ resp, body = self.get_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 within '\
+ 'the required time (%s s).' % (volume_name, status,
+ self.build_timeout)
+ raise exceptions.TimeoutException(message)
+
+ def is_resource_deleted(self, id):
+ try:
+ self.get_volume(id)
+ except exceptions.NotFound:
+ return True
+ return False
diff --git a/tempest/test.py b/tempest/test.py
index 4bb8ca4..dc480d1 100644
--- a/tempest/test.py
+++ b/tempest/test.py
@@ -62,6 +62,29 @@
del self.resource_keys[key]
+def call_until_true(func, duration, sleep_for):
+ """
+ Call the given function until it returns True (and return True) or
+ until the specified duration (in seconds) elapses (and return
+ False).
+
+ :param func: A zero argument callable that returns True on success.
+ :param duration: The number of seconds for which to attempt a successful
+ call of the function.
+ :param sleep_for: The number of seconds to sleep after an unsuccessful
+ invocation of the function.
+ """
+ now = time.time()
+ timeout = now + duration
+ while now < timeout:
+ if func():
+ return True
+ LOG.debug("Sleeping for %d seconds", sleep_for)
+ time.sleep(sleep_for)
+ now = time.time()
+ return False
+
+
class DefaultClientTest(TestCase):
"""
@@ -78,10 +101,7 @@
expected status to show. At any time, if the returned
status of the thing is ERROR, fail out.
"""
- now = time.time()
- timeout = now + self.config.compute.build_timeout
- sleep_for = self.config.compute.build_interval
- while now < timeout:
+ def check_status():
# python-novaclient has resources available to its client
# that all implement a get() method taking an identifier
# for the singular resource to retrieve.
@@ -92,13 +112,15 @@
"In ERROR state."
% thing)
elif new_status == expected_status:
- return # All good.
+ return True # All good.
LOG.debug("Waiting for %s to get to %s status. "
"Currently in %s status",
thing, expected_status, new_status)
- LOG.debug("Sleeping for %d seconds", sleep_for)
- self.fail("Timed out waiting for thing %s to become %s"
- % (thing_id, expected_status))
+ if not call_until_true(check_status,
+ self.config.compute.build_timeout,
+ self.config.compute.build_interval):
+ self.fail("Timed out waiting for thing %s to become %s"
+ % (thing_id, expected_status))
class ComputeFuzzClientTest(TestCase):
@@ -132,10 +154,7 @@
resp, server = client.create_server('random_server')
self.status_timeout(client.get_server, server['id'], 'ACTIVE')
"""
- now = time.time()
- timeout = now + self.config.compute.build_timeout
- sleep_for = self.config.compute.build_interval
- while now < timeout:
+ def check_status():
# Tempest REST client has resources available to its client
# that all implement a various get_$resource() methods taking
# an identifier for the singular resource to retrieve.
@@ -146,10 +165,12 @@
"In ERROR state."
% thing)
elif new_status == expected_status:
- return # All good.
+ return True # All good.
LOG.debug("Waiting for %s to get to %s status. "
"Currently in %s status",
thing, expected_status, new_status)
- LOG.debug("Sleeping for %d seconds", sleep_for)
- self.fail("Timed out waiting for thing %s to become %s"
- % (thing_id, expected_status))
+ if not call_until_true(check_status,
+ self.config.compute.build_timeout,
+ self.config.compute.build_interval):
+ self.fail("Timed out waiting for thing %s to become %s"
+ % (thing_id, expected_status))
diff --git a/tempest/tests/compute/base.py b/tempest/tests/compute/base.py
index 2617fbb..f36c8f2 100644
--- a/tempest/tests/compute/base.py
+++ b/tempest/tests/compute/base.py
@@ -26,7 +26,6 @@
from tempest import openstack
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.services.identity.json.admin_client import AdminClient
__all__ = ['BaseComputeTest', 'BaseComputeTestJSON', 'BaseComputeTestXML',
'BaseComputeAdminTestJSON', 'BaseComputeAdminTestXML']
@@ -34,7 +33,7 @@
LOG = logging.getLogger(__name__)
-class BaseComputeTest(unittest.TestCase):
+class BaseCompTest(unittest.TestCase):
"""Base test case class for all Compute API tests"""
@@ -79,12 +78,8 @@
"""
Returns an instance of the Identity Admin API client
"""
- client_args = (cls.config,
- cls.config.identity_admin.username,
- cls.config.identity_admin.password,
- cls.config.identity.auth_url)
- tenant_name = cls.config.identity_admin.tenant_name
- admin_client = AdminClient(*client_args, tenant_name=tenant_name)
+ os = openstack.IdentityManager(interface=cls._interface)
+ admin_client = os.admin_client
return admin_client
@classmethod
@@ -206,7 +201,7 @@
time.sleep(self.build_interval)
-class BaseComputeTestJSON(BaseComputeTest):
+class BaseComputeTestJSON(BaseCompTest):
@classmethod
def setUpClass(cls):
cls._interface = "json"
@@ -216,7 +211,7 @@
BaseComputeTest = BaseComputeTestJSON
-class BaseComputeTestXML(BaseComputeTest):
+class BaseComputeTestXML(BaseCompTest):
@classmethod
def setUpClass(cls):
cls._interface = "xml"
diff --git a/tempest/tests/compute/test_floating_ips_actions.py b/tempest/tests/compute/test_floating_ips_actions.py
index afd0a3f..2f7532b 100644
--- a/tempest/tests/compute/test_floating_ips_actions.py
+++ b/tempest/tests/compute/test_floating_ips_actions.py
@@ -21,16 +21,15 @@
from tempest import openstack
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.compute.base import BaseComputeTest
+from tempest.tests.compute import base
-class FloatingIPsTest(BaseComputeTest):
+class FloatingIPsTestBase(object):
server_id = None
floating_ip = None
- @classmethod
+ @staticmethod
def setUpClass(cls):
- super(FloatingIPsTest, cls).setUpClass()
cls.client = cls.floating_ips_client
cls.servers_client = cls.servers_client
@@ -45,7 +44,7 @@
resp, body = cls.client.create_floating_ip()
cls.floating_ip_id = body['id']
cls.floating_ip = body['ip']
- #Generating a nonexistant floatingIP id
+ #Generating a nonexistent floatingIP id
cls.floating_ip_ids = []
resp, body = cls.client.list_floating_ips()
for i in range(len(body)):
@@ -55,19 +54,18 @@
if cls.non_exist_id not in cls.floating_ip_ids:
break
- @classmethod
+ @staticmethod
def tearDownClass(cls):
#Deleting the server which is created in this method
resp, body = cls.servers_client.delete_server(cls.server_id)
#Deleting the floating IP which is created in this method
resp, body = cls.client.delete_floating_ip(cls.floating_ip_id)
- super(FloatingIPsTest, cls).tearDownClass()
@attr(type='positive')
def test_allocate_floating_ip(self):
"""
Positive test:Allocation of a new floating IP to a project
- should be succesfull
+ should be successful
"""
try:
resp, body = self.client.create_floating_ip()
@@ -86,7 +84,7 @@
def test_delete_floating_ip(self):
"""
Positive test:Deletion of valid floating IP from project
- should be succesfull
+ should be successful
"""
#Creating the floating IP that is to be deleted in this method
resp, floating_ip_body = self.client.create_floating_ip()
@@ -103,7 +101,7 @@
def test_associate_disassociate_floating_ip(self):
"""
Positive test:Associate and disassociate the provided floating IP to a
- specific server should be successfull
+ specific server should be successful
"""
#Association of floating IP to fixed IP address
resp, body =\
@@ -120,25 +118,25 @@
def test_delete_nonexistant_floating_ip(self):
"""
- Negative test:Deletion of a nonexistant floating IP
+ Negative test:Deletion of a nonexistent floating IP
from project should fail
"""
- #Deleting the non existant floating IP
+ #Deleting the non existent floating IP
try:
resp, body = self.client.delete_floating_ip(self.non_exist_id)
except:
pass
else:
- self.fail('Should not be able to delete a nonexistant floating IP')
+ self.fail('Should not be able to delete a nonexistent floating IP')
@unittest.skip("Skipped until the Bug #957706 is resolved")
@attr(type='negative')
def test_associate_nonexistant_floating_ip(self):
"""
- Negative test:Association of a non existant floating IP
+ Negative test:Association of a non existent floating IP
to specific server should fail
"""
- #Associating non existant floating IP
+ #Associating non existent floating IP
try:
resp, body = \
self.client.associate_floating_ip_to_server("0.0.0.0",
@@ -147,14 +145,14 @@
pass
else:
self.fail('Should not be able to associate'
- ' a nonexistant floating IP')
+ ' a nonexistent floating IP')
@attr(type='negative')
def test_dissociate_nonexistant_floating_ip(self):
"""
- Negative test:Dissociation of a non existant floating IP should fail
+ Negative test:Dissociation of a non existent floating IP should fail
"""
- #Dissociating non existant floating IP
+ #Dissociating non existent floating IP
try:
resp, body = \
self.client.disassociate_floating_ip_from_server("0.0.0.0",
@@ -163,14 +161,13 @@
pass
else:
self.fail('Should not be able to dissociate'
- ' a nonexistant floating IP')
+ ' a nonexistent floating IP')
- @unittest.skip("Skipped until the Bug #1029911 is resolved")
- @attr(type='negative')
+ @attr(type='positive')
def test_associate_already_associated_floating_ip(self):
"""
- Negative test:Association of an already associated floating IP
- to specific server should raise BadRequest exception
+ positive test:Association of an already associated floating IP
+ to specific server should change the association of the Floating IP
"""
#Create server so as to use for Multiple association
resp, body = self.servers_client.create_server('floating-server2',
@@ -180,34 +177,32 @@
self.new_server_id = body['id']
#Associating floating IP for the first time
- try:
- resp, _ = \
- self.client.associate_floating_ip_to_server(self.floating_ip,
- self.server_id)
+ resp, _ = \
+ self.client.associate_floating_ip_to_server(self.floating_ip,
+ self.server_id)
#Associating floating IP for the second time
- resp = {}
- resp['status'] = None
- resp, body = \
- self.client.associate_floating_ip_to_server(self.floating_ip,
- self.new_server_id)
- except exceptions.BadRequest:
+ resp, body = \
+ self.client.associate_floating_ip_to_server(self.floating_ip,
+ self.new_server_id)
+
+ #Make sure no longer associated with old server
+ try:
+ self.client.disassociate_floating_ip_from_server(\
+ self.floating_ip,
+ self.server_id)
+ except exceptions.NotFound:
pass
else:
- self.fail('Association of an already associated floating IP'
- ' to specific server should raise BadRequest')
- finally:
- if (resp['status'] != None):
- #Dissociation of the floating IP associated in this method
- resp, _ = \
- self.client.disassociate_floating_ip_from_server(\
- self.floating_ip,
- self.new_server_id)
+ self.fail('The floating IP should be associated to the second'
+ 'server')
+ if (resp['status'] != None):
#Dissociation of the floating IP associated in this method
resp, _ = \
- self.client.disassociate_floating_ip_from_server(self.floating_ip,
- self.server_id)
- #Deletion of server created in this method
- resp, body = self.servers_client.delete_server(self.new_server_id)
+ self.client.disassociate_floating_ip_from_server(\
+ self.floating_ip,
+ self.new_server_id)
+ #Deletion of server created in this method
+ resp, body = self.servers_client.delete_server(self.new_server_id)
@unittest.skip("Skipped until the Bug #957706 is resolved")
@attr(type='negative')
@@ -225,3 +220,29 @@
else:
self.fail('Association of floating IP to specific server'
' with out passing floating IP should raise BadRequest')
+
+
+class FloatingIPsTestJSON(base.BaseComputeTestJSON,
+ FloatingIPsTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(FloatingIPsTestJSON, cls).setUpClass()
+ FloatingIPsTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ FloatingIPsTestBase.tearDownClass(cls)
+ super(FloatingIPsTestJSON, cls).tearDownClass()
+
+
+class FloatingIPsTestXML(base.BaseComputeTestXML,
+ FloatingIPsTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(FloatingIPsTestXML, cls).setUpClass()
+ FloatingIPsTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ FloatingIPsTestBase.tearDownClass(cls)
+ super(FloatingIPsTestXML, cls).tearDownClass()
diff --git a/tempest/tests/compute/test_images.py b/tempest/tests/compute/test_images.py
index 694965f..94bab4f 100644
--- a/tempest/tests/compute/test_images.py
+++ b/tempest/tests/compute/test_images.py
@@ -203,29 +203,27 @@
" characters")
@attr(type='negative')
- @unittest.skip("Until Bug 1005397 is fixed")
def test_create_image_specify_uuid_35_characters_or_less(self):
"""Return an error if Image ID passed is 35 characters or less"""
try:
snapshot_name = rand_name('test-snap-')
test_uuid = ('a' * 35)
- self.assertRaises(exceptions.BadRequest, self.client.create_image,
+ self.assertRaises(exceptions.NotFound, self.client.create_image,
test_uuid, snapshot_name)
except:
- self.fail("Should return 400 Bad Request if server uuid is 35"
+ self.fail("Should return 404 Not Found if server uuid is 35"
" characters or less")
@attr(type='negative')
- @unittest.skip("Until Bug 1005397 is fixed")
def test_create_image_specify_uuid_37_characters_or_more(self):
"""Return an error if Image ID passed is 37 characters or more"""
try:
snapshot_name = rand_name('test-snap-')
test_uuid = ('a' * 37)
- self.assertRaises(exceptions.BadRequest, self.client.create_image,
+ self.assertRaises(exceptions.NotFound, self.client.create_image,
test_uuid, snapshot_name)
except:
- self.fail("Should return 400 Bad Request if server uuid is 37"
+ self.fail("Should return 404 Not Found if server uuid is 37"
" characters or more")
@attr(type='negative')
diff --git a/tempest/tests/compute/test_list_floating_ips.py b/tempest/tests/compute/test_list_floating_ips.py
index d2d9ba2..ac70b8f 100644
--- a/tempest/tests/compute/test_list_floating_ips.py
+++ b/tempest/tests/compute/test_list_floating_ips.py
@@ -20,14 +20,13 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.compute.base import BaseComputeTest
+from tempest.tests.compute import base
-class FloatingIPDetailsTest(BaseComputeTest):
+class FloatingIPDetailsTestBase(object):
- @classmethod
+ @staticmethod
def setUpClass(cls):
- super(FloatingIPDetailsTest, cls).setUpClass()
cls.client = cls.floating_ips_client
cls.floating_ip = []
cls.floating_ip_id = []
@@ -37,11 +36,10 @@
cls.floating_ip.append(body)
cls.floating_ip_id.append(body['id'])
- @classmethod
+ @staticmethod
def tearDownClass(cls):
for i in range(3):
cls.client.delete_floating_ip(cls.floating_ip_id[i])
- super(FloatingIPDetailsTest, cls).tearDownClass()
@attr(type='positive')
def test_list_floating_ips(self):
@@ -101,3 +99,29 @@
else:
self.fail('Should not be able to GET the details from a'
'nonexistant floating IP')
+
+
+class FloatingIPDetailsTestJSON(base.BaseComputeTestJSON,
+ FloatingIPDetailsTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(FloatingIPDetailsTestJSON, cls).setUpClass()
+ FloatingIPDetailsTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ FloatingIPDetailsTestBase.tearDownClass(cls)
+ super(FloatingIPDetailsTestJSON, cls).tearDownClass()
+
+
+class FloatingIPDetailsTestXML(base.BaseComputeTestXML,
+ FloatingIPDetailsTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(FloatingIPDetailsTestXML, cls).setUpClass()
+ FloatingIPDetailsTestBase.setUpClass(cls)
+
+ @classmethod
+ def tearDownClass(cls):
+ FloatingIPDetailsTestBase.tearDownClass(cls)
+ super(FloatingIPDetailsTestXML, cls).tearDownClass()
diff --git a/tempest/tests/compute/test_server_actions.py b/tempest/tests/compute/test_server_actions.py
index edcf42b..e0bb47b 100644
--- a/tempest/tests/compute/test_server_actions.py
+++ b/tempest/tests/compute/test_server_actions.py
@@ -185,16 +185,15 @@
raise exceptions.TimeoutException(message)
@attr(type='negative')
- def test_reboot_nonexistant_server_soft(self):
+ def test_reboot_nonexistent_server_soft(self):
"""
- Negative Test: The server reboot on non existant server should return
+ Negative Test: The server reboot on non existent server should return
an error
"""
self.assertRaises(exceptions.NotFound, self.client.reboot, 999, 'SOFT')
- @unittest.skip('Until bug 963248 is fixed.')
@attr(type='negative')
- def test_rebuild_nonexistant_server(self):
+ def test_rebuild_nonexistent_server(self):
"""
Negative test: The server rebuild for a non existing server should not
be allowed
diff --git a/tempest/tests/compute/test_volumes_get.py b/tempest/tests/compute/test_volumes_get.py
index cda943d..fcdec96 100644
--- a/tempest/tests/compute/test_volumes_get.py
+++ b/tempest/tests/compute/test_volumes_get.py
@@ -18,21 +18,16 @@
from nose.plugins.attrib import attr
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.compute.base import BaseComputeTest
+from tempest.tests.compute import base
-class VolumesGetTest(BaseComputeTest):
-
- @classmethod
- def setUpClass(cls):
- super(VolumesGetTest, cls).setUpClass()
- cls.client = cls.volumes_extensions_client
+class VolumesGetTestBase(object):
@attr(type='smoke')
def test_volume_create_get_delete(self):
"""CREATE, GET, DELETE Volume"""
try:
- v_name = rand_name('Volume-')
+ v_name = rand_name('Volume-%s-') % self._interface
metadata = {'Type': 'work'}
#Create volume
resp, volume = self.client.create_volume(size=1,
@@ -93,3 +88,21 @@
#Delete the Volume created in this method
resp, _ = self.client.delete_volume(volume['id'])
self.assertEqual(202, resp.status)
+ #Checking if the deleted Volume still exists
+ self.client.wait_for_resource_deletion(volume['id'])
+
+
+class VolumesGetTestXML(base.BaseComputeTestXML, VolumesGetTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(VolumesGetTestXML, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
+
+
+class VolumesGetTestJSON(base.BaseComputeTestJSON, VolumesGetTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(VolumesGetTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
diff --git a/tempest/tests/compute/test_volumes_list.py b/tempest/tests/compute/test_volumes_list.py
index 679a23b..3f03996 100644
--- a/tempest/tests/compute/test_volumes_list.py
+++ b/tempest/tests/compute/test_volumes_list.py
@@ -18,10 +18,10 @@
import nose
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.compute.base import BaseComputeTest
+from tempest.tests.compute import base
-class VolumesListTest(BaseComputeTest):
+class VolumesTestBase(object):
"""
This test creates a number of 1G volumes. To run successfully,
@@ -31,9 +31,40 @@
VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
"""
+ def test_volume_list(self):
+ """Should return the list of Volumes"""
+ # Fetch all Volumes
+ resp, fetched_list = self.client.list_volumes()
+ self.assertEqual(200, resp.status)
+ # Now check if all the Volumes created in setup are in fetched list
+ missing_volumes = \
+ [v for v in self.volume_list if v not in fetched_list]
+
+ self.assertFalse(missing_volumes,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['displayName']
+ for m_vol in missing_volumes))
+
+ def test_volume_list_with_details(self):
+ """Should return the list of Volumes with details"""
+ #Fetch all Volumes
+ resp, fetched_list = self.client.list_volumes_with_detail()
+ self.assertEqual(200, resp.status)
+ #Now check if all the Volumes created in setup are in fetched list
+ missing_volumes = \
+ [v for v in self.volume_list if v not in fetched_list]
+
+ self.assertFalse(missing_volumes,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['displayName']
+ for m_vol in missing_volumes))
+
+
+class VolumesTestXML(base.BaseComputeTestXML, VolumesTestBase):
@classmethod
def setUpClass(cls):
- super(VolumesListTest, cls).setUpClass()
+ cls._interface = 'xml'
+ super(VolumesTestXML, cls).setUpClass()
cls.client = cls.volumes_extensions_client
# Create 3 Volumes
cls.volume_list = list()
@@ -41,10 +72,11 @@
for i in range(3):
v_name = rand_name('volume')
metadata = {'Type': 'work'}
+ v_name += cls._interface
try:
resp, volume = cls.client.create_volume(size=1,
- display_name=v_name,
- metadata=metadata)
+ display_name=v_name,
+ metadata=metadata)
cls.client.wait_for_volume_status(volume['id'],
'available')
resp, volume = cls.client.get_volume(volume['id'])
@@ -71,30 +103,52 @@
# Delete the created Volumes
for volume in cls.volume_list:
resp, _ = cls.client.delete_volume(volume['id'])
- super(VolumesListTest, cls).tearDownClass()
+ cls.client.wait_for_resource_deletion(volume['id'])
+ super(VolumesTestXML, cls).tearDownClass()
- def test_volume_list(self):
- """Should return the list of Volumes"""
- # Fetch all Volumes
- resp, fetched_list = self.client.list_volumes()
- self.assertEqual(200, resp.status)
- # Now check if all the Volumes created in setup are in fetched list
- missing_volumes =\
- [v for v in self.volume_list if v not in fetched_list]
- self.assertFalse(missing_volumes,
- "Failed to find volume %s in fetched list"
- % ', '.join(m_vol['displayName']
- for m_vol in missing_volumes))
- def test_volume_list_with_details(self):
- """Should return the list of Volumes with details"""
- #Fetch all Volumes
- resp, fetched_list = self.client.list_volumes_with_detail()
- self.assertEqual(200, resp.status)
- #Now check if all the Volumes created in setup are in fetched list
- missing_volumes =\
- [v for v in self.volume_list if v not in fetched_list]
- self.assertFalse(missing_volumes,
- "Failed to find volume %s in fetched list"
- % ', '.join(m_vol['displayName']
- for m_vol in missing_volumes))
+class VolumesTestJSON(base.BaseComputeTestJSON, VolumesTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = 'json'
+ super(VolumesTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
+ # Create 3 Volumes
+ cls.volume_list = []
+ cls.volume_id_list = []
+ for i in range(3):
+ v_name = rand_name('volume-%s')
+ metadata = {'Type': 'work'}
+ v_name += cls._interface
+ try:
+ resp, volume = cls.client.create_volume(size=1,
+ display_name=v_name,
+ metadata=metadata)
+ cls.client.wait_for_volume_status(volume['id'],
+ 'available')
+ resp, volume = cls.client.get_volume(volume['id'])
+ cls.volume_list.append(volume)
+ cls.volume_id_list.append(volume['id'])
+ except:
+ if cls.volume_list:
+ # We could not create all the volumes, though we were able
+ # to create *some* of the volumes. This is typically
+ # because the backing file size of the volume group is
+ # too small. So, here, we clean up whatever we did manage
+ # to create and raise a SkipTest
+ for volume in cls.volume_list:
+ cls.client.delete_volume(volume)
+ msg = ("Failed to create ALL necessary volumes to run "
+ "test. This typically means that the backing file "
+ "size of the nova-volumes group is too small to "
+ "create the 3 volumes needed by this test case")
+ raise nose.SkipTest(msg)
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ # Delete the created Volumes
+ for volume in cls.volume_list:
+ resp, _ = cls.client.delete_volume(volume['id'])
+ cls.client.wait_for_resource_deletion(volume['id'])
+ super(VolumesTestJSON, cls).tearDownClass()
diff --git a/tempest/tests/compute/test_volumes_negative.py b/tempest/tests/compute/test_volumes_negative.py
index ea2811c..5c93f07 100644
--- a/tempest/tests/compute/test_volumes_negative.py
+++ b/tempest/tests/compute/test_volumes_negative.py
@@ -20,15 +20,10 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.compute.base import BaseComputeTest
+from tempest.tests.compute import base
-class VolumesNegativeTest(BaseComputeTest):
-
- @classmethod
- def setUpClass(cls):
- super(VolumesNegativeTest, cls).setUpClass()
- cls.client = cls.volumes_extensions_client
+class VolumesNegativeTestBase(object):
@attr(type='negative')
def test_volume_get_nonexistant_volume_id(self):
@@ -140,3 +135,21 @@
Negative: Should not be able to delete volume when empty ID is passed
"""
resp, volume = self.client.delete_volume('')
+
+
+class VolumesNegativeTestXML(base.BaseComputeTestXML,
+ VolumesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(VolumesNegativeTestXML, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
+
+
+class VolumesNegativeTestJSON(base.BaseComputeTestJSON,
+ VolumesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(VolumesNegativeTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_extensions_client
diff --git a/tempest/tests/identity/admin/test_roles.py b/tempest/tests/identity/admin/test_roles.py
index 813f64a..e0b180b 100644
--- a/tempest/tests/identity/admin/test_roles.py
+++ b/tempest/tests/identity/admin/test_roles.py
@@ -19,14 +19,13 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.identity.base import BaseIdentityAdminTest
+from tempest.tests.identity import base
-class RolesTest(BaseIdentityAdminTest):
+class RolesTestBase(object):
- @classmethod
+ @staticmethod
def setUpClass(cls):
- super(RolesTest, cls).setUpClass()
for _ in xrange(5):
resp, role = cls.client.create_role(rand_name('role-'))
@@ -101,11 +100,25 @@
self.client.delete_role(role1_id)
-class UserRolesTest(RolesTest):
+class RolesTestJSON(base.BaseIdentityAdminTestJSON,
+ RolesTestBase):
@classmethod
def setUpClass(cls):
- super(UserRolesTest, cls).setUpClass()
+ super(RolesTestJSON, cls).setUpClass()
+ RolesTestBase.setUpClass(cls)
+
+
+class RolesTestXML(base.BaseIdentityAdminTestXML,
+ RolesTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(RolesTestXML, cls).setUpClass()
+ RolesTestBase.setUpClass(cls)
+
+
+class UserRolesTestBase(RolesTestBase):
def test_assign_user_role(self):
"""Assign a role to a user on a tenant"""
@@ -249,3 +262,19 @@
(user, tenant, role) = self._get_role_params()
self.assertRaises(exceptions.NotFound, self.client.list_user_roles,
tenant['id'], 'junk-role-aabbcc11')
+
+
+class UserRolesTestJSON(RolesTestJSON,
+ UserRolesTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(UserRolesTestJSON, cls).setUpClass()
+
+
+class UserRolesTestXML(RolesTestXML,
+ UserRolesTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(UserRolesTestXML, cls).setUpClass()
diff --git a/tempest/tests/identity/admin/test_services.py b/tempest/tests/identity/admin/test_services.py
index d107a6e..fa4178a 100644
--- a/tempest/tests/identity/admin/test_services.py
+++ b/tempest/tests/identity/admin/test_services.py
@@ -20,10 +20,10 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.identity.base import BaseIdentityAdminTest
+from tempest.tests.identity import base
-class ServicesTest(BaseIdentityAdminTest):
+class ServicesTestBase(object):
def test_create_get_delete_service(self):
"""GET Service"""
@@ -64,3 +64,17 @@
#Checking whether service is deleted successfully
self.assertRaises(exceptions.NotFound, self.client.get_service,
service_data['id'])
+
+
+class ServicesTestJSON(base.BaseIdentityAdminTestJSON,
+ ServicesTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(ServicesTestJSON, cls).setUpClass()
+
+
+class ServicesTestXML(base.BaseIdentityAdminTestXML,
+ ServicesTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(ServicesTestXML, cls).setUpClass()
diff --git a/tempest/tests/identity/admin/test_tenants.py b/tempest/tests/identity/admin/test_tenants.py
index 253e52e..d8b6c33 100644
--- a/tempest/tests/identity/admin/test_tenants.py
+++ b/tempest/tests/identity/admin/test_tenants.py
@@ -19,15 +19,13 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.identity.base import BaseIdentityAdminTest
+from tempest.tests.identity import base
-class TenantsTest(BaseIdentityAdminTest):
+class TenantsTestBase(object):
- @classmethod
+ @staticmethod
def setUpClass(cls):
- super(TenantsTest, cls).setUpClass()
-
for _ in xrange(5):
resp, tenant = cls.client.create_tenant(rand_name('tenant-'))
cls.data.tenants.append(tenant)
@@ -133,10 +131,12 @@
st1 = resp['status']
en1 = body['enabled']
self.assertTrue(st1.startswith('2'))
- self.assertFalse(en1, 'Enable should be False in response')
+ self.assertEqual('false', str(en1).lower(),
+ 'Enable should be False in response')
resp, body = self.client.get_tenant(tenant_id)
en2 = body['enabled']
- self.assertFalse(en2, 'Enable should be False in lookup')
+ self.assertEqual('false', str(en2).lower(),
+ 'Enable should be False in lookup')
self.client.delete_tenant(tenant_id)
def test_tenant_create_duplicate(self):
@@ -246,7 +246,25 @@
resp3_en = body['enabled']
self.assertNotEqual(resp1_en, resp3_en)
- self.assertEqual(t_en, resp1_en)
+ self.assertEqual('false', str(resp1_en).lower())
self.assertEqual(resp2_en, resp3_en)
self.client.delete_tenant(t_id)
+
+
+class TenantsTestJSON(base.BaseIdentityAdminTestJSON,
+ TenantsTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TenantsTestJSON, cls).setUpClass()
+ TenantsTestBase.setUpClass(cls)
+
+
+class TenantsTestXML(base.BaseIdentityAdminTestXML,
+ TenantsTestBase):
+
+ @classmethod
+ def setUpClass(cls):
+ super(TenantsTestXML, cls).setUpClass()
+ TenantsTestBase.setUpClass(cls)
diff --git a/tempest/tests/identity/admin/test_users.py b/tempest/tests/identity/admin/test_users.py
index 1e1b752..07c032e 100644
--- a/tempest/tests/identity/admin/test_users.py
+++ b/tempest/tests/identity/admin/test_users.py
@@ -20,10 +20,10 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.identity.base import BaseIdentityAdminTest
+from tempest.tests.identity import base
-class UsersTest(BaseIdentityAdminTest):
+class UsersTestBase(object):
alt_user = rand_name('test_user_')
alt_password = rand_name('pass_')
@@ -331,3 +331,17 @@
if len(fail) != 0:
self.fail('Should raise Not Found when list users with invalid'
'tenant ids %s' % fail)
+
+
+class UsersTestJSON(base.BaseIdentityAdminTestJSON,
+ UsersTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(UsersTestJSON, cls).setUpClass()
+
+
+class UsersTestXML(base.BaseIdentityAdminTestXML,
+ UsersTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(UsersTestXML, cls).setUpClass()
diff --git a/tempest/tests/identity/base.py b/tempest/tests/identity/base.py
index 60037ca..f397a5b 100644
--- a/tempest/tests/identity/base.py
+++ b/tempest/tests/identity/base.py
@@ -18,50 +18,25 @@
import nose
import unittest2 as unittest
-import tempest.config
from tempest.common.utils.data_utils import rand_name
-from tempest.services.identity.json.admin_client import AdminClient
-from tempest.services.identity.json.admin_client import TokenClient
+from tempest import openstack
-class BaseIdentityAdminTest(unittest.TestCase):
+class BaseIdAdminTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
- cls.config = tempest.config.TempestConfig()
- cls.username = cls.config.identity_admin.username
- cls.password = cls.config.identity_admin.password
- cls.tenant_name = cls.config.identity_admin.tenant_name
-
- if not (cls.username
- and cls.password
- and cls.tenant_name):
- raise nose.SkipTest("Missing Admin credentials in configuration")
-
- client_args = (cls.config,
- cls.username,
- cls.password,
- cls.config.identity.auth_url)
- cls.client = AdminClient(*client_args, tenant_name=cls.tenant_name)
- cls.token_client = TokenClient(cls.config)
+ os = openstack.IdentityManager(interface=cls._interface)
+ cls.client = os.admin_client
+ cls.token_client = os.token_client
if not cls.client.has_admin_extensions():
raise nose.SkipTest("Admin extensions disabled")
cls.data = DataGenerator(cls.client)
- # Create an admin client with regular Compute API credentials. This
- # client is used in tests to validate Unauthorized is returned
- # for non-admin users accessing Identity Admin API commands
- cls.na_username = cls.config.compute.username
- cls.na_password = cls.config.compute.password
- cls.na_tenant_name = cls.config.compute.tenant_name
- na_client_args = (cls.config,
- cls.na_username,
- cls.na_password,
- cls.config.identity.auth_url)
- cls.non_admin_client = AdminClient(*na_client_args,
- tenant_name=cls.na_tenant_name)
+ os = openstack.IdentityNaManager(interface=cls._interface)
+ cls.non_admin_client = os.admin_client
@classmethod
def tearDownClass(cls):
@@ -94,6 +69,22 @@
return role[0]
+class BaseIdentityAdminTestJSON(BaseIdAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(BaseIdentityAdminTestJSON, cls).setUpClass()
+
+BaseIdentityAdminTest = BaseIdentityAdminTestJSON
+
+
+class BaseIdentityAdminTestXML(BaseIdAdminTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(BaseIdentityAdminTestXML, cls).setUpClass()
+
+
class DataGenerator(object):
def __init__(self, client):
diff --git a/tempest/tests/volume/base.py b/tempest/tests/volume/base.py
index 1f36ceb..2e016d5 100644
--- a/tempest/tests/volume/base.py
+++ b/tempest/tests/volume/base.py
@@ -24,7 +24,6 @@
from tempest import config
from tempest import openstack
from tempest.common.utils.data_utils import rand_name
-from tempest.services.identity.json.admin_client import AdminClient
from tempest import exceptions
LOG = logging.getLogger(__name__)
@@ -74,13 +73,8 @@
"""
Returns an instance of the Identity Admin API client
"""
- client_args = (cls.config,
- cls.config.identity_admin.username,
- cls.config.identity_admin.password,
- cls.config.identity.auth_url)
- tenant_name = cls.config.identity_admin.tenant_name
- admin_client = AdminClient(*client_args, tenant_name=tenant_name)
- return admin_client
+ os = openstack.IdentityManager()
+ return os.admin_client
@classmethod
def _get_isolated_creds(cls):
@@ -151,3 +145,17 @@
condition()
return
time.sleep(self.build_interval)
+
+
+class BaseVolumeTestJSON(BaseVolumeTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(BaseVolumeTestJSON, cls).setUpClass()
+
+
+class BaseVolumeTestXML(BaseVolumeTest):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(BaseVolumeTestXML, cls).setUpClass()
diff --git a/tempest/tests/volume/test_volumes_get.py b/tempest/tests/volume/test_volumes_get.py
index 4305c67..03ecb5a 100644
--- a/tempest/tests/volume/test_volumes_get.py
+++ b/tempest/tests/volume/test_volumes_get.py
@@ -18,15 +18,10 @@
from nose.plugins.attrib import attr
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
-class VolumesGetTest(BaseVolumeTest):
-
- @classmethod
- def setUpClass(cls):
- super(VolumesGetTest, cls).setUpClass()
- cls.client = cls.volumes_client
+class VolumesGetTestBase(object):
@attr(type='smoke')
def test_volume_create_get_delete(self):
@@ -97,3 +92,19 @@
resp, _ = self.client.delete_volume(volume['id'])
self.assertEqual(202, resp.status)
self.client.wait_for_resource_deletion(volume['id'])
+
+
+class VolumesGetTestXML(base.BaseVolumeTestXML, VolumesGetTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "xml"
+ super(VolumesGetTestXML, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+
+class VolumesGetTestJSON(base.BaseVolumeTestJSON, VolumesGetTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = "json"
+ super(VolumesGetTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_client
diff --git a/tempest/tests/volume/test_volumes_list.py b/tempest/tests/volume/test_volumes_list.py
index 24055af..6c6dac2 100644
--- a/tempest/tests/volume/test_volumes_list.py
+++ b/tempest/tests/volume/test_volumes_list.py
@@ -19,10 +19,10 @@
from nose.plugins.attrib import attr
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
-class VolumesListTest(BaseVolumeTest):
+class VolumesListTestBase(object):
"""
This test creates a number of 1G volumes. To run successfully,
@@ -32,9 +32,38 @@
VOLUME_BACKING_FILE_SIZE is atleast 4G in your localrc
"""
+ @attr(type='smoke')
+ def test_volume_list(self):
+ """Get a list of Volumes"""
+ # Fetch all volumes
+ resp, fetched_list = self.client.list_volumes()
+ self.assertEqual(200, resp.status)
+ # Now check if all the volumes created in setup are in fetched list
+ missing_vols = [v for v in self.volume_list if v not in fetched_list]
+ self.assertFalse(missing_vols,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['display_name']
+ for m_vol in missing_vols))
+
+ @attr(type='smoke')
+ def test_volume_list_with_details(self):
+ """Get a list of Volumes with details"""
+ # Fetch all Volumes
+ resp, fetched_list = self.client.list_volumes_with_detail()
+ self.assertEqual(200, resp.status)
+ # Verify that all the volumes are returned
+ missing_vols = [v for v in self.volume_list if v not in fetched_list]
+ self.assertFalse(missing_vols,
+ "Failed to find volume %s in fetched list"
+ % ', '.join(m_vol['display_name']
+ for m_vol in missing_vols))
+
+
+class VolumeListTestXML(base.BaseVolumeTestXML, VolumesListTestBase):
@classmethod
def setUpClass(cls):
- super(VolumesListTest, cls).setUpClass()
+ cls._interface = 'xml'
+ super(VolumeListTestXML, cls).setUpClass()
cls.client = cls.volumes_client
# Create 3 test volumes
@@ -74,30 +103,51 @@
for volume in cls.volume_id_list:
resp, _ = cls.client.delete_volume(volume)
cls.client.wait_for_resource_deletion(volume)
- super(VolumesListTest, cls).tearDownClass()
+ super(VolumeListTestXML, cls).tearDownClass()
- @attr(type='smoke')
- def test_volume_list(self):
- """Get a list of Volumes"""
- # Fetch all volumes
- resp, fetched_list = self.client.list_volumes()
- self.assertEqual(200, resp.status)
- # Now check if all the volumes created in setup are in fetched list
- missing_vols = [v for v in self.volume_list if v not in fetched_list]
- self.assertFalse(missing_vols,
- "Failed to find volume %s in fetched list"
- % ', '.join(m_vol['display_name']
- for m_vol in missing_vols))
- @attr(type='smoke')
- def test_volume_list_with_details(self):
- """Get a list of Volumes with details"""
- # Fetch all Volumes
- resp, fetched_list = self.client.list_volumes_with_detail()
- self.assertEqual(200, resp.status)
- # Verify that all the volumes are returned
- missing_vols = [v for v in self.volume_list if v not in fetched_list]
- self.assertFalse(missing_vols,
- "Failed to find volume %s in fetched list"
- % ', '.join(m_vol['display_name']
- for m_vol in missing_vols))
+class VolumeListTestJSON(base.BaseVolumeTestJSON, VolumesListTestBase):
+ @classmethod
+ def setUpClass(cls):
+ cls._interface = 'json'
+ super(VolumeListTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+ # Create 3 test volumes
+ cls.volume_list = []
+ cls.volume_id_list = []
+ for i in range(3):
+ v_name = rand_name('volume')
+ metadata = {'Type': 'work'}
+ try:
+ resp, volume = cls.client.create_volume(size=1,
+ display_name=v_name,
+ metadata=metadata)
+ cls.client.wait_for_volume_status(volume['id'],
+ 'available')
+ resp, volume = cls.client.get_volume(volume['id'])
+ cls.volume_list.append(volume)
+ cls.volume_id_list.append(volume['id'])
+ except:
+ if cls.volume_list:
+ # We could not create all the volumes, though we were able
+ # to create *some* of the volumes. This is typically
+ # because the backing file size of the volume group is
+ # too small. So, here, we clean up whatever we did manage
+ # to create and raise a SkipTest
+ for volume in cls.volume_id_list:
+ cls.client.delete_volume(volume)
+ msg = ("Failed to create ALL necessary volumes to run "
+ "test. This typically means that the backing file "
+ "size of the nova-volumes group is too small to "
+ "create the 3 volumes needed by this test case")
+ raise nose.SkipTest(msg)
+ raise
+
+ @classmethod
+ def tearDownClass(cls):
+ # Delete the created volumes
+ for volume in cls.volume_id_list:
+ resp, _ = cls.client.delete_volume(volume)
+ cls.client.wait_for_resource_deletion(volume)
+ super(VolumeListTestJSON, cls).tearDownClass()
diff --git a/tempest/tests/volume/test_volumes_negative.py b/tempest/tests/volume/test_volumes_negative.py
index 63b209e..bf7e5f0 100644
--- a/tempest/tests/volume/test_volumes_negative.py
+++ b/tempest/tests/volume/test_volumes_negative.py
@@ -20,15 +20,10 @@
from tempest import exceptions
from tempest.common.utils.data_utils import rand_name
-from tempest.tests.volume.base import BaseVolumeTest
+from tempest.tests.volume import base
-class VolumesNegativeTest(BaseVolumeTest):
-
- @classmethod
- def setUpClass(cls):
- super(VolumesNegativeTest, cls).setUpClass()
- cls.client = cls.volumes_client
+class VolumesNegativeTestBase(object):
@raises(exceptions.NotFound)
@attr(type='negative')
@@ -131,3 +126,18 @@
Should not be able to delete volume when empty ID is passed
"""
resp, volume = self.client.delete_volume('')
+
+
+class VolumesNegativeTestXML(base.BaseVolumeTestXML, VolumesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesNegativeTestXML, cls).setUpClass()
+ cls.client = cls.volumes_client
+
+
+class VolumesNegativeTestJSON(base.BaseVolumeTestJSON,
+ VolumesNegativeTestBase):
+ @classmethod
+ def setUpClass(cls):
+ super(VolumesNegativeTestJSON, cls).setUpClass()
+ cls.client = cls.volumes_client