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:
parent
fef832fb2a
commit
4b1969ac5b
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue