heat_keystoneclient add legacy fallback path

If users fail to observe the reccomendations in the commit messages
and/or release notes and use a heat.conf from a previous version of
heat currently some functionality will fail due to the missing
domain configuration in heat.conf.  Instead degrade gracefully with
warnings, calling the non-domain functions which are maintained
(for now) for backwards compatibility.

Hopefully we won't have to maintain this huge pile of legacy stuff
for too long :(

Change-Id: Ia029d585dfdb2563609725589c41083819a11a3b
Closes-Bug: #1287980
This commit is contained in:
Steven Hardy 2014-03-05 16:38:54 +00:00
parent 0614b707d3
commit 7953ce1fec
2 changed files with 223 additions and 15 deletions

View File

@ -81,14 +81,25 @@ class KeystoneClientV3(object):
# populates self.context.auth_token with a trust-scoped token
self._client_v3 = self._v3_client_init()
# The stack domain user ID must be set in heat.conf
# The stack domain user ID should be set in heat.conf
# It can be created via python-openstackclient
# openstack --os-identity-api-version=3 domain create heat
stack_domain = cfg.CONF.stack_user_domain
if not stack_domain:
raise exception.Error("stack_user_domain ID not set in heat.conf")
logger.debug(_("Using stack domain %s") % stack_domain)
self.stack_domain_id = stack_domain
# If the domain is specified, then you must specify a domain
# admin user. If no domain is specified, we fall back to
# legacy behavior with warnings.
self.stack_domain_id = cfg.CONF.stack_user_domain
self.domain_admin_user = cfg.CONF.stack_domain_admin
self.domain_admin_password = cfg.CONF.stack_domain_admin_password
if self.stack_domain_id:
if not (self.domain_admin_user and self.domain_admin_password):
raise exception.Error(_('heat.conf misconfigured, cannot '
'specify stack_user_domain without'
' stack_domain_admin and'
' stack_domain_admin_password'))
else:
logger.warning(_('stack_user_domain ID not set in heat.conf '
'falling back to using default'))
logger.debug(_('Using stack domain %s') % self.stack_domain_id)
@property
def client_v3(self):
@ -183,14 +194,10 @@ class KeystoneClientV3(object):
return creds
def _domain_admin_creds(self):
domain_admin_user = cfg.CONF.stack_domain_admin
if not domain_admin_user:
raise ValueError('heat.conf misconfigured, '
'no stack_domain_admin user specified')
creds = {
'username': domain_admin_user,
'username': self.domain_admin_user,
'user_domain_id': self.stack_domain_id,
'password': cfg.CONF.stack_domain_admin_password,
'password': self.domain_admin_password,
'auth_url': self.v3_endpoint,
'endpoint': self.v3_endpoint}
return creds
@ -309,6 +316,12 @@ class KeystoneClientV3(object):
the specified project (which is expected to be in the stack_domain.
Returns the keystone ID of the resulting user
"""
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain user create, '
'configure domain in heat.conf'))
return self.create_stack_user(username=username, password=password)
# We add the new user to a special keystone role
# This role is designed to allow easier differentiation of the
@ -344,6 +357,12 @@ class KeystoneClientV3(object):
raise ValueError(_('User %s in invalid project') % action)
def delete_stack_domain_user(self, user_id, project_id):
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain user delete, '
'configure domain in heat.conf'))
return self.delete_stack_user(user_id)
self._check_stack_domain_user(user_id, project_id, 'delete')
self.domain_admin_client.users.delete(user_id)
@ -352,6 +371,12 @@ class KeystoneClientV3(object):
def create_stack_domain_project(self, stack_name):
'''Creates a project in the heat stack-user domain.'''
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain project, '
'configure domain in heat.conf'))
return self.context.tenant_id
# Note we use the tenant ID not name to ensure uniqueness in a multi-
# domain environment (where the tenant name may not be globally unique)
project_name = '%s-%s' % (self.context.tenant_id, stack_name)
@ -363,6 +388,12 @@ class KeystoneClientV3(object):
return domain_project.id
def delete_stack_domain_project(self, project_id):
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain project, '
'configure domain in heat.conf'))
return
self.domain_admin_client.projects.delete(project=project_id)
def _find_ec2_keypair(self, access, user_id=None):
@ -424,6 +455,12 @@ class KeystoneClientV3(object):
secret=data_blob['secret'])
def create_stack_domain_user_keypair(self, user_id, project_id):
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain keypair, '
'configure domain in heat.conf'))
return self.create_ec2_keypair(user_id)
data_blob = {'access': uuid.uuid4().hex,
'secret': uuid.uuid4().hex}
creds = self.domain_admin_client.credentials.create(
@ -440,10 +477,22 @@ class KeystoneClientV3(object):
self.client_v3.users.update(user=user_id, enabled=True)
def disable_stack_domain_user(self, user_id, project_id):
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain disable, '
'configure domain in heat.conf'))
return self.disable_stack_user(user_id)
self._check_stack_domain_user(user_id, project_id, 'disable')
self.domain_admin_client.users.update(user=user_id, enabled=False)
def enable_stack_domain_user(self, user_id, project_id):
if not self.stack_domain_id:
# FIXME(shardy): Legacy fallback for folks using old heat.conf
# files which lack domain configuration
logger.warning(_('Falling back to legacy non-domain enable, '
'configure domain in heat.conf'))
return self.enable_stack_user(user_id)
self._check_stack_domain_user(user_id, project_id, 'enable')
self.domain_admin_client.users.update(user=user_id, enabled=True)

View File

@ -218,6 +218,35 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client.create_stack_domain_user(username='duser',
project_id='aproject')
def test_create_stack_domain_user_legacy_fallback(self):
"""Test creating a stack domain user, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
# mock keystone client functions
self._stubs_v3()
self.mock_ks_v3_client.users = self.m.CreateMockAnything()
mock_user = self.m.CreateMockAnything()
mock_user.id = 'auser123'
self.mock_ks_v3_client.users.create(name='auser',
password='password',
default_project=ctx.tenant_id
).AndReturn(mock_user)
self.mock_ks_v3_client.roles = self.m.CreateMockAnything()
self.mock_ks_v3_client.roles.list().AndReturn(self._mock_roles_list())
self.mock_ks_v3_client.roles.grant(project=ctx.tenant_id,
role='4546',
user='auser123').AndReturn(None)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.create_stack_domain_user(username='auser',
project_id='aproject',
password='password')
def test_create_stack_domain_user_error_norole(self):
"""Test creating a stack domain user, no role error path."""
ctx = utils.dummy_context()
@ -258,6 +287,23 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client.delete_stack_domain_user(user_id='duser123',
project_id='aproject')
def test_delete_stack_domain_user_legacy_fallback(self):
"""Test deleting a stack domain user, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
# mock keystone client functions
self._stubs_v3()
self.mock_ks_v3_client.users = self.m.CreateMockAnything()
self.mock_ks_v3_client.users.delete(user='user123').AndReturn(None)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_domain_user(user_id='user123',
project_id='aproject')
def test_delete_stack_domain_user_error_domain(self):
"""Test deleting a stack domain user, wrong domain."""
@ -409,11 +455,26 @@ class KeystoneClientTest(HeatTestCase):
self.assertEqual('atrust123', trust_context.trust_id)
self.assertEqual('5678', trust_context.trustor_user_id)
def test_init_domain_cfg_not_set(self):
def test_init_domain_cfg_not_set_fallback(self):
"""Test error path when config lacks stack domain ID."""
"""Test error path when config lacks domain config."""
cfg.CONF.clear_override('stack_user_domain')
cfg.CONF.clear_override('stack_domain_admin')
cfg.CONF.clear_override('stack_domain_admin_password')
ctx = utils.dummy_context()
ctx.username = None
ctx.password = None
ctx.trust_id = None
self.assertIsNotNone(heat_keystoneclient.KeystoneClient(ctx))
def test_init_domain_cfg_not_set_error(self):
"""Test error path when config lacks domain config."""
cfg.CONF.clear_override('stack_domain_admin')
cfg.CONF.clear_override('stack_domain_admin_password')
ctx = utils.dummy_context()
ctx.username = None
@ -421,7 +482,10 @@ class KeystoneClientTest(HeatTestCase):
ctx.trust_id = None
err = self.assertRaises(exception.Error,
heat_keystoneclient.KeystoneClient, ctx)
self.assertIn('stack_user_domain ID not set in heat.conf', err)
exp_msg = ('heat.conf misconfigured, cannot specify stack_user_domain '
'without stack_domain_admin and '
'stack_domain_admin_password')
self.assertIn(exp_msg, err)
def test_init_admin_client(self):
@ -627,6 +691,24 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client.enable_stack_domain_user(user_id='duser123',
project_id='aproject')
def test_enable_stack_domain_user_legacy_fallback(self):
"""Test enabling a stack domain user, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
# mock keystone client functions
self._stubs_v3()
self.mock_ks_v3_client.users = self.m.CreateMockAnything()
self.mock_ks_v3_client.users.update(user='user123', enabled=True
).AndReturn(None)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.enable_stack_domain_user(user_id='user123',
project_id='aproject')
def test_enable_stack_domain_user_error_project(self):
"""Test enabling a stack domain user, wrong project."""
@ -674,6 +756,24 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client.disable_stack_domain_user(user_id='duser123',
project_id='aproject')
def test_disable_stack_domain_user_legacy_fallback(self):
"""Test enabling a stack domain user, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
# mock keystone client functions
self._stubs_v3()
self.mock_ks_v3_client.users = self.m.CreateMockAnything()
self.mock_ks_v3_client.users.update(user='user123', enabled=False
).AndReturn(None)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.disable_stack_domain_user(user_id='user123',
project_id='aproject')
def test_disable_stack_domain_user_error_project(self):
"""Test disabling a stack domain user, wrong project."""
@ -783,6 +883,43 @@ class KeystoneClientTest(HeatTestCase):
self.assertEqual('dummy_access2', ec2_cred.access)
self.assertEqual('dummy_secret2', ec2_cred.secret)
def test_create_stack_domain_user_keypair_legacy_fallback(self):
"""Test creating ec2 credentials for domain user, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
self._stubs_v3()
ctx = utils.dummy_context()
ctx.trust_id = None
ex_data = {'access': 'dummy_access2',
'secret': 'dummy_secret2'}
ex_data_json = json.dumps(ex_data)
# stub UUID.hex to match ex_data
self._stub_uuid(['dummy_access2', 'dummy_secret2'])
# mock keystone client credentials functions
self.mock_ks_v3_client.credentials = self.m.CreateMockAnything()
mock_credential = self.m.CreateMockAnything()
mock_credential.id = '1234567'
mock_credential.user_id = 'atestuser2'
mock_credential.blob = ex_data_json
mock_credential.type = 'ec2'
# mock keystone client create function
self.mock_ks_v3_client.credentials.create(
user='atestuser2', type='ec2', data=ex_data_json,
project=ctx.tenant_id).AndReturn(mock_credential)
self.m.ReplayAll()
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
ec2_cred = heat_ks_client.create_stack_domain_user_keypair(
user_id='atestuser2', project_id='aproject')
self.assertEqual('1234567', ec2_cred.id)
self.assertEqual('dummy_access2', ec2_cred.access)
self.assertEqual('dummy_secret2', ec2_cred.secret)
def test_get_ec2_keypair_id(self):
"""Test getting ec2 credential by id."""
@ -936,6 +1073,18 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client.create_stack_domain_project(
stack_name='astack'))
def test_create_stack_domain_project_legacy_fallback(self):
"""Test the create_stack_domain_project function, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
self.assertEqual(ctx.tenant_id,
heat_ks_client.create_stack_domain_project(
stack_name='astack'))
def test_delete_stack_domain_project(self):
"""Test the delete_stack_domain_project function."""
@ -950,6 +1099,16 @@ class KeystoneClientTest(HeatTestCase):
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
heat_ks_client.delete_stack_domain_project(project_id='aprojectid')
def test_delete_stack_domain_project_legacy_fallback(self):
"""Test the delete_stack_domain_project function, fallback path."""
cfg.CONF.clear_override('stack_user_domain')
ctx = utils.dummy_context()
ctx.trust_id = None
heat_ks_client = heat_keystoneclient.KeystoneClient(ctx)
self.assertIsNone(heat_ks_client.delete_stack_domain_project(
project_id='aprojectid'))
def _test_url_for(self, service_url, expected_kwargs, **kwargs):
"""
Helper function for testing url_for depending on different ways to