Add base resolve method for attributes

Current patch adds two new methods to base class:
 * _resolve_all_attributes - for choosing path for resolving, which
   depends on type of attribute (base or custom).
 * _show_resource - method for resolving base 'show' attribute.
 This method should be re-defined for resources, which use clients with
 specific implementations of 'show' commands.

 New attribute for base class was added - entity, which is used for
 resolving 'show' attribute.

 Tests for Neutron resources and Server resource were updated
 corresponding changes mentioned above.

 Hook for stack_resource class is removed in this patch.

Change-Id: I4ee0a53407739e5590c9a8d12bc7c1e84077f4c4
This commit is contained in:
Sergey Kraynev 2015-06-25 09:05:53 -04:00
parent 5903c53fcb
commit 02275f8901
10 changed files with 169 additions and 48 deletions

View File

@ -11,6 +11,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
from oslo_log import log as logging
from oslo_utils import timeutils
import six
@ -159,15 +161,20 @@ def format_resource_attributes(resource, with_attr=None):
# 'console_urls' for nova server, user can view it by taking with_attr
# parameter
if 'show' in six.iterkeys(resolver):
show_attr = resolver['show']
for a in with_attr:
if a not in show_attr:
show_attr[a] = resolve(a, resolver)
return show_attr
else:
attributes = set(list(six.iterkeys(resolver)) + with_attr)
return dict((attr, resolve(attr, resolver))
for attr in attributes)
show_attr = resolve('show', resolver)
# check if 'show' resolved to dictionary. so it's not None
if isinstance(show_attr, collections.Mapping):
for a in with_attr:
if a not in show_attr:
show_attr[a] = resolve(a, resolver)
return show_attr
else:
# remove 'show' attribute if it's None or not a mapping
# then resolve all attributes manually
del resolver._attributes['show']
attributes = set(list(six.iterkeys(resolver)) + with_attr)
return dict((attr, resolve(attr, resolver))
for attr in attributes)
def format_resource_properties(resource):

View File

@ -122,6 +122,10 @@ class Resource(object):
# Resource implementations set this to update policies
update_policy_schema = {}
# Default entity of resource, which is used for during resolving
# show attribute
entity = None
# Description dictionary, that describes the common attributes for all
# resources
base_attributes_schema = {
@ -194,7 +198,7 @@ class Resource(object):
self.attributes_schema.update(self.base_attributes_schema)
self.attributes = attributes.Attributes(self.name,
self.attributes_schema,
self._resolve_attribute)
self._resolve_all_attributes)
self.abandon_in_progress = False
@ -1308,6 +1312,44 @@ class Resource(object):
if not updated_ok:
LOG.warn(_LW('Failed to unlock resource %s'), rsrc.name)
def _resolve_all_attributes(self, attr):
"""Method for resolving all attributes.
This method uses basic _resolve_attribute method for resolving
specific attributes. Base attributes will be resolved with
corresponding method, which should be defined in each resource
class.
:param attr: attribute name, which will be resolved
:returns: method of resource class, which resolve base attribute
"""
if attr in self.base_attributes_schema:
# check resource_id, because usually it is required for getting
# information about resource
if not self.resource_id:
return None
try:
return getattr(self, '_{0}_resource'.format(attr))()
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return None
else:
return self._resolve_attribute(attr)
def _show_resource(self):
"""Default implementation; should be overridden by resources
:returns: the map of resource information or None
"""
if self.entity:
try:
obj = getattr(self.client(), self.entity)
resource = obj.get(self.resource_id)
return resource.to_dict()
except AttributeError as ex:
LOG.warn(_LW("Resolving 'show' attribute has failed : %s"), ex)
return None
def _resolve_attribute(self, name):
"""
Default implementation; should be overridden by resources that expose

View File

@ -119,16 +119,12 @@ class NeutronResource(resource.Resource):
result=_('Resource is not built'))
def _resolve_attribute(self, name):
if self.resource_id:
try:
attributes = self._show_resource()
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return None
if name == 'show':
return attributes
return attributes[name]
try:
attributes = self._show_resource()
except Exception as ex:
self.client_plugin().ignore_not_found(ex)
return None
return attributes[name]
def FnGetRefId(self):
return six.text_type(self.resource_id)

View File

@ -516,6 +516,8 @@ class Server(stack_user.StackUser):
default_client_name = 'nova'
entity = 'servers'
def translation_rules(self):
return [properties.TranslationRule(
self.properties,
@ -934,8 +936,6 @@ class Server(stack_user.StackUser):
return server.accessIPv4
if name == self.ACCESSIPV6:
return server.accessIPv6
if name == self.SHOW:
return server._info
if name == self.CONSOLE_URLS:
return self.client_plugin('nova').get_console_urls(server)

View File

@ -79,9 +79,11 @@ class StackResource(resource.Resource):
if not self.attributes and outputs:
self.attributes_schema = (
attributes.Attributes.schema_from_outputs(outputs))
self.attributes = attributes.Attributes(self.name,
self.attributes_schema,
self._resolve_attribute)
# Note: it can be updated too and for show return dictionary
# with all available outputs
self.attributes = attributes.Attributes(
self.name, self.attributes_schema,
self._resolve_all_attributes)
def _needs_update(self, after, before, after_props, before_props,
prev_resource):
@ -525,10 +527,7 @@ class StackResource(resource.Resource):
return result
def _resolve_attribute(self, name):
# NOTE(skraynev): should be removed in patch with methods,
# which resolve base attributes
if name != 'show':
return self.get_output(name)
return self.get_output(name)
def implementation_signature(self):
schema_names = ([prop for prop in self.properties_schema] +

View File

@ -117,7 +117,7 @@ class TemplateResource(stack_resource.StackResource):
self.attributes_schema.update(self.base_attributes_schema)
self.attributes = attributes.Attributes(self.name,
self.attributes_schema,
self._resolve_attribute)
self._resolve_all_attributes)
def child_params(self):
'''

View File

@ -63,18 +63,10 @@ class GenericResource(resource.Resource):
class ResWithShowAttr(GenericResource):
attributes_schema = {'foo': attributes.Schema('A generic attribute'),
'Foo': attributes.Schema('Another generic attribute'),
'show': attributes.Schema('A dict of all details '
'of attributes')}
def _resolve_attribute(self, name):
if name == 'show':
return {'foo': self.name,
'Foo': self.name,
'Another': self.name}
else:
return self.name
def _show_resource(self):
return {'foo': self.name,
'Foo': self.name,
'Another': self.name}
class ResWithComplexPropsAndAttrs(GenericResource):

View File

@ -120,19 +120,26 @@ class NeutronTest(common.HeatTestCase):
mock_show_resource.side_effect = [{'attr1': 'val1', 'attr2': 'val2'},
{'attr1': 'val1', 'attr2': 'val2'},
{'attr1': 'val1', 'attr2': 'val2'},
qe.NotFound,
qe.NeutronClientException]
qe.NotFound]
res._show_resource = mock_show_resource
nclientplugin = neutron.NeutronClientPlugin(mock.MagicMock())
res.client_plugin = mock.Mock(return_value=nclientplugin)
self.assertEqual({'attr1': 'val1', 'attr2': 'val2'},
res._resolve_attribute('show'))
res.FnGetAtt('show'))
self.assertEqual('val2', res._resolve_attribute('attr2'))
self.assertRaises(KeyError, res._resolve_attribute, 'attr3')
self.assertIsNone(res._resolve_attribute('attr2'))
res.resource_id = None
self.assertIsNone(res._resolve_attribute('show'))
# use local cached object
self.assertEqual({'attr1': 'val1', 'attr2': 'val2'},
res.FnGetAtt('show'))
# reset cache and call 'show' again, so resolver should be used again
# and return None due to resource_id is None
with mock.patch.object(res.attributes, '_resolved_values') as res_vals:
res_vals.return_value = {}
self.assertIsNone(res.FnGetAtt('show'))
class GetSecGroupUuidTest(common.HeatTestCase):

View File

@ -110,12 +110,15 @@ class FormatTest(common.HeatTestCase):
def test_format_resource_attributes(self):
res = self.stack['generic1']
# the _resolve_attribute method of 'generic1' return res.name
# the _resolve_attribute method of 'generic1' returns map with all
# attributes except 'show' (because it's None in this test)
formatted_attributes = api.format_resource_attributes(res)
self.assertEqual(res.name, formatted_attributes)
expected = {'foo': 'generic1', 'Foo': 'generic1'}
self.assertEqual(expected, formatted_attributes)
def test_format_resource_attributes_show_attribute(self):
res = self.stack['generic3']
res.resource_id = 'generic3_id'
formatted_attributes = api.format_resource_attributes(res)
self.assertEqual(3, len(formatted_attributes))
self.assertIn('foo', formatted_attributes)
@ -124,6 +127,7 @@ class FormatTest(common.HeatTestCase):
def test_format_resource_attributes_show_attribute_with_attr(self):
res = self.stack['generic3']
res.resource_id = 'generic3_id'
formatted_attributes = api.format_resource_attributes(
res, with_attr=['c'])
self.assertEqual(4, len(formatted_attributes))

View File

@ -1645,6 +1645,80 @@ class ResourceTest(common.HeatTestCase):
self.assertTrue(mock_get_obj.called)
self.assertFalse(db_res.select_and_update.called)
def create_resource_for_attributes_tests(self):
tmpl = template.Template({
'heat_template_version': '2013-05-23',
'resources': {
'res': {
'type': 'GenericResourceType'
}
}
})
stack = parser.Stack(utils.dummy_context(), 'test', tmpl)
return stack
def test_resolve_attributes_stuff_base_attribute(self):
# check path with resolving base attributes (via 'show' attribute)
stack = self.create_resource_for_attributes_tests()
res = stack['res']
with mock.patch.object(res, '_show_resource') as show_attr:
# return None, if resource_id is None
self.assertIsNone(res.FnGetAtt('show'))
# set resource_id and recheck with re-written _show_resource
res.resource_id = mock.Mock()
show_attr.return_value = 'my attr'
self.assertEqual('my attr', res.FnGetAtt('show'))
self.assertEqual(1, show_attr.call_count)
# clean resolved_values
with mock.patch.object(res.attributes, '_resolved_values') as r_v:
with mock.patch.object(res, 'client_plugin') as client_plug:
r_v.return_value = {}
# generate error during calling _show_resource
show_attr.side_effect = [Exception]
self.assertIsNone(res.FnGetAtt('show'))
self.assertEqual(2, show_attr.call_count)
self.assertEqual(1, client_plug.call_count)
def test_resolve_attributes_stuff_custom_attribute(self):
# check path with resolve_attribute
stack = self.create_resource_for_attributes_tests()
res = stack['res']
with mock.patch.object(res, '_resolve_attribute') as res_attr:
res.FnGetAtt('Foo')
res_attr.assert_called_once_with('Foo')
def test_show_resource(self):
# check default function _show_resource
stack = self.create_resource_for_attributes_tests()
res = stack['res']
# check default value of entity
self.assertIsNone(res.entity)
self.assertIsNone(res.FnGetAtt('show'))
# set entity and recheck
res.resource_id = 'test_resource_id'
res.entity = 'test'
# mock gettring resource info
res.client = mock.Mock()
test_obj = mock.Mock()
test_resource = mock.Mock()
test_resource.to_dict.return_value = {'test': 'info'}
test_obj.get.return_value = test_resource
res.client().test = test_obj
self.assertEqual({'test': 'info'}, res._show_resource())
# check handling AttributeError exception
test_obj.get.side_effect = AttributeError
self.assertIsNone(res._show_resource())
class ResourceAdoptTest(common.HeatTestCase):