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
This commit is contained in:
Steven Hardy 2014-02-10 18:09:41 +00:00
parent fef832fb2a
commit 4b1969ac5b
5 changed files with 239 additions and 80 deletions

View File

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

View File

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

View File

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

View File

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

View File

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