diff --git a/src/config.yaml b/src/config.yaml index d743416..7386b09 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -41,7 +41,7 @@ options: description: "neutron network" floating-network-name: type: string - default: "ext-net" + default: "ext_net" description: "floating IP network" swift-resource-ip: type: string diff --git a/src/lib/charm/openstack/tempest.py b/src/lib/charm/openstack/tempest.py index df6b004..b79e2d9 100644 --- a/src/lib/charm/openstack/tempest.py +++ b/src/lib/charm/openstack/tempest.py @@ -1,13 +1,17 @@ -import re import os +import re import subprocess import time +import urllib import glanceclient -import keystoneclient.v2_0 as keystoneclient +import keystoneauth1 +import keystoneclient.v2_0.client as keystoneclient_v2 +import keystoneclient.v3.client as keystoneclient_v3 +import keystoneclient.auth.identity.v3 as keystone_id_v3 +import keystoneclient.session as session import neutronclient.v2_0.client as neutronclient -import novaclient.v2 as novaclient -import urllib +import novaclient.client as novaclient_client import charms_openstack.charm as charm import charms_openstack.adapters as adapters @@ -57,6 +61,8 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): def __init__(self, relation): """Initialise a keystone client and collect user defined config""" self.kc = None + self.keystone_session = None + self.api_version = '2' super(TempestAdminAdapter, self).__init__(relation) self.init_keystone_client() self.uconfig = hookenv.config() @@ -64,7 +70,16 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @property def keystone_info(self): """Collection keystone information from keystone relation""" - return self.relation.credentials() + ks_info = self.relation.credentials() + ks_info['default_credentials_domain_name'] = 'default' + if ks_info.get('api_version'): + ks_info['api_version'] = ks_info.get('api_version') + else: + ks_info['api_version'] = self.api_version + if not ks_info.get('service_user_domain_name'): + ks_info['service_user_domain_name'] = 'admin_domain' + + return ks_info @property def ks_client(self): @@ -72,29 +87,86 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): self.init_keystone_client() return self.kc - @property - def keystone_auth_url(self): - return '{}://{}:{}/v2.0'.format( + def keystone_auth_url(self, api_version=None): + if not api_version: + api_version = self.keystone_info.get('api_version', '2') + ep_suffix = { + '2': 'v2.0', + '3': 'v3'}[api_version] + return '{}://{}:{}/{}'.format( 'http', self.keystone_info['service_hostname'], - self.keystone_info['service_port'] + self.keystone_info['service_port'], + ep_suffix, ) + def resolve_endpoint(self, service_type, interface): + if self.api_version == '2': + ep = self.ks_client.service_catalog.url_for( + service_type=service_type, + endpoint_type='{}URL'.format(interface) + ) + else: + svc_id = self.ks_client.services.find(type=service_type).id + ep = self.ks_client.endpoints.find( + service_id=svc_id, + interface=interface).url + return ep + + def set_keystone_v2_client(self): + self.keystone_session = None + self.kc = keystoneclient_v2.Client(**self.admin_creds_v2) + + def set_keystone_v3_client(self): + auth = keystone_id_v3.Password(**self.admin_creds_v3) + self.keystone_session = session.Session(auth=auth) + self.kc = keystoneclient_v3.Client(session=self.keystone_session) + def init_keystone_client(self): """Initialise keystone client""" if self.kc: return - auth = { + if self.keystone_info.get('api_version', '2') > '2': + self.set_keystone_v3_client() + self.api_version = '3' + else: + # XXX Temporarily catching the Unauthorized exception to deal with + # the case (pre-17.02) where the keystone charm maybe in v3 mode + # without telling charms via the identity-admin relation + try: + self.set_keystone_v2_client() + self.api_version = '2' + except keystoneauth1.exceptions.http.Unauthorized: + self.set_keystone_v3_client() + self.api_version = '3' + self.kc.services.list() + + def admin_creds_base(self, api_version): + return { 'username': self.keystone_info['service_username'], 'password': self.keystone_info['service_password'], - 'auth_url': self.keystone_auth_url, - 'tenant_name': self.keystone_info['service_tenant_name'], - 'region_name': self.keystone_info['service_region'], - } - try: - self.kc = keystoneclient.client.Client(**auth) - except: - hookenv.log("Keystone is not ready, deferring keystone query") + 'auth_url': self.keystone_auth_url(api_version=api_version)} + + @property + def admin_creds_v2(self): + creds = self.admin_creds_base(api_version='2') + creds['tenant_name'] = self.keystone_info['service_tenant_name'] + creds['region_name'] = self.keystone_info['service_region'] + return creds + + @property + def admin_creds_v3(self): + creds = self.admin_creds_base(api_version='3') + creds['project_name'] = self.keystone_info.get( + 'service_project_name', + 'admin') + creds['user_domain_name'] = self.keystone_info.get( + 'service_user_domain_name', + 'admin_domain') + creds['project_domain_name'] = self.keystone_info.get( + 'service_project_domain_name', + 'Default') + return creds @property def ec2_creds(self): @@ -102,16 +174,19 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'access_token' token1, 'secret_token': token2} """ - if not self.ks_client: - return {} - current_creds = self.ks_client.ec2.list(self.ks_client.user_id) - if current_creds: - creds = current_creds[0] - else: - creds = self.ks_client.ec2.create( - self.ks_client.user_id, - self.ks_client.tenant_id) - return {'access_token': creds.access, 'secret_token': creds.secret} + _ec2creds = {} + if self.api_version == '2': + current_creds = self.ks_client.ec2.list(self.ks_client.user_id) + if current_creds: + _ec2creds = current_creds[0] + else: + creds = self.ks_client.ec2.create( + self.ks_client.user_id, + self.ks_client.tenant_id) + _ec2creds = { + 'access_token': creds.access, + 'secret_token': creds.secret} + return _ec2creds @property def image_info(self): @@ -120,12 +195,14 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'image_id' id1, 'image_alt_id': id2} """ image_info = {} - try: - glance_endpoint = self.ks_client.service_catalog.url_for( - service_type='image', - endpoint_type='publicURL') - glance_client = glanceclient.Client( - '2', glance_endpoint, token=self.ks_client.auth_token) + if self.service_present('glance'): + if self.keystone_session: + glance_client = glanceclient.Client( + '2', session=self.keystone_session) + else: + glance_ep = self.resolve_endpoint('image', 'public') + glance_client = glanceclient.Client( + '2', glance_ep, token=self.ks_client.auth_token) for image in glance_client.images.list(): if self.uconfig.get('glance-image-name') == image.name: image_info['image_id'] = image.id @@ -137,8 +214,6 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): if self.uconfig.get('image-alt-ssh-user'): image_info['image_alt_ssh_user'] = \ self.uconfig.get('image-alt-ssh-user') - except: - hookenv.log("Glance is not ready, deferring glance query") return image_info @property @@ -149,13 +224,17 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'public_network_id' id1, 'router_id': id2} """ network_info = {} - try: - neutron_ep = self.ks_client.service_catalog.url_for( - service_type='network', - endpoint_type='publicURL') - neutron_client = neutronclient.Client( - endpoint_url=neutron_ep, - token=self.ks_client.auth_token) + if self.service_present('neutron'): + if self.keystone_session: + neutron_client = neutronclient.Client( + session=self.keystone_session) + else: + neutron_ep = self.ks_client.service_catalog.url_for( + service_type='network', + endpoint_type='publicURL') + neutron_client = neutronclient.Client( + endpoint_url=neutron_ep, + token=self.ks_client.auth_token) routers = neutron_client.list_routers( name=self.uconfig['router-name']) if len(routers['routers']) == 0: @@ -175,12 +254,18 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): if len(networks['networks']) == 0: hookenv.log("Floating network name not found") else: - network_info['floating-network-name'] = \ + network_info['floating_network_name'] = \ self.uconfig['floating-network-name'] - except: - hookenv.log("Neutron is not ready, deferring neutron query") return network_info + def service_present(self, service): + """Check if a given service type is registered in the catalogue + + :params service: string Service type + @returns Boolean: True if service is registered + """ + return service in self.get_present_services() + @property def compute_info(self): """Return flavor ids for user-defined flavors @@ -188,29 +273,28 @@ class TempestAdminAdapter(adapters.OpenStackRelationAdapter): @returns {'flavor_id' id1, 'flavor_alt_id': id2} """ compute_info = {} - try: - nova_ep = self.ks_client.service_catalog.url_for( - service_type='compute', - endpoint_type='publicURL' - ) - compute_info['nova_endpoint'] = nova_ep + if self.service_present('nova'): + if self.keystone_session: + nova_client = novaclient_client.Client( + 2, session=self.keystone_session) + else: + nova_client = novaclient_client.Client( + 2, + self.keystone_info['service_username'], + self.keystone_info['service_password'], + self.keystone_info['service_tenant_name'], + self.keystone_auth_url(), + ) + nova_ep = self.resolve_endpoint('compute', 'public') url = urllib.parse.urlparse(nova_ep) compute_info['nova_base'] = '{}://{}'.format( url.scheme, url.netloc.split(':')[0]) - nova_client = novaclient.client.Client( - self.keystone_info['service_username'], - self.keystone_info['service_password'], - self.keystone_info['service_tenant_name'], - self.keystone_auth_url, - ) for flavor in nova_client.flavors.list(): if self.uconfig['flavor-name'] == flavor.name: compute_info['flavor_id'] = flavor.id if self.uconfig['flavor-alt-name'] == flavor.name: compute_info['flavor_alt_id'] = flavor.id - except: - hookenv.log("Nova is not ready, deferring nova query") return compute_info def get_present_services(self): diff --git a/src/templates/tempest.conf b/src/templates/tempest.conf index 666a6da..c5fdc1b 100644 --- a/src/templates/tempest.conf +++ b/src/templates/tempest.conf @@ -1,18 +1,13 @@ -# {{ identity_admin.ec2_creds.access_token }} -# {{ identity_admin.image_info }} -# {{ identity_admin.image_info.image_id }} -# {{ identity_admin.image_info.image_alt_id }} -# {{ identity_admin.network_info }} -# {{ identity_admin.compute_info }} -# SI: {{ identity_admin.service_info }} [DEFAULT] lock_path=/tmp [baremetal] +{% if identity_admin.ec2_creds.access_token -%} [boto] ec2_url = {{ identity_admin.compute_info.nova_base }}:8773/services/Cloud s3_url = {{ identity_admin.compute_info.nova_base }}:3333 aws_access = {{ identity_admin.ec2_creds.access_token }} aws_secret = {{ identity_admin.ec2_creds.secret_token }} +{% endif -%} [cli] enabled=true timeout=60 @@ -36,13 +31,17 @@ dashboard_url={{ dashboard_url }}/horizon login_url={{ dashboard_url }}/horizon/auth/login/ [data_processing] [debug] +[auth] +default_credentials_domain_name = {{ identity_admin.keystone_info.default_credentials_domain_name }} +admin_username={{ identity_admin.keystone_info.service_username }} +admin_project_name={{ identity_admin.keystone_info.service_tenant_name }} +admin_password={{ identity_admin.keystone_info.service_password }} +admin_domain_name={{ identity_admin.keystone_info.service_user_domain_name }} + [identity] +admin_domain_scope=true uri=http://{{ identity_admin.keystone_info.service_hostname }}:5000/v2.0 uri_v3=http://{{ identity_admin.keystone_info.service_hostname }}:5000/v3 -admin_username=admin -admin_tenant_name=admin -admin_password=openstack -admin_domain_name=Default username = demo password = pass tenant_name = demo @@ -50,7 +49,15 @@ alt_username = alt_demo alt_password = secret alt_tenant_name = alt_demo admin_role = Admin +auth_version=v{{ identity_admin.keystone_info.api_version }} [identity-feature-enabled] +{% if identity_admin.keystone_info.api_version == 3 -%} +api_v3=true +api_v2=false +{% else -%} +api_v3=false +api_v2=true +{% endif -%} [image] http_image = http://{{ options.swift_undercloud_ep }}:80/swift/v1/images/cirros-0.3.3-x86_64-uec.tar.gz [image-feature-enabled] diff --git a/unit_tests/test_lib_charm_openstack_tempest.py b/unit_tests/test_lib_charm_openstack_tempest.py index 6b53c0d..b1d5dc4 100644 --- a/unit_tests/test_lib_charm_openstack_tempest.py +++ b/unit_tests/test_lib_charm_openstack_tempest.py @@ -49,7 +49,7 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): 'service_password': 'pass1', 'service_tenant_name': 'svc', 'service_region': 'reg1'} - self.patch_object(tempest.keystoneclient.client, 'Client') + self.patch_object(tempest.keystoneclient_v2, 'Client') self.patch_object(tempest.hookenv, 'config') self.patch_object( tempest.adapters.OpenStackRelationAdapter, '__init__') @@ -69,14 +69,11 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): def test_ec2_creds(self): self.patch_object(tempest.hookenv, 'config') self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') - self.patch_object( - tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, '_setup_properties') kc = mock.MagicMock() - creds = mock.MagicMock() - creds.access = 'ac2' - creds.secret = 'st2' kc.user_id = 'bob' - kc.ec2.list = lambda x: [creds] + kc.ec2.list = lambda x: [{'access_token': 'ac2', + 'secret_token': 'st2'}] self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) a = tempest.TempestAdminAdapter('rel2') self.assertEqual(a.ec2_creds, {'access_token': 'ac2', @@ -85,6 +82,8 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): def test_image_info(self): self.patch_object(tempest.hookenv, 'config') self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object(tempest.TempestAdminAdapter, 'service_present', + return_value=True) self.patch_object( tempest.adapters.OpenStackRelationAdapter, '__init__') self.patch_object(tempest.TempestAdminAdapter, 'ks_client') @@ -111,6 +110,8 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): def test_network_info(self): self.patch_object(tempest.hookenv, 'config') + self.patch_object(tempest.TempestAdminAdapter, 'service_present', + return_value=True) self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') self.patch_object( tempest.adapters.OpenStackRelationAdapter, '__init__') @@ -124,6 +125,7 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): self.ks_client.return_value = kc self.config.return_value = { 'router-name': 'route1', + 'floating-network-name': 'ext_net', 'network-name': 'net1'} nc = mock.MagicMock() nc.list_routers = lambda name=None: {'routers': [router1]} @@ -132,11 +134,15 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): a = tempest.TempestAdminAdapter('rel2') self.assertEqual( a.network_info, - {'public_network_id': 'pubnet1', 'router_id': '16'}) + {'floating_network_name': 'ext_net', + 'public_network_id': 'pubnet1', + 'router_id': '16'}) def test_compute_info(self): self.patch_object(tempest.hookenv, 'config') self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object(tempest.TempestAdminAdapter, 'service_present', + return_value=True) self.patch_object( tempest.adapters.OpenStackRelationAdapter, '__init__') ki = { @@ -151,14 +157,15 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): self.patch_object( tempest.TempestAdminAdapter, 'keystone_auth_url', - new='auth_url') + return_value='auth_url') self.config.return_value = { + 'flavor-alt-name': None, 'flavor-name': 'm3.huuge'} kc = mock.MagicMock() kc.service_catalog.url_for = \ lambda service_type=None, endpoint_type=None: 'http://nova:999/bob' self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) - self.patch_object(tempest.novaclient.client, 'Client') + self.patch_object(tempest.novaclient_client, 'Client') _flavor1 = mock.MagicMock() _flavor1.name = 'm3.huuge' _flavor1.id = 'id1' @@ -170,8 +177,7 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): a.compute_info, { 'flavor_id': 'id1', - 'nova_base': 'http://nova', - 'nova_endpoint': 'http://nova:999/bob'}) + 'nova_base': 'http://nova'}) def test_get_present_services(self): self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') @@ -249,6 +255,16 @@ class TestTempestAdminAdapter(test_utils.PatchHelper): 'zaqar': 'false', 'neutron': 'false'}) + def test_service_present(self): + self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') + self.patch_object( + tempest.adapters.OpenStackRelationAdapter, '__init__') + self.patch_object(tempest.TempestAdminAdapter, 'get_present_services', + return_value=['svc1', 'svc2']) + a = tempest.TempestAdminAdapter('rel2') + self.assertTrue(a.service_present('svc1')) + self.assertFalse(a.service_present('svc3')) + class TestTempestCharm(Helper):