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:
Liam Young 2017-01-12 14:28:33 +00:00
parent 2d054f6bb9
commit 8d638c8d9c
4 changed files with 191 additions and 84 deletions

View File

@ -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

View File

@ -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):

View File

@ -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]

View File

@ -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):