Adds an identity admin client and API tests for keystone roles.
Added a config option for the [identity] section
* catalog type - to specify endpoints for the Identity service
Fixes bug 902389
Change-Id: I429d8bbfe3e6de8432a1a7b79a8676c63925f78f
diff --git a/etc/tempest.conf.sample b/etc/tempest.conf.sample
index 226fa30..aa101d3 100644
--- a/etc/tempest.conf.sample
+++ b/etc/tempest.conf.sample
@@ -3,6 +3,10 @@
# test clients use when authenticating with different user/tenant
# combinations
+# The type of endpoint for a Identity service. Unless you have a
+# custom Keystone service catalog implementation, you probably want to leave
+# this value as "identity"
+catalog_type = identity
# Set to True if your test environment's Keystone authentication service should
# be accessed over HTTPS
use_ssl = False
diff --git a/tempest/common/rest_client.py b/tempest/common/rest_client.py
index e9741bf..fd2f684 100644
--- a/tempest/common/rest_client.py
+++ b/tempest/common/rest_client.py
@@ -132,12 +132,15 @@
if mgmt_url == None:
raise exceptions.EndpointNotFound(service)
- #TODO (dwalleck): This is a horrible stopgap.
- #Need to join strings more cleanly
- temp = mgmt_url.rsplit('/')
- service_url = temp[0] + '//' + temp[2] + '/' + temp[3] + '/'
- management_url = service_url + tenant_id
- return token, management_url
+ if mgmt_url.endswith(tenant_id):
+ return token, mgmt_url
+ else:
+ #TODO (dwalleck): This is a horrible stopgap.
+ #Need to join strings more cleanly
+ temp = mgmt_url.rsplit('/')
+ service_url = temp[0] + '//' + temp[2] + '/' + temp[3] + '/'
+ management_url = service_url + tenant_id
+ return token, management_url
elif resp.status == 401:
raise exceptions.AuthenticationFailure(user=user,
password=password)
diff --git a/tempest/config.py b/tempest/config.py
index fcfeee5..d4a0c03 100644
--- a/tempest/config.py
+++ b/tempest/config.py
@@ -45,6 +45,11 @@
SECTION_NAME = "identity"
@property
+ def catalog_type(self):
+ """Catalog type of the Identity service."""
+ return self.get("catalog_type", 'identity')
+
+ @property
def host(self):
"""Host IP for making Identity API requests."""
return self.get("host", "127.0.0.1")
diff --git a/tempest/openstack.py b/tempest/openstack.py
index 491f385..3e19ba5 100644
--- a/tempest/openstack.py
+++ b/tempest/openstack.py
@@ -30,6 +30,7 @@
from tempest.services.nova.json.floating_ips_client import FloatingIPsClient
from tempest.services.nova.json.keypairs_client import KeyPairsClient
from tempest.services.nova.json.volumes_client import VolumesClient
+from tempest.services.identity.json.admin_client import AdminClient
LOG = logging.getLogger(__name__)
@@ -79,6 +80,7 @@
self.security_groups_client = SecurityGroupsClient(*client_args)
self.floating_ips_client = FloatingIPsClient(*client_args)
self.volumes_client = VolumesClient(*client_args)
+ self.admin_client = AdminClient(*client_args)
class AltManager(Manager):
diff --git a/tempest/services/identity/__init__.py b/tempest/services/identity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/identity/__init__.py
diff --git a/tempest/services/identity/json/__init__.py b/tempest/services/identity/json/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/services/identity/json/__init__.py
diff --git a/tempest/services/identity/json/admin_client.py b/tempest/services/identity/json/admin_client.py
new file mode 100644
index 0000000..45a7985
--- /dev/null
+++ b/tempest/services/identity/json/admin_client.py
@@ -0,0 +1,44 @@
+from tempest.common.rest_client import RestClient
+import json
+
+
+class AdminClient(RestClient):
+
+ def __init__(self, config, username, password, auth_url, tenant_name=None):
+ super(AdminClient, self).__init__(config, username, password,
+ auth_url, tenant_name)
+ self.service = self.config.identity.catalog_type
+ self.endpoint_url = 'adminURL'
+
+ 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"""
+ post_body = {
+ 'name': name,
+ }
+ post_body = json.dumps({'role': post_body})
+ resp, body = self.post('OS-KSADM/roles', post_body,
+ self.headers)
+ body = json.loads(body)
+ return resp, body['role']
+
+ def delete_role(self, role_id):
+ """Delete a role"""
+ resp, body = self.delete('OS-KSADM/roles/%s' % role_id)
+ return resp, body
+
+ def list_roles(self):
+ """Returns roles"""
+ resp, body = self.get('OS-KSADM/roles')
+ body = json.loads(body)
+ return resp, body['roles']
diff --git a/tempest/tests/identity/__init__.py b/tempest/tests/identity/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tempest/tests/identity/__init__.py
diff --git a/tempest/tests/identity/test_roles.py b/tempest/tests/identity/test_roles.py
new file mode 100644
index 0000000..9f51505
--- /dev/null
+++ b/tempest/tests/identity/test_roles.py
@@ -0,0 +1,84 @@
+import unittest2 as unittest
+
+import nose
+
+from tempest import openstack
+from tempest import exceptions
+from tempest.common.utils.data_utils import rand_name
+from tempest.tests import utils
+
+
+class RolesTest(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.os = openstack.AdminManager()
+ cls.client = cls.os.admin_client
+ cls.config = cls.os.config
+
+ if not cls.client.has_admin_extensions():
+ raise nose.SkipTest("Admin extensions disabled")
+
+ cls.roles = []
+ for _ in xrange(5):
+ resp, body = cls.client.create_role(rand_name('role-'))
+ cls.roles.append(body['id'])
+
+ @classmethod
+ def tearDownClass(cls):
+ for role in cls.roles:
+ cls.client.delete_role(role)
+
+ def test_list_roles(self):
+ """Return a list of all roles"""
+ resp, body = self.client.list_roles()
+ found = [role for role in body if role['id'] in self.roles]
+ self.assertTrue(any(found))
+ self.assertEqual(len(found), len(self.roles))
+
+ def test_role_create_delete(self):
+ """Role should be created, verified, and deleted"""
+ role_name = rand_name('role-test-')
+ resp, body = self.client.create_role(role_name)
+ self.assertTrue('status' in resp)
+ self.assertTrue(resp['status'].startswith('2'))
+ self.assertEqual(role_name, body['name'])
+
+ resp, body = self.client.list_roles()
+ found = [role for role in body if role['name'] == role_name]
+ self.assertTrue(any(found))
+
+ resp, body = self.client.delete_role(found[0]['id'])
+ self.assertTrue('status' in resp)
+ self.assertTrue(resp['status'].startswith('2'))
+
+ resp, body = self.client.list_roles()
+ found = [role for role in body if role['name'] == role_name]
+ self.assertFalse(any(found))
+
+ def test_role_create_blank_name(self):
+ """Should not be able to create a role with a blank name"""
+ try:
+ resp, body = self.client.create_role('')
+ except exceptions.Duplicate:
+ self.fail('A role with a blank name already exists.')
+ self.assertTrue('status' in resp)
+ self.assertFalse(resp['status'].startswith('2'), 'Create role with '
+ 'empty name should fail')
+
+ def test_role_create_duplicate(self):
+ """Role names should be unique"""
+ role_name = rand_name('role-dup-')
+ resp, body = self.client.create_role(role_name)
+ role1_id = body.get('id')
+ self.assertTrue('status' in resp)
+ self.assertTrue(resp['status'].startswith('2'))
+
+ try:
+ resp, body = self.client.create_role(role_name)
+ # this should raise an exception
+ self.fail('Should not be able to create a duplicate role name.'
+ ' %s' % role_name)
+ except exceptions.Duplicate:
+ pass
+ self.client.delete_role(role1_id)