Merge "Conditionally expose resources based on available services"
This commit is contained in:
commit
7d90a2a255
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue