Add Keystone v3 support to charm
This change adds Keystone v3 support to the charm. Whether to run using the keystone v2 or v3 api should be dictated by the api_version sent down the identity-admin relation but the change to enable that has not landed yet and it is useful to have this charm work for v3 against stable charms. To allow this there is some hard coding of expected v3 credentials that can be removed later. fwiw this change represents a net reduction in hardcoded creds. changes include: 1) Fix typo in default floating-network-name ext_net rather that ext-net 2) Refactor init_keystone_client to initialise the right client depending on api version. 3) Add a keystone_session object which can be shared by the individual project clients 4) Use credential information passed down the identity-admin relation in the tempest.conf template 5) Disable the ec2 cred checks for keystone v3 as these rely on the client being able to resolve users and projects to their uuids which does not seem to be possible with a project scoped client. This shoud be fixed at a later date by the identity admin relation passing the uuids to the charm 6) Remove try/catch-all excepts blocks. They were initially put in as the charm tried to render its config as it came up meaning that the services it tried to query may have been down. The charm no longer does this and a client failure should be fatal. Also try/catch-all excepts blocks are a fundamentally bad thing. Where needed check that the service is present in the catalogue before querying its api. Change-Id: I3dd5ed610794eece515d9a03391f9844ac83efc0
This commit is contained in:
parent
2d054f6bb9
commit
8d638c8d9c
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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):
|
||||
|
||||
|
|
Loading…
Reference in New Issue