Merge "Add support for a resource level external_id"

This commit is contained in:
Jenkins 2016-08-15 04:00:27 +00:00 committed by Gerrit Code Review
commit 0803639788
9 changed files with 282 additions and 19 deletions

View File

@ -634,6 +634,7 @@ the following syntax
depends_on: <resource ID or list of ID> depends_on: <resource ID or list of ID>
update_policy: <update policy> update_policy: <update policy>
deletion_policy: <deletion policy> deletion_policy: <deletion policy>
external_id: <external resource ID>
resource ID resource ID
A resource ID which must be unique within the ``resources`` section of the A resource ID which must be unique within the ``resources`` section of the
@ -671,6 +672,16 @@ deletion_policy
This attribute is optional; the default policy is to delete the physical This attribute is optional; the default policy is to delete the physical
resource when deleting a resource from the stack. resource when deleting a resource from the stack.
external_id
Allows for specifying the resource_id for an existing external
(to the stack) resource. External resources can not depend on other
resources, but we allow other resources depend on external resource.
This attribute is optional.
Note: when this is specified, properties will not be used for building the
resource and the resource is not managed by Heat. This is not possible to
update that attribute. Also resource won't be deleted by heat when stack
is deleted.
Depending on the type of resource, the resource block might include more Depending on the type of resource, the resource block might include more
resource specific data. resource specific data.

View File

@ -153,6 +153,11 @@ class InvalidTemplateReference(HeatException):
' is incorrect.') ' is incorrect.')
class InvalidExternalResourceDependency(HeatException):
msg_fmt = _("Invalid dependency with external %(resource_type)s "
"resource: %(external_id)s")
class EntityNotFound(HeatException): class EntityNotFound(HeatException):
msg_fmt = _("The %(entity)s (%(name)s) could not be found.") msg_fmt = _("The %(entity)s (%(name)s) could not be found.")

View File

@ -10,6 +10,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import collections
import six import six
from heat.common import exception from heat.common import exception
@ -70,6 +71,7 @@ class HOTemplate20130523(template_common.CommonTemplate):
_HOT_TO_CFN_ATTRS.update( _HOT_TO_CFN_ATTRS.update(
{OUTPUT_VALUE: cfn_template.CfnTemplate.OUTPUT_VALUE}) {OUTPUT_VALUE: cfn_template.CfnTemplate.OUTPUT_VALUE})
extra_rsrc_defn = ()
functions = { functions = {
'Fn::GetAZs': cfn_funcs.GetAZs, 'Fn::GetAZs': cfn_funcs.GetAZs,
'get_param': hot_funcs.GetParam, 'get_param': hot_funcs.GetParam,
@ -181,6 +183,7 @@ class HOTemplate20130523(template_common.CommonTemplate):
for attr, attr_value in six.iteritems(attrs): for attr, attr_value in six.iteritems(attrs):
cfn_attr = mapping[attr] cfn_attr = mapping[attr]
if cfn_attr is not None:
cfn_object[cfn_attr] = attr_value cfn_object[cfn_attr] = attr_value
cfn_objects[name] = cfn_object cfn_objects[name] = cfn_object
@ -216,6 +219,50 @@ class HOTemplate20130523(template_common.CommonTemplate):
user_params=user_params, user_params=user_params,
param_defaults=param_defaults) param_defaults=param_defaults)
def validate_resource_definitions(self, stack):
resources = self.t.get(self.RESOURCES) or {}
allowed_keys = set(self._RESOURCE_KEYS)
try:
for name, snippet in resources.items():
path = '.'.join([self.RESOURCES, name])
data = self.parse(stack, snippet, path)
if not self.validate_resource_key_type(self.RES_TYPE,
six.string_types,
'string',
allowed_keys,
name, data):
args = {'name': name, 'type_key': self.RES_TYPE}
msg = _('Resource %(name)s is missing '
'"%(type_key)s"') % args
raise KeyError(msg)
self._validate_resource_key_types(allowed_keys, name, data)
except (TypeError, ValueError) as ex:
raise exception.StackValidationFailed(message=six.text_type(ex))
def _validate_resource_key_types(self, allowed_keys, name, data):
self.validate_resource_key_type(
self.RES_PROPERTIES,
(collections.Mapping, function.Function),
'object', allowed_keys, name, data)
self.validate_resource_key_type(
self.RES_METADATA,
(collections.Mapping, function.Function),
'object', allowed_keys, name, data)
self.validate_resource_key_type(
self.RES_DEPENDS_ON,
collections.Sequence,
'list or string', allowed_keys, name, data)
self.validate_resource_key_type(
self.RES_DELETION_POLICY,
(six.string_types, function.Function),
'string', allowed_keys, name, data)
self.validate_resource_key_type(
self.RES_UPDATE_POLICY,
(collections.Mapping, function.Function),
'object', allowed_keys, name, data)
def resource_definitions(self, stack): def resource_definitions(self, stack):
resources = self.t.get(self.RESOURCES) or {} resources = self.t.get(self.RESOURCES) or {}
parsed_resources = self.parse(stack, resources) parsed_resources = self.parse(stack, resources)
@ -245,7 +292,8 @@ class HOTemplate20130523(template_common.CommonTemplate):
'update_policy': data.get(cls.RES_UPDATE_POLICY), 'update_policy': data.get(cls.RES_UPDATE_POLICY),
'description': None 'description': None
} }
for key in cls.extra_rsrc_defn:
kwargs[key] = data.get(key)
return rsrc_defn.ResourceDefinition(name, **kwargs) return rsrc_defn.ResourceDefinition(name, **kwargs)
def add_resource(self, definition, name=None): def add_resource(self, definition, name=None):
@ -377,7 +425,6 @@ class HOTemplate20160408(HOTemplate20151015):
class HOTemplate20161014(HOTemplate20160408): class HOTemplate20161014(HOTemplate20160408):
CONDITIONS = 'conditions' CONDITIONS = 'conditions'
SECTIONS = HOTemplate20160408.SECTIONS + (CONDITIONS,) SECTIONS = HOTemplate20160408.SECTIONS + (CONDITIONS,)
@ -385,6 +432,20 @@ class HOTemplate20161014(HOTemplate20160408):
_CFN_TO_HOT_SECTIONS = HOTemplate20160408._CFN_TO_HOT_SECTIONS _CFN_TO_HOT_SECTIONS = HOTemplate20160408._CFN_TO_HOT_SECTIONS
_CFN_TO_HOT_SECTIONS.update({ _CFN_TO_HOT_SECTIONS.update({
cfn_template.CfnTemplate.CONDITIONS: CONDITIONS}) cfn_template.CfnTemplate.CONDITIONS: CONDITIONS})
_RESOURCE_KEYS = HOTemplate20160408._RESOURCE_KEYS
_EXT_KEY = (RES_EXTERNAL_ID,) = ('external_id',)
_RESOURCE_KEYS += _EXT_KEY
_RESOURCE_HOT_TO_CFN_ATTRS = HOTemplate20160408._RESOURCE_HOT_TO_CFN_ATTRS
_RESOURCE_HOT_TO_CFN_ATTRS.update({RES_EXTERNAL_ID: None})
extra_rsrc_defn = HOTemplate20160408.extra_rsrc_defn + (RES_EXTERNAL_ID,)
def _validate_resource_key_types(self, allowed_keys, name, data):
super(HOTemplate20161014, self)._validate_resource_key_types(
allowed_keys, name, data)
self.validate_resource_key_type(
self.RES_EXTERNAL_ID,
(six.string_types, function.Function),
'string', allowed_keys, name, data)
deletion_policies = { deletion_policies = {
'Delete': rsrc_defn.ResourceDefinition.DELETE, 'Delete': rsrc_defn.ResourceDefinition.DELETE,

View File

@ -573,10 +573,10 @@ class Resource(object):
if k in immutable_set] if k in immutable_set]
if update_replace_forbidden: if update_replace_forbidden:
mesg = _("Update to properties %(props)s of %(name)s (%(res)s)" msg = _("Update to properties %(props)s of %(name)s (%(res)s)"
) % {'props': ", ".join(sorted(update_replace_forbidden)), ) % {'props': ", ".join(sorted(update_replace_forbidden)),
'res': self.type(), 'name': self.name} 'res': self.type(), 'name': self.name}
raise exception.NotSupported(feature=mesg) raise exception.NotSupported(feature=msg)
if changed_properties_set and self.needs_replace_with_prop_diff( if changed_properties_set and self.needs_replace_with_prop_diff(
changed_properties_set, changed_properties_set,
@ -865,6 +865,13 @@ class Resource(object):
Subclasses should provide a handle_create() method to customise Subclasses should provide a handle_create() method to customise
creation. creation.
""" """
external = self.t.external_id()
if external is not None:
yield self._do_action(self.ADOPT,
resource_data={'resource_id': external})
self.check()
return
action = self.CREATE action = self.CREATE
if (self.action, self.status) != (self.INIT, self.COMPLETE): if (self.action, self.status) != (self.INIT, self.COMPLETE):
exc = exception.Error(_('State %s invalid for create') exc = exception.Error(_('State %s invalid for create')
@ -1198,8 +1205,18 @@ class Resource(object):
if before is None: if before is None:
before = self.frozen_definition() before = self.frozen_definition()
after_props, before_props = self._prepare_update_props( external = after.external_id()
after, before) if before.external_id() != external:
msg = _("Update to property %(prop)s of %(name)s (%(res)s)"
) % {'prop': hot_tmpl.HOTemplate20161014.RES_EXTERNAL_ID,
'res': self.type(), 'name': self.name}
exc = exception.NotSupported(feature=msg)
raise exception.ResourceFailure(exc, self, action)
elif external is not None:
LOG.debug("Skip update on external resource.")
return
after_props, before_props = self._prepare_update_props(after, before)
yield self._break_if_required( yield self._break_if_required(
self.UPDATE, environment.HOOK_PRE_UPDATE) self.UPDATE, environment.HOOK_PRE_UPDATE)

View File

@ -70,7 +70,7 @@ class ResourceDefinitionCore(object):
def __init__(self, name, resource_type, properties=None, metadata=None, def __init__(self, name, resource_type, properties=None, metadata=None,
depends=None, deletion_policy=None, update_policy=None, depends=None, deletion_policy=None, update_policy=None,
description=None): description=None, external_id=None):
"""Initialise with the parsed definition of a resource. """Initialise with the parsed definition of a resource.
Any intrinsic functions present in any of the sections should have been Any intrinsic functions present in any of the sections should have been
@ -84,6 +84,7 @@ class ResourceDefinitionCore(object):
:param deletion_policy: The deletion policy for the resource :param deletion_policy: The deletion policy for the resource
:param update_policy: A dictionary of supplied update policies :param update_policy: A dictionary of supplied update policies
:param description: A string describing the resource :param description: A string describing the resource
:param external_id: A uuid of an external resource
""" """
self.name = name self.name = name
self.resource_type = resource_type self.resource_type = resource_type
@ -93,6 +94,7 @@ class ResourceDefinitionCore(object):
self._depends = depends self._depends = depends
self._deletion_policy = deletion_policy self._deletion_policy = deletion_policy
self._update_policy = update_policy self._update_policy = update_policy
self._external_id = external_id
self._hash = hash(self.resource_type) self._hash = hash(self.resource_type)
self._rendering = None self._rendering = None
@ -124,6 +126,12 @@ class ResourceDefinitionCore(object):
function.Function)) function.Function))
self._hash ^= _hash_data(update_policy) self._hash ^= _hash_data(update_policy)
if external_id is not None:
assert isinstance(external_id, (six.string_types,
function.Function))
self._hash ^= _hash_data(external_id)
self._deletion_policy = self.RETAIN
def freeze(self, **overrides): def freeze(self, **overrides):
"""Return a frozen resource definition, with all functions resolved. """Return a frozen resource definition, with all functions resolved.
@ -147,7 +155,7 @@ class ResourceDefinitionCore(object):
args = ('name', 'resource_type', '_properties', '_metadata', args = ('name', 'resource_type', '_properties', '_metadata',
'_depends', '_deletion_policy', '_update_policy', '_depends', '_deletion_policy', '_update_policy',
'description') 'description', '_external_id')
defn = type(self)(**dict(arg_item(a) for a in args)) defn = type(self)(**dict(arg_item(a) for a in args))
defn._frozen = True defn._frozen = True
@ -171,7 +179,8 @@ class ResourceDefinitionCore(object):
metadata=reparse_snippet(self._metadata), metadata=reparse_snippet(self._metadata),
depends=reparse_snippet(self._depends), depends=reparse_snippet(self._depends),
deletion_policy=reparse_snippet(self._deletion_policy), deletion_policy=reparse_snippet(self._deletion_policy),
update_policy=reparse_snippet(self._update_policy)) update_policy=reparse_snippet(self._update_policy),
external_id=reparse_snippet(self._external_id))
def dep_attrs(self, resource_name): def dep_attrs(self, resource_name):
"""Iterate over attributes of a given resource that this references. """Iterate over attributes of a given resource that this references.
@ -196,16 +205,26 @@ class ResourceDefinitionCore(object):
return stack[res_name] return stack[res_name]
def strict_func_deps(data, datapath): def strict_func_deps(data, datapath):
return six.moves.filter(lambda r: getattr(r, 'strict_dependency', return six.moves.filter(
True), lambda r: getattr(r, 'strict_dependency', True),
function.dependencies(data, datapath)) function.dependencies(data, datapath))
explicit_depends = [] if self._depends is None else self._depends explicit_depends = [] if self._depends is None else self._depends
prop_deps = strict_func_deps(self._properties, path(PROPERTIES))
metadata_deps = strict_func_deps(self._metadata, path(METADATA))
# (ricolin) External resource should not depend on any other resources.
# This operation is not allowed for now.
if self.external_id():
if explicit_depends:
raise exception.InvalidExternalResourceDependency(
external_id=self.external_id(),
resource_type=self.resource_type
)
return itertools.chain()
return itertools.chain((get_resource(dep) for dep in explicit_depends), return itertools.chain((get_resource(dep) for dep in explicit_depends),
strict_func_deps(self._properties, prop_deps, metadata_deps)
path(PROPERTIES)),
strict_func_deps(self._metadata,
path(METADATA)))
def properties(self, schema, context=None): def properties(self, schema, context=None):
"""Return a Properties object representing the resource properties. """Return a Properties object representing the resource properties.
@ -238,6 +257,10 @@ class ResourceDefinitionCore(object):
"""Return the resource metadata.""" """Return the resource metadata."""
return function.resolve(self._metadata) or {} return function.resolve(self._metadata) or {}
def external_id(self):
"""Return the external resource id."""
return function.resolve(self._external_id)
def render_hot(self): def render_hot(self):
"""Return a HOT snippet for the resource definition.""" """Return a HOT snippet for the resource definition."""
if self._rendering is None: if self._rendering is None:
@ -248,6 +271,7 @@ class ResourceDefinitionCore(object):
'deletion_policy': '_deletion_policy', 'deletion_policy': '_deletion_policy',
'update_policy': '_update_policy', 'update_policy': '_update_policy',
'depends_on': '_depends', 'depends_on': '_depends',
'external_id': '_external_id',
} }
def rawattrs(): def rawattrs():

View File

@ -335,6 +335,30 @@ class ResourceTest(common.HeatTestCase):
actual = res.prepare_abandon() actual = res.prepare_abandon()
self.assertEqual(expected, actual) self.assertEqual(expected, actual)
def test_create_from_external(self):
tmpl = rsrc_defn.ResourceDefinition(
'test_resource', 'GenericResourceType',
external_id='f00d')
res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack)
scheduler.TaskRunner(res.create)()
self.assertEqual((res.CHECK, res.COMPLETE), res.state)
self.assertEqual('f00d', res.resource_id)
def test_updated_from_external(self):
tmpl = rsrc_defn.ResourceDefinition('test_resource',
'GenericResourceType')
utmpl = rsrc_defn.ResourceDefinition(
'test_resource', 'GenericResourceType',
external_id='f00d')
res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack)
expected_err_msg = ('NotSupported: resources.test_resource: Update '
'to property external_id of test_resource '
'(GenericResourceType) is not supported.')
err = self.assertRaises(exception.ResourceFailure,
scheduler.TaskRunner(res.update, utmpl)
)
self.assertEqual(expected_err_msg, six.text_type(err))
def test_state_set_invalid(self): def test_state_set_invalid(self):
tmpl = rsrc_defn.ResourceDefinition('test_resource', 'Foo') tmpl = rsrc_defn.ResourceDefinition('test_resource', 'Foo')
res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack) res = generic_rsrc.GenericResource('test_resource', tmpl, self.stack)

View File

@ -15,11 +15,25 @@ import six
import warnings import warnings
from heat.common import exception from heat.common import exception
from heat.common import template_format
from heat.engine.cfn import functions as cfn_funcs from heat.engine.cfn import functions as cfn_funcs
from heat.engine.hot import functions as hot_funcs from heat.engine.hot import functions as hot_funcs
from heat.engine import properties from heat.engine import properties
from heat.engine import rsrc_defn from heat.engine import rsrc_defn
from heat.tests import common from heat.tests import common
from heat.tests import utils
TEMPLATE_WITH_EX_REF_IMPLICIT_DEPEND = '''
heat_template_version: 2016-10-14
resources:
test1:
type: OS::Heat::TestResource
external_id: foobar
properties:
value: {get_resource: test2}
test2:
type: OS::Heat::TestResource
'''
class ResourceDefinitionTest(common.HeatTestCase): class ResourceDefinitionTest(common.HeatTestCase):
@ -76,6 +90,20 @@ class ResourceDefinitionTest(common.HeatTestCase):
stack = {'foo': 'FOO', 'bar': 'BAR'} stack = {'foo': 'FOO', 'bar': 'BAR'}
self.assertEqual(['FOO'], list(rd.dependencies(stack))) self.assertEqual(['FOO'], list(rd.dependencies(stack)))
def test_dependencies_explicit_ext(self):
rd = rsrc_defn.ResourceDefinition('rsrc', 'SomeType', depends=['foo'],
external_id='abc')
stack = {'foo': 'FOO', 'bar': 'BAR'}
self.assertRaises(
exception.InvalidExternalResourceDependency,
rd.dependencies, stack)
def test_dependencies_implicit_ext(self):
t = template_format.parse(TEMPLATE_WITH_EX_REF_IMPLICIT_DEPEND)
stack = utils.parse_stack(t)
rsrc = stack['test1']
self.assertEqual([], list(rsrc.t.dependencies(stack)))
def test_dependencies_explicit_invalid(self): def test_dependencies_explicit_invalid(self):
rd = rsrc_defn.ResourceDefinition('rsrc', 'SomeType', depends=['baz']) rd = rsrc_defn.ResourceDefinition('rsrc', 'SomeType', depends=['baz'])
stack = {'foo': 'FOO', 'bar': 'BAR'} stack = {'foo': 'FOO', 'bar': 'BAR'}

View File

@ -0,0 +1,83 @@
# 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.
from heat_integrationtests.functional import functional_base
class ExternalReferencesTest(functional_base.FunctionalTestsBase):
TEMPLATE = '''
heat_template_version: 2016-10-14
resources:
test1:
type: OS::Heat::TestResource
'''
TEMPLATE_WITH_EX_REF = '''
heat_template_version: 2016-10-14
resources:
test1:
type: OS::Heat::TestResource
external_id: foobar
outputs:
str:
value: {get_resource: test1}
'''
def test_create_with_external_ref(self):
stack_name = self._stack_rand_name()
stack_identifier = self.stack_create(
stack_name=stack_name,
template=self.TEMPLATE_WITH_EX_REF,
files={},
disable_rollback=True,
parameters={},
environment={}
)
stack = self.client.stacks.get(stack_identifier)
self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
expected_resources = {'test1': 'OS::Heat::TestResource'}
self.assertEqual(expected_resources,
self.list_resources(stack_identifier))
stack = self.client.stacks.get(stack_identifier)
self.assertEqual(
[{'description': 'No description given',
'output_key': 'str',
'output_value': 'foobar'}], stack.outputs)
def test_update_with_external_ref(self):
stack_name = self._stack_rand_name()
stack_identifier = self.stack_create(
stack_name=stack_name,
template=self.TEMPLATE,
files={},
disable_rollback=True,
parameters={},
environment={}
)
stack = self.client.stacks.get(stack_identifier)
self._wait_for_stack_status(stack_identifier, 'CREATE_COMPLETE')
expected_resources = {'test1': 'OS::Heat::TestResource'}
self.assertEqual(expected_resources,
self.list_resources(stack_identifier))
stack = self.client.stacks.get(stack_identifier)
self.assertEqual([], stack.outputs)
stack_name = stack_identifier.split('/')[0]
kwargs = {'stack_id': stack_identifier, 'stack_name': stack_name,
'template': self.TEMPLATE_WITH_EX_REF, 'files': {},
'disable_rollback': True, 'parameters': {}, 'environment': {}
}
self.client.stacks.update(**kwargs)
self._wait_for_stack_status(stack_identifier, 'UPDATE_FAILED')

View File

@ -0,0 +1,10 @@
---
prelude: >
Support external resource reference in template.
features:
- Add `external_id` attribute for resource to reference
on an exists external resource. The resource (with
`external_id` attribute) will not able to be updated.
This will keep management rights stay externally.
- This feature only supports templates with version over
`2016-10-14`.