Merge "Conditionally expose resources based on available services"

This commit is contained in:
Jenkins 2015-07-08 01:00:44 +00:00 committed by Gerrit Code Review
commit 7d90a2a255
11 changed files with 242 additions and 19 deletions

View File

@ -524,3 +524,14 @@ class KeystoneServiceNameConflict(HeatException):
class SIGHUPInterrupt(HeatException):
msg_fmt = _("System SIGHUP signal received.")
class StackResourceUnavailable(StackValidationFailed):
message = _("Service %(service_name)s does not have required endpoint in "
"service catalog for the resource %(resource_name)s")
def __init__(self, service_name, resource_name):
super(StackResourceUnavailable, self).__init__(
message=self.message % dict(
service_name=service_name,
resource_name=resource_name))

View File

@ -205,3 +205,15 @@ class ClientPlugin(object):
return args
# FIXME(kanagaraj-manickam) Update other client plugins to leverage
# this method (bug 1461041)
def does_endpoint_exist(self,
service_type,
service_name):
endpoint_type = self._get_client_option(service_name,
'endpoint_type')
try:
self.url_for(service_type=service_type,
endpoint_type=endpoint_type)
return True
except exceptions.EndpointNotFound:
return False

View File

@ -32,6 +32,7 @@ from heat.common import short_id
from heat.common import timeutils
from heat.engine import attributes
from heat.engine.cfn import template as cfn_tmpl
from heat.engine import clients
from heat.engine import environment
from heat.engine import event
from heat.engine import function
@ -152,8 +153,18 @@ class Resource(object):
resource_name=name)
except exception.TemplateNotFound:
ResourceClass = template_resource.TemplateResource
assert issubclass(ResourceClass, Resource)
if not ResourceClass.is_service_available(stack.context):
ex = exception.StackResourceUnavailable(
service_name=ResourceClass.default_client_name,
resource_name=name
)
LOG.error(six.text_type(ex))
raise ex
return super(Resource, cls).__new__(ResourceClass)
def __init__(self, name, definition, stack):
@ -514,6 +525,34 @@ class Resource(object):
assert client_name, "Must specify client name"
return self.stack.clients.client_plugin(client_name)
@classmethod
def is_service_available(cls, context):
# NOTE(kanagaraj-manickam): return True to satisfy the cases like
# resource does not have endpoint, such as RandomString, OS::Heat
# resources as they are implemented within the engine.
if cls.default_client_name is None:
return True
try:
client_plugin = clients.Clients(context).client_plugin(
cls.default_client_name)
service_types = client_plugin.service_types
if not service_types:
return True
# NOTE(kanagaraj-manickam): if one of the service_type does
# exist in the keystone, then considered it as available.
for service_type in service_types:
if client_plugin.does_endpoint_exist(
service_type=service_type,
service_name=cls.default_client_name):
return True
except Exception as ex:
LOG.exception(ex)
return False
def keystone(self):
return self.client('keystone')

View File

@ -147,6 +147,8 @@ class HeatTestCase(testscenarios.WithScenarios,
generic_rsrc.ResourceWithCustomConstraint)
resource._register_class('ResourceWithComplexAttributesType',
generic_rsrc.ResourceWithComplexAttributes)
resource._register_class('ResourceWithDefaultClientName',
generic_rsrc.ResourceWithDefaultClientName)
def stub_wallclock(self):
"""

View File

@ -33,6 +33,10 @@ class GenericResource(resource.Resource):
attributes_schema = {'foo': attributes.Schema('A generic attribute'),
'Foo': attributes.Schema('Another generic attribute')}
@classmethod
def is_service_available(cls, context):
return True
def handle_create(self):
LOG.warn(_LW('Creating generic resource (Type "%s")'),
self.type())
@ -177,3 +181,7 @@ class ResourceWithAttributeType(GenericResource):
return "valid_sting"
elif name == 'attr2':
return "invalid_type"
class ResourceWithDefaultClientName(resource.Resource):
default_client_name = 'sample'

View File

@ -50,6 +50,12 @@ class FakeCronTrigger(object):
self.remaining_executions = 3
class MistralCronTriggerTestResource(cron_trigger.CronTrigger):
@classmethod
def is_service_available(cls, context):
return True
class MistralCronTriggerTest(common.HeatTestCase):
def setUp(self):
@ -64,11 +70,11 @@ class MistralCronTriggerTest(common.HeatTestCase):
self.rsrc_defn = resource_defns['cron_trigger']
self.client = mock.Mock()
self.patchobject(cron_trigger.CronTrigger, 'client',
self.patchobject(MistralCronTriggerTestResource, 'client',
return_value=self.client)
def _create_resource(self, name, snippet, stack):
ct = cron_trigger.CronTrigger(name, snippet, stack)
ct = MistralCronTriggerTestResource(name, snippet, stack)
self.client.cron_triggers.create.return_value = FakeCronTrigger(
'my_cron_trigger')
self.client.cron_triggers.get.return_value = FakeCronTrigger(

View File

@ -179,6 +179,12 @@ class FakeWorkflow(object):
self.name = name
class MistralWorkFlowTestResource(workflow.Workflow):
@classmethod
def is_service_available(cls, context):
return True
class TestMistralWorkflow(common.HeatTestCase):
def setUp(self):
@ -193,7 +199,7 @@ class TestMistralWorkflow(common.HeatTestCase):
self.rsrc_defn = resource_defns['workflow']
self.mistral = mock.Mock()
self.patchobject(workflow.Workflow, 'mistral',
self.patchobject(MistralWorkFlowTestResource, 'mistral',
return_value=self.mistral)
self.patches = []
@ -216,7 +222,7 @@ class TestMistralWorkflow(common.HeatTestCase):
patch.stop()
def _create_resource(self, name, snippet, stack):
wf = workflow.Workflow(name, snippet, stack)
wf = MistralWorkFlowTestResource(name, snippet, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('test_stack-workflow-b5fiekfci3yc')]
scheduler.TaskRunner(wf.create)()
@ -234,7 +240,7 @@ class TestMistralWorkflow(common.HeatTestCase):
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -269,7 +275,7 @@ class TestMistralWorkflow(common.HeatTestCase):
rsrc_defns = stack.t.resource_definitions(stack)['workflow']
wf = workflow.Workflow('workflow', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('workflow', rsrc_defns, stack)
exc = self.assertRaises(exception.StackValidationFailed,
wf.validate)
@ -281,7 +287,7 @@ class TestMistralWorkflow(common.HeatTestCase):
rsrc_defns = stack.t.resource_definitions(stack)['workflow']
wf = workflow.Workflow('workflow', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('workflow', rsrc_defns, stack)
self.mistral.workflows.create.side_effect = Exception('boom!')
@ -379,7 +385,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_full)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -394,7 +400,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_full)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -417,7 +423,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_full)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -434,7 +440,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_full)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -456,7 +462,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_full)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['create_vm']
wf = workflow.Workflow('create_vm', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('create_vm', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('create_vm')]
scheduler.TaskRunner(wf.create)()
@ -472,7 +478,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_with_params)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['workflow']
wf = workflow.Workflow('workflow', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('workflow', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('workflow')]
scheduler.TaskRunner(wf.create)()
@ -487,7 +493,7 @@ class TestMistralWorkflow(common.HeatTestCase):
tmpl = template_format.parse(workflow_template_with_params_override)
stack = utils.parse_stack(tmpl)
rsrc_defns = stack.t.resource_definitions(stack)['workflow']
wf = workflow.Workflow('workflow', rsrc_defns, stack)
wf = MistralWorkFlowTestResource('workflow', rsrc_defns, stack)
self.mistral.workflows.create.return_value = [
FakeWorkflow('workflow')]
scheduler.TaskRunner(wf.create)()

View File

@ -107,6 +107,10 @@ class NeutronTest(common.HeatTestCase):
class SomeNeutronResource(nr.NeutronResource):
properties_schema = {}
@classmethod
def is_service_available(cls, context):
return True
tmpl = rsrc_defn.ResourceDefinition('test_res', 'Foo')
stack = mock.MagicMock()
stack.has_cache_data = mock.Mock(return_value=False)

View File

@ -43,18 +43,25 @@ magnum_template = '''
RESOURCE_TYPE = 'OS::Magnum::BayModel'
class MagnumBayModelTestResource(baymodel.BayModel):
@classmethod
def is_service_available(cls, context):
return True
class TestMagnumBayModel(common.HeatTestCase):
def setUp(self):
super(TestMagnumBayModel, self).setUp()
self.ctx = utils.dummy_context()
resource._register_class(RESOURCE_TYPE, baymodel.BayModel)
resource._register_class(RESOURCE_TYPE, MagnumBayModelTestResource)
t = template_format.parse(magnum_template)
self.stack = utils.parse_stack(t)
resource_defns = self.stack.t.resource_definitions(self.stack)
self.rsrc_defn = resource_defns['test_baymodel']
self.client = mock.Mock()
self.patchobject(baymodel.BayModel, 'client', return_value=self.client)
self.patchobject(MagnumBayModelTestResource, 'client',
return_value=self.client)
self.stub_FlavorConstraint_validate()
self.stub_KeypairConstraint_validate()
self.stub_ImageConstraint_validate()
@ -65,7 +72,7 @@ class TestMagnumBayModel(common.HeatTestCase):
self.test_bay_model = self.stack['test_baymodel']
value = mock.MagicMock(uuid=self.resource_id)
self.client.baymodels.create.return_value = value
bm = baymodel.BayModel(name, snippet, stack)
bm = MagnumBayModelTestResource(name, snippet, stack)
scheduler.TaskRunner(bm.create)()
return bm

View File

@ -11,7 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import mock
import mox
from heat.common import identifier
@ -239,7 +239,9 @@ class WaitCondMetadataUpdateTest(common.HeatTestCase):
self.m.StubOutWithMock(scheduler.TaskRunner, '_sleep')
return stack
def test_wait_meta(self):
@mock.patch(('heat.engine.resources.aws.ec2.instance.Instance'
'.is_service_available'))
def test_wait_meta(self, mock_is_service_available):
'''
1 create stack
2 assert empty instance metadata
@ -247,6 +249,7 @@ class WaitCondMetadataUpdateTest(common.HeatTestCase):
4 assert valid waitcond metadata
5 assert valid instance metadata
'''
mock_is_service_available.return_value = True
self.stack = self.create_stack()
watch = self.stack['WC']

View File

@ -28,6 +28,7 @@ from heat.common import timeutils
from heat.db import api as db_api
from heat.engine import attributes
from heat.engine.cfn import functions as cfn_funcs
from heat.engine import clients
from heat.engine import constraints
from heat.engine import dependencies
from heat.engine import environment
@ -2263,3 +2264,127 @@ class ResourceHookTest(common.HeatTestCase):
res.has_hook = mock.Mock(return_value=False)
self.assertRaises(exception.ResourceActionNotSupported,
res.signal, {'unset_hook': 'pre-create'})
class ResourceAvailabilityTest(common.HeatTestCase):
def _mock_client_plugin(self, service_types=[], is_available=True):
mock_client_plugin = mock.Mock()
mock_service_types = mock.PropertyMock(return_value=service_types)
type(mock_client_plugin).service_types = mock_service_types
mock_client_plugin.does_endpoint_exist = mock.Mock(
return_value=is_available)
return mock_service_types, mock_client_plugin
def test_default_true_with_default_client_name_none(self):
'''
When default_client_name is None, resource is considered as available.
'''
with mock.patch(('heat.tests.generic_resource'
'.ResourceWithDefaultClientName.default_client_name'),
new_callable=mock.PropertyMock) as mock_client_name:
mock_client_name.return_value = None
self.assertTrue((generic_rsrc.ResourceWithDefaultClientName.
is_service_available(context=mock.Mock())))
@mock.patch.object(clients.OpenStackClients, 'client_plugin')
def test_default_true_empty_service_types(
self,
mock_client_plugin_method):
'''
When service_types is empty list, resource is considered as available.
'''
mock_service_types, mock_client_plugin = self._mock_client_plugin()
mock_client_plugin_method.return_value = mock_client_plugin
self.assertTrue(
generic_rsrc.ResourceWithDefaultClientName.is_service_available(
context=mock.Mock()))
mock_client_plugin_method.assert_called_once_with(
generic_rsrc.ResourceWithDefaultClientName.default_client_name)
mock_service_types.assert_called_once_with()
@mock.patch.object(clients.OpenStackClients, 'client_plugin')
def test_service_deployed(
self,
mock_client_plugin_method):
'''
When the service is deployed, resource is considered as available.
'''
mock_service_types, mock_client_plugin = self._mock_client_plugin(
['test_type']
)
mock_client_plugin_method.return_value = mock_client_plugin
self.assertTrue(
generic_rsrc.ResourceWithDefaultClientName.is_service_available(
context=mock.Mock()))
mock_client_plugin_method.assert_called_once_with(
generic_rsrc.ResourceWithDefaultClientName.default_client_name)
mock_service_types.assert_called_once_with()
mock_client_plugin.does_endpoint_exist.assert_called_once_with(
service_type='test_type',
service_name=(generic_rsrc.ResourceWithDefaultClientName
.default_client_name)
)
@mock.patch.object(clients.OpenStackClients, 'client_plugin')
def test_service_not_deployed(
self,
mock_client_plugin_method):
'''
When the service is not deployed, resource is considered as
unavailable.
'''
mock_service_types, mock_client_plugin = self._mock_client_plugin(
['test_type_un_deployed'],
False
)
mock_client_plugin_method.return_value = mock_client_plugin
self.assertFalse(
generic_rsrc.ResourceWithDefaultClientName.is_service_available(
context=mock.Mock()))
mock_client_plugin_method.assert_called_once_with(
generic_rsrc.ResourceWithDefaultClientName.default_client_name)
mock_service_types.assert_called_once_with()
mock_client_plugin.does_endpoint_exist.assert_called_once_with(
service_type='test_type_un_deployed',
service_name=(generic_rsrc.ResourceWithDefaultClientName
.default_client_name)
)
def test_service_not_deployed_throws_exception(self):
'''
When the service is not deployed, make sure resource is throwing
StackResourceUnavailable exception.
'''
with mock.patch.object(
generic_rsrc.ResourceWithDefaultClientName,
'is_service_available') as mock_method:
mock_method.return_value = False
definition = rsrc_defn.ResourceDefinition(
name='Test Resource',
resource_type=mock.Mock())
mock_stack = mock.MagicMock()
ex = self.assertRaises(
exception.StackResourceUnavailable,
generic_rsrc.ResourceWithDefaultClientName.__new__,
cls=generic_rsrc.ResourceWithDefaultClientName,
name='test_stack',
definition=definition,
stack=mock_stack)
msg = ('Service sample does not have required endpoint in service'
' catalog for the resource test_stack')
self.assertEqual(msg,
six.text_type(ex),
'invalid exception message')
# Make sure is_service_available is called on the right class
mock_method.assert_called_once_with(mock_stack.context)