From 4b1969ac5b69f2cf95cceb2e947da4342395a931 Mon Sep 17 00:00:00 2001 From: Steven Hardy Date: Mon, 10 Feb 2014 18:09:41 +0000 Subject: [PATCH] migrate StackUser base class to stack domain users Migrates the user/credential interfaces to stack domain users, so the user is created in the instance-user domain not the domain of the user creating the stack. Partial-Bug: #1089261 blueprint: instance-users Change-Id: If4c57144a32050a8d0b145444a84896cce53908b --- heat/engine/stack_user.py | 45 +++++++-- heat/tests/fakes.py | 26 ++++-- heat/tests/generic_resource.py | 10 ++ heat/tests/test_signal.py | 73 ++------------- heat/tests/test_stack_user.py | 165 +++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 80 deletions(-) create mode 100644 heat/tests/test_stack_user.py diff --git a/heat/engine/stack_user.py b/heat/engine/stack_user.py index 384bd74dc1..656ca045fc 100644 --- a/heat/engine/stack_user.py +++ b/heat/engine/stack_user.py @@ -28,21 +28,38 @@ logger = logging.getLogger(__name__) class StackUser(resource.Resource): - # Subclasses create a user, and optionally keypair - # associated with a resource in a stack + # Subclasses create a user, and optionally keypair associated with a + # resource in a stack. Users are created in the heat stack user domain + # (in a project specific to the stack) + def __init__(self, name, json_snippet, stack): + super(StackUser, self).__init__(name, json_snippet, stack) + self.password = None + def handle_create(self): self._create_user() def _create_user(self): - user_id = self.keystone().create_stack_user( - self.physical_resource_name()) + # Check for stack user project, create if not yet set + if not self.stack.stack_user_project_id: + project_id = self.keystone().create_stack_domain_project( + stack_name=self.stack.name) + self.stack.set_stack_user_project_id(project_id) + # Create a keystone user in the stack domain project + user_id = self.keystone().create_stack_domain_user( + username=self.physical_resource_name(), + password=self.password, + project_id=self.stack.stack_user_project_id) + + # Store the ID in resource data, for compatibility with SignalResponder db_api.resource_data_set(self, 'user_id', user_id) def _get_user_id(self): try: return db_api.resource_data_get(self, 'user_id') except exception.NotFound: + # FIXME(shardy): This is a legacy hack for backwards compatibility + # remove after an appropriate transitional period... # Assume this is a resource that was created with # a previous version of heat and that the resource_id # is the user_id @@ -58,11 +75,21 @@ class StackUser(resource.Resource): if user_id is None: return try: - self.keystone().delete_stack_user(user_id) + self.keystone().delete_stack_domain_user( + user_id=user_id, project_id=self.stack.stack_user_project_id) except kc_exception.NotFound: pass - for data_key in ('ec2_signed_url', 'access_key', 'secret_key', - 'credential_id'): + except ValueError: + # FIXME(shardy): This is a legacy delete path for backwards + # compatibility with resources created before the migration + # to stack_user.StackUser domain users. After an appropriate + # transitional period, this should be removed. + logger.warning(_('Reverting to legacy user delete path')) + try: + self.keystone().delete_stack_user(user_id) + except kc_exception.NotFound: + pass + for data_key in ('credential_id', 'access_key', 'secret_key'): try: db_api.resource_data_delete(self, data_key) except exception.NotFound: @@ -73,7 +100,8 @@ class StackUser(resource.Resource): # an ec2 keypair associated with the user, the resulting keys are # stored in resource_data user_id = self._get_user_id() - kp = self.keystone().create_ec2_keypair(user_id) + kp = self.keystone().create_stack_domain_user_keypair( + user_id=user_id, project_id=self.stack.stack_user_project_id) if not kp: raise exception.Error(_("Error creating ec2 keypair for user %s") % user_id) @@ -84,3 +112,4 @@ class StackUser(resource.Resource): redact=True) db_api.resource_data_set(self, 'secret_key', kp.secret, redact=True) + return kp diff --git a/heat/tests/fakes.py b/heat/tests/fakes.py index 235df443dd..162ee3f2ad 100644 --- a/heat/tests/fakes.py +++ b/heat/tests/fakes.py @@ -86,7 +86,13 @@ class FakeKeystoneClient(object): self.access = access self.secret = secret self.credential_id = credential_id - self.creds = None + + class FakeCred(object): + id = self.credential_id + access = self.access + secret = self.secret + self.creds = FakeCred() + self.auth_token = 'abcd1234' def create_stack_user(self, username, password=''): @@ -107,12 +113,6 @@ class FakeKeystoneClient(object): def create_ec2_keypair(self, user_id): if user_id == self.user_id: - if not self.creds: - class FakeCred(object): - id = self.credential_id - access = self.access - secret = self.secret - self.creds = FakeCred() return self.creds def delete_ec2_keypair(self, user_id, access): @@ -142,3 +142,15 @@ class FakeKeystoneClient(object): def delete_stack_domain_project(self, project_id): pass + + def create_stack_domain_project(self, stack_name): + return 'aprojectid' + + def create_stack_domain_user(self, username, project_id, password=None): + return self.user_id + + def delete_stack_domain_user(self, user_id, project_id): + pass + + def create_stack_domain_user_keypair(self, user_id, project_id): + return self.creds diff --git a/heat/tests/generic_resource.py b/heat/tests/generic_resource.py index b1fd18b415..9519434f57 100644 --- a/heat/tests/generic_resource.py +++ b/heat/tests/generic_resource.py @@ -14,6 +14,7 @@ from heat.engine import resource from heat.engine import signal_responder +from heat.engine import stack_user from heat.openstack.common import log as logging from heat.openstack.common.gettextutils import _ @@ -101,3 +102,12 @@ class SignalResource(signal_responder.SignalResponder): def _resolve_attribute(self, name): if name == 'AlarmUrl' and self.resource_id is not None: return unicode(self._get_signed_url()) + + +class StackUserResource(stack_user.StackUser): + properties_schema = {} + attributes_schema = {} + + def handle_create(self): + super(StackUserResource, self).handle_create() + self.resource_id_set(self._get_user_id()) diff --git a/heat/tests/test_signal.py b/heat/tests/test_signal.py index 4e506975ab..8b5da99a41 100644 --- a/heat/tests/test_signal.py +++ b/heat/tests/test_signal.py @@ -30,7 +30,7 @@ from heat.engine import clients from heat.engine import parser from heat.engine import resource from heat.engine import scheduler -from heat.engine import signal_responder as sr +from heat.engine import stack_user from keystoneclient import exceptions as kc_exceptions @@ -84,47 +84,20 @@ class SignalTest(HeatTestCase): stack.store() if stub: - self.m.StubOutWithMock(sr.SignalResponder, 'keystone') - sr.SignalResponder.keystone().MultipleTimes().AndReturn( + self.m.StubOutWithMock(stack_user.StackUser, 'keystone') + stack_user.StackUser.keystone().MultipleTimes().AndReturn( self.fc) self.m.ReplayAll() return stack - @utils.stack_delete_after - def test_handle_create_fail_user(self): - self.stack = self.create_stack(stack_name='create_fail_user', - stub=False) - - class FakeKeystoneClientFail(fakes.FakeKeystoneClient): - def create_stack_user(self, name): - raise kc_exceptions.Forbidden("Denied!") - - self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') - clients.OpenStackClients.keystone().MultipleTimes().AndReturn( - FakeKeystoneClientFail()) - self.m.ReplayAll() - - self.stack.create() - - rsrc = self.stack['signal_handler'] - self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) - self.assertIn('Forbidden', rsrc.status_reason) - self.m.VerifyAll() - @utils.stack_delete_after def test_handle_create_fail_keypair_raise(self): - self.stack = self.create_stack(stack_name='create_fail_keypair', - stub=False) + self.stack = self.create_stack(stack_name='create_fail_keypair') - class FakeKeystoneClientFail(fakes.FakeKeystoneClient): - def create_ec2_keypair(self, name): - raise kc_exceptions.Forbidden("Denied!") - - self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') - clients.OpenStackClients.keystone().MultipleTimes().AndReturn( - FakeKeystoneClientFail(user_id='123xyz')) + self.m.StubOutWithMock(stack_user.StackUser, '_create_keypair') + stack_user.StackUser._create_keypair().AndRaise(Exception('Failed')) self.m.ReplayAll() self.stack.create() @@ -132,32 +105,8 @@ class SignalTest(HeatTestCase): rsrc = self.stack['signal_handler'] rs_data = db_api.resource_data_get_all(rsrc) self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) - self.assertIn('Forbidden', rsrc.status_reason) - self.assertEqual('123xyz', rs_data.get('user_id')) - self.assertIsNone(rsrc.resource_id) - self.m.VerifyAll() - - @utils.stack_delete_after - def test_handle_create_fail_keypair_none(self): - self.stack = self.create_stack(stack_name='create_fail_keypair', - stub=False) - - class FakeKeystoneClientFail(fakes.FakeKeystoneClient): - def create_ec2_keypair(self, name): - return None - - self.m.StubOutWithMock(clients.OpenStackClients, 'keystone') - clients.OpenStackClients.keystone().MultipleTimes().AndReturn( - FakeKeystoneClientFail(user_id='123xyz')) - self.m.ReplayAll() - - self.stack.create() - - rsrc = self.stack['signal_handler'] - rs_data = db_api.resource_data_get_all(rsrc) - self.assertEqual((rsrc.CREATE, rsrc.FAILED), rsrc.state) - self.assertIn('Error creating ec2 keypair', rsrc.status_reason) - self.assertEqual('123xyz', rs_data.get('user_id')) + self.assertIn('Failed', rsrc.status_reason) + self.assertEqual('1234', rs_data.get('user_id')) self.assertIsNone(rsrc.resource_id) self.m.VerifyAll() @@ -187,12 +136,6 @@ class SignalTest(HeatTestCase): self.assertEqual('1234', rs_data.get('user_id')) self.assertEqual(rsrc.resource_id, rs_data.get('user_id')) self.assertEqual(4, len(rs_data.keys())) - - # And that we remove it on delete - scheduler.TaskRunner(rsrc.delete)() - self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) - rs_data = db_api.resource_data_get_all(rsrc) - self.assertEqual(1, len(rs_data.keys())) self.m.VerifyAll() @utils.stack_delete_after diff --git a/heat/tests/test_stack_user.py b/heat/tests/test_stack_user.py new file mode 100644 index 0000000000..ce9b5e6fcd --- /dev/null +++ b/heat/tests/test_stack_user.py @@ -0,0 +1,165 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from heat.tests import fakes +from heat.tests import generic_resource +from heat.tests.common import HeatTestCase +from heat.tests import utils + +from heat.common import short_id +from heat.common import template_format + +from heat.db import api as db_api + +from heat.engine import resource +from heat.engine import scheduler +from heat.engine import stack_user + +from keystoneclient import exceptions as kc_exceptions + + +user_template = ''' +heat_template_version: 2013-05-23 + +resources: + user: + type: StackUserResourceType +''' + + +class StackUserTest(HeatTestCase): + + def setUp(self): + super(StackUserTest, self).setUp() + utils.setup_dummy_db() + resource._register_class('StackUserResourceType', + generic_resource.StackUserResource) + self.fc = fakes.FakeKeystoneClient() + self.resource_id = str(uuid.uuid4()) + + def tearDown(self): + super(StackUserTest, self).tearDown() + utils.reset_dummy_db() + + def _user_create(self, stack_name, project_id, user_id, + resource_name='user', create_project=True): + t = template_format.parse(user_template) + stack = utils.parse_stack(t, stack_name=stack_name) + rsrc = stack[resource_name] + + self.m.StubOutWithMock(stack_user.StackUser, 'keystone') + stack_user.StackUser.keystone().MultipleTimes().AndReturn(self.fc) + + if create_project: + self.m.StubOutWithMock(fakes.FakeKeystoneClient, + 'create_stack_domain_project') + fakes.FakeKeystoneClient.create_stack_domain_project( + stack_name=stack_name).AndReturn(project_id) + else: + stack.set_stack_user_project_id(project_id) + + self.m.StubOutWithMock(short_id, 'get_id') + short_id.get_id(self.resource_id).AndReturn('aabbcc') + + self.m.StubOutWithMock(fakes.FakeKeystoneClient, + 'create_stack_domain_user') + expected_username = '%s-%s-%s' % (stack_name, resource_name, 'aabbcc') + fakes.FakeKeystoneClient.create_stack_domain_user( + username=expected_username, password=None, + project_id=project_id).AndReturn(user_id) + + return rsrc + + def test_handle_create_no_stack_project(self): + rsrc = self._user_create(stack_name='user_test123', + project_id='aproject123', + user_id='auser123') + self.m.ReplayAll() + + with utils.UUIDStub(self.resource_id): + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + rs_data = db_api.resource_data_get_all(rsrc) + self.assertEqual({'user_id': 'auser123'}, rs_data) + self.m.VerifyAll() + + def test_handle_create_existing_project(self): + rsrc = self._user_create(stack_name='user_test456', + project_id='aproject456', + user_id='auser456', + create_project=False) + self.m.ReplayAll() + + with utils.UUIDStub(self.resource_id): + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + rs_data = db_api.resource_data_get_all(rsrc) + self.assertEqual({'user_id': 'auser456'}, rs_data) + self.m.VerifyAll() + + def test_handle_delete(self): + rsrc = self._user_create(stack_name='user_testdel', + project_id='aprojectdel', + user_id='auserdel') + + self.m.StubOutWithMock(fakes.FakeKeystoneClient, + 'delete_stack_domain_user') + fakes.FakeKeystoneClient.delete_stack_domain_user( + user_id='auserdel', project_id='aprojectdel').AndReturn(None) + + self.m.ReplayAll() + + with utils.UUIDStub(self.resource_id): + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_handle_delete_not_found(self): + rsrc = self._user_create(stack_name='user_testdel2', + project_id='aprojectdel2', + user_id='auserdel2') + + self.m.StubOutWithMock(fakes.FakeKeystoneClient, + 'delete_stack_domain_user') + fakes.FakeKeystoneClient.delete_stack_domain_user( + user_id='auserdel2', project_id='aprojectdel2').AndRaise( + kc_exceptions.NotFound) + + self.m.ReplayAll() + + with utils.UUIDStub(self.resource_id): + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll() + + def test_handle_delete_noid(self): + rsrc = self._user_create(stack_name='user_testdel2', + project_id='aprojectdel2', + user_id='auserdel2') + + self.m.ReplayAll() + + with utils.UUIDStub(self.resource_id): + scheduler.TaskRunner(rsrc.create)() + self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state) + db_api.resource_data_delete(rsrc, 'user_id') + scheduler.TaskRunner(rsrc.delete)() + self.assertEqual((rsrc.DELETE, rsrc.COMPLETE), rsrc.state) + self.m.VerifyAll()