From 7354e68bde32d2829038a573705dcb3cddf95455 Mon Sep 17 00:00:00 2001 From: Morgan Fainberg Date: Tue, 10 Jun 2014 15:32:10 -0700 Subject: [PATCH] Support using domain_name instead of domain_id When using keystoneclient (heat.common.heat_keystoneclient) to perform actions, support the use of domain_name instead of exclusively domain_id. The new config option to use domain_name instead of domain_id is ``stack_user_domain_name``. If ``stack_user_domain_id`` option is set (renamed from ``stack_user_domain``), the new domain_name-specific option will be ignored. Change-Id: I0d3937a98b95100ccaaaf7a46d3ed722aba27de3 Closes-Bug: #1313003 --- bin/heat-keystone-setup-domain | 2 +- etc/heat/heat.conf.sample | 11 ++- heat/common/config.py | 11 ++- heat/common/heat_keystoneclient.py | 64 +++++++++----- heat/tests/test_heatclient.py | 130 ++++++++++++++++++++++++++--- 5 files changed, 179 insertions(+), 39 deletions(-) diff --git a/bin/heat-keystone-setup-domain b/bin/heat-keystone-setup-domain index 808a8d4d95..72ac4eb7c9 100755 --- a/bin/heat-keystone-setup-domain +++ b/bin/heat-keystone-setup-domain @@ -103,7 +103,7 @@ def main(): c.roles.grant(role=admin_role, user=domain_admin, domain=heat_domain) print("\nPlease update your heat.conf with the following in [DEFAULT]\n") - print("stack_user_domain=%s" % heat_domain.id) + print("stack_user_domain_id=%s" % heat_domain.id) print("stack_domain_admin=%s" % HEAT_DOMAIN_ADMIN) print("stack_domain_admin_password=%s" % HEAT_DOMAIN_PASSWORD) diff --git a/etc/heat/heat.conf.sample b/etc/heat/heat.conf.sample index 60de078ba3..06ca086907 100644 --- a/etc/heat/heat.conf.sample +++ b/etc/heat/heat.conf.sample @@ -107,8 +107,15 @@ #heat_stack_user_role=heat_stack_user # Keystone domain ID which contains heat template-defined -# users. (string value) -#stack_user_domain= +# users. If this option is set, stack_user_domain_name option +# will be ignored. (string value) +# Deprecated group/name - [DEFAULT]/stack_user_domain +#stack_user_domain_id= + +# Keystone domain name which contains heat template-defined +# users. If `stack_user_domain_id` option is set, this option +# is ignored. (string value) +#stack_user_domain_name= # Keystone username, a user with roles sufficient to manage # users and projects in the stack_user_domain. (string value) diff --git a/heat/common/config.py b/heat/common/config.py index f9f962b029..fc51ed1047 100644 --- a/heat/common/config.py +++ b/heat/common/config.py @@ -56,9 +56,16 @@ service_opts = [ cfg.StrOpt('heat_stack_user_role', default="heat_stack_user", help='Keystone role for heat template-defined users.'), - cfg.StrOpt('stack_user_domain', + cfg.StrOpt('stack_user_domain_id', + deprecated_opts=[cfg.DeprecatedOpt('stack_user_domain', + group=None)], help='Keystone domain ID which contains heat template-defined ' - 'users.'), + 'users. If this option is set, stack_user_domain_name ' + 'option will be ignored.'), + cfg.StrOpt('stack_user_domain_name', + help='Keystone domain name which contains heat ' + 'template-defined users. If `stack_user_domain_id` option ' + 'is set, this option is ignored.'), cfg.StrOpt('stack_domain_admin', help='Keystone username, a user with roles sufficient to ' 'manage users and projects in the stack_user_domain.'), diff --git a/heat/common/heat_keystoneclient.py b/heat/common/heat_keystoneclient.py index bf4d639e2b..027e749914 100644 --- a/heat/common/heat_keystoneclient.py +++ b/heat/common/heat_keystoneclient.py @@ -88,19 +88,25 @@ class KeystoneClientV3(object): # 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._stack_domain_is_id = True + self._stack_domain_id = None + self.stack_domain = cfg.CONF.stack_user_domain_id + if not self.stack_domain and cfg.CONF.stack_user_domain_name: + self.stack_domain = cfg.CONF.stack_user_domain_name + self._stack_domain_is_id = False 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 self.stack_domain: 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')) + 'specify "stack_user_domain_id" or ' + '"stack_user_domain_name" without ' + '"stack_domain_admin" and ' + '"stack_domain_admin_password"')) else: - LOG.warning(_('stack_user_domain ID not set in heat.conf ' - 'falling back to using default')) - LOG.debug('Using stack domain %s' % self.stack_domain_id) + LOG.warning(_('stack_user_domain_id or stack_user_domain_name not ' + 'set in heat.conf falling back to using default')) + LOG.debug('Using stack domain %s' % self.stack_domain) @property def client(self): @@ -132,7 +138,11 @@ class KeystoneClientV3(object): c = kc_v3.Client(**admin_creds) # Note we must specify the domain when getting the token # as only a domain scoped token can create projects in the domain - if c.authenticate(domain_id=self.stack_domain_id): + if self._stack_domain_is_id: + auth_kwargs = {'domain_id': self.stack_domain} + else: + auth_kwargs = {'domain_name': self.stack_domain} + if c.authenticate(**auth_kwargs): self._domain_admin_client = c else: LOG.error(_("Domain admin client authentication failed")) @@ -194,10 +204,13 @@ class KeystoneClientV3(object): def _domain_admin_creds(self): creds = { 'username': self.domain_admin_user, - 'user_domain_id': self.stack_domain_id, 'password': self.domain_admin_password, 'auth_url': self.v3_endpoint, 'endpoint': self.v3_endpoint} + if self._stack_domain_is_id: + creds['user_domain_id'] = self.stack_domain + else: + creds['user_domain_name'] = self.stack_domain return creds def _ssl_options(self): @@ -308,7 +321,7 @@ class KeystoneClientV3(object): Returns the keystone ID of the resulting user. """ - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain user create, ' @@ -325,7 +338,7 @@ class KeystoneClientV3(object): # Create user user = self.domain_admin_client.users.create( name=self._get_username(username), password=password, - default_project=project_id, domain=self.stack_domain_id) + default_project=project_id, domain=self.stack_domain) # Add to stack user role LOG.debug("Adding user %(user)s to role %(role)s" % { 'user': user.id, 'role': role_id}) @@ -344,13 +357,22 @@ class KeystoneClientV3(object): def _check_stack_domain_user(self, user_id, project_id, action): """Sanity check that domain/project is correct.""" user = self.domain_admin_client.users.get(user_id) - if user.domain_id != self.stack_domain_id: + + if not self._stack_domain_id: + if self._stack_domain_is_id: + self._stack_domain_id = self.stack_domain + else: + domain = self.domain_admin_client.domains.get( + self.stack_domain) + self._stack_domain_id = domain.id + + if user.domain_id != self._stack_domain_id: raise ValueError(_('User %s in invalid domain') % action) if user.default_project_id != project_id: raise ValueError(_('User %s in invalid project') % action) def delete_stack_domain_user(self, user_id, project_id): - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain user delete, ' @@ -371,7 +393,7 @@ class KeystoneClientV3(object): def create_stack_domain_project(self, stack_id): """Create a project in the heat stack-user domain.""" - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain project, ' @@ -383,12 +405,12 @@ class KeystoneClientV3(object): desc = "Heat stack user project" domain_project = self.domain_admin_client.projects.create( name=project_name, - domain=self.stack_domain_id, + domain=self.stack_domain, description=desc) return domain_project.id def delete_stack_domain_project(self, project_id): - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain project, ' @@ -462,7 +484,7 @@ class KeystoneClientV3(object): secret=data_blob['secret']) def create_stack_domain_user_keypair(self, user_id, project_id): - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain keypair, ' @@ -479,7 +501,7 @@ class KeystoneClientV3(object): def delete_stack_domain_user_keypair(self, user_id, project_id, credential_id): - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain keypair, ' @@ -498,7 +520,7 @@ class KeystoneClientV3(object): self.client.users.update(user=user_id, enabled=True) def disable_stack_domain_user(self, user_id, project_id): - if not self.stack_domain_id: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain disable, ' @@ -508,7 +530,7 @@ class KeystoneClientV3(object): 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: + if not self.stack_domain: # FIXME(shardy): Legacy fallback for folks using old heat.conf # files which lack domain configuration LOG.warning(_('Falling back to legacy non-domain enable, ' diff --git a/heat/tests/test_heatclient.py b/heat/tests/test_heatclient.py index 204cc8f565..1fe131a4aa 100644 --- a/heat/tests/test_heatclient.py +++ b/heat/tests/test_heatclient.py @@ -16,6 +16,7 @@ import uuid import keystoneclient.exceptions as kc_exception from keystoneclient.v3 import client as kc_v3 +from keystoneclient.v3 import domains as kc_v3_domains from oslo.config import cfg from heat.common import exception @@ -36,6 +37,8 @@ class KeystoneClientTest(HeatTestCase): self.mock_admin_client = self.m.CreateMock(kc_v3.Client) self.mock_ks_v3_client = self.m.CreateMock(kc_v3.Client) + self.mock_ks_v3_client_domain_mngr = self.m.CreateMock( + kc_v3_domains.DomainManager) self.m.StubOutWithMock(kc_v3, "Client") dummy_url = 'http://server.test:5000/v2.0' @@ -47,11 +50,14 @@ class KeystoneClientTest(HeatTestCase): group='keystone_authtoken') cfg.CONF.set_override('admin_tenant_name', 'service', group='keystone_authtoken') - cfg.CONF.set_override('stack_user_domain', 'adomain123') + cfg.CONF.set_override('stack_user_domain_id', 'adomain123') cfg.CONF.set_override('stack_domain_admin', 'adminuser123') cfg.CONF.set_override('stack_domain_admin_password', 'adminsecret') self.addCleanup(self.m.VerifyAll) + def _clear_domain_override(self): + cfg.CONF.clear_override('stack_user_domain_id') + def _stub_admin_client(self, auth_ok=True): kc_v3.Client( auth_url='http://server.test:5000/v3', @@ -63,6 +69,7 @@ class KeystoneClientTest(HeatTestCase): password='verybadpass', project_name='service', username='heat').AndReturn(self.mock_admin_client) + self.mock_admin_client.domains = self.mock_ks_v3_client_domain_mngr self.mock_admin_client.authenticate().AndReturn(auth_ok) if auth_ok: self.mock_admin_client.auth_ref = self.m.CreateMockAnything() @@ -79,6 +86,7 @@ class KeystoneClientTest(HeatTestCase): password='adminsecret', user_domain_id='adomain123', username='adminuser123').AndReturn(self.mock_admin_client) + self.mock_admin_client.domains = self.mock_ks_v3_client_domain_mngr self.mock_admin_client.authenticate( domain_id='adomain123').AndReturn(auth_ok) if auth_ok: @@ -218,7 +226,7 @@ class KeystoneClientTest(HeatTestCase): def test_create_stack_domain_user_legacy_fallback(self): """Test creating a stack domain user, fallback path.""" - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -293,7 +301,7 @@ class KeystoneClientTest(HeatTestCase): def test_delete_stack_domain_user_legacy_fallback(self): """Test deleting a stack domain user, fallback path.""" - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -469,7 +477,7 @@ class KeystoneClientTest(HeatTestCase): """Test error path when config lacks domain config.""" - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() cfg.CONF.clear_override('stack_domain_admin') cfg.CONF.clear_override('stack_domain_admin_password') @@ -492,9 +500,10 @@ class KeystoneClientTest(HeatTestCase): ctx.trust_id = None err = self.assertRaises(exception.Error, heat_keystoneclient.KeystoneClient, ctx) - exp_msg = ('heat.conf misconfigured, cannot specify stack_user_domain ' - 'without stack_domain_admin and ' - 'stack_domain_admin_password') + exp_msg = ('heat.conf misconfigured, cannot specify ' + '"stack_user_domain_id" or "stack_user_domain_name" ' + 'without "stack_domain_admin" and ' + '"stack_domain_admin_password"') self.assertIn(exp_msg, str(err)) def test_init_admin_client(self): @@ -705,7 +714,7 @@ class KeystoneClientTest(HeatTestCase): def test_enable_stack_domain_user_legacy_fallback(self): """Test enabling a stack domain user, fallback path.""" - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -770,7 +779,7 @@ class KeystoneClientTest(HeatTestCase): def test_disable_stack_domain_user_legacy_fallback(self): """Test enabling a stack domain user, fallback path.""" - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -842,7 +851,7 @@ class KeystoneClientTest(HeatTestCase): credential_id='acredentialid') def test_delete_stack_domain_user_keypair_legacy_fallback(self): - cfg.CONF.clear_override('stack_user_domain') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -972,7 +981,7 @@ class KeystoneClientTest(HeatTestCase): 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._clear_domain_override() self._stubs_v3() @@ -1165,7 +1174,7 @@ class KeystoneClientTest(HeatTestCase): 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') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -1194,7 +1203,7 @@ class KeystoneClientTest(HeatTestCase): 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') + self._clear_domain_override() ctx = utils.dummy_context() ctx.trust_id = None @@ -1255,3 +1264,98 @@ class KeystoneClientTest(HeatTestCase): } service_url = 'http://regionone.example.com:1234/v1' self._test_url_for(service_url, kwargs) + + +class KeystoneClientTestDomainName(KeystoneClientTest): + def setUp(self): + cfg.CONF.set_override('stack_user_domain_name', 'adomain123') + super(KeystoneClientTestDomainName, self).setUp() + cfg.CONF.clear_override('stack_user_domain_id') + + def _clear_domain_override(self): + cfg.CONF.clear_override('stack_user_domain_name') + + def _stub_domain_admin_client_domain_get(self): + self.mock_ks_v3_client_domain_mngr.get('adomain123').AndReturn( + kc_v3_domains.Domain( + self.mock_ks_v3_client_domain_mngr, + {'id': 'adomain123'}, + loaded=True)) + + def _stub_domain_admin_client(self, auth_ok=True): + kc_v3.Client( + auth_url='http://server.test:5000/v3', + cacert=None, + cert=None, + endpoint='http://server.test:5000/v3', + insecure=False, + key=None, + password='adminsecret', + user_domain_name='adomain123', + username='adminuser123').AndReturn(self.mock_admin_client) + self.mock_admin_client.domains = self.mock_ks_v3_client_domain_mngr + self.mock_admin_client.authenticate( + domain_name='adomain123').AndReturn(auth_ok) + if auth_ok: + self.mock_admin_client.auth_ref = self.m.CreateMockAnything() + self.mock_admin_client.auth_ref.user_id = '1234' + + def test_enable_stack_domain_user_error_project(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_enable_stack_domain_user_error_project() + + def test_delete_stack_domain_user_keypair(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user_keypair() + + def test_delete_stack_domain_user_error_project(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user_error_project() + + def test_delete_stack_domain_user_keypair_error_project(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user_keypair_error_project() + + def test_delete_stack_domain_user(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user() + + def test_enable_stack_domain_user(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_enable_stack_domain_user() + + def test_delete_stack_domain_user_error_domain(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user_error_domain() + + def test_disable_stack_domain_user_error_project(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_disable_stack_domain_user_error_project() + + def test_enable_stack_domain_user_error_domain(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_enable_stack_domain_user_error_domain() + + def test_delete_stack_domain_user_keypair_error_domain(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_delete_stack_domain_user_keypair_error_domain() + + def test_disable_stack_domain_user(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_disable_stack_domain_user() + + def test_disable_stack_domain_user_error_domain(self): + self._stub_domain_admin_client_domain_get() + p = super(KeystoneClientTestDomainName, self) + p.test_disable_stack_domain_user_error_domain()