Make resource a dict subclass usable by shade layer

Users of shade/openstack.cloud expect munch.Munch objects to be returned
by all the methods. These objects are basically dicts that allow
object-notation access.

In an attempt to minimize the size of the mapping layer between shade
and sdk code, make the Resource objects behave like dicts as well as
like objects. We want the attributes exposed as dictionary attributes
to be the data model defined with the Component descriptors, so we wind
up needing to override the munch methods anyway - at which point munch
itself ceases being valuable.

he overall idea here is allowing the shade-layer API to return SDK
Resource objects directly without transforming them with to_dict first.
If we can make that work, it means that a user could use shade and sdk
methods interchangably, being able to do something like:

  # Create server using shade layer
  server = conn.create_server('my-server', auto_ip)
  # reboot server using sdk
  conn.compute.servers.reboot(server, 'HARD')

There is a REALLY ugly hack in here (with a comment) that's needed to
make json.dumps(server) work. It's not a hack we should actually use,
but my brain is too tired right now to figure out how to make it
actually work.

There is a latent bug in image. A TODO has been left.

Co-Authored-By: Rosario Di Somma <rosario.disomma@dreamhost.com>
Change-Id: Ib48f0bfa74231bf3171a798a179f6606177c95b0
This commit is contained in:
Monty Taylor 2018-08-06 10:05:54 -05:00
parent d4db52f2fa
commit 2f97394847
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
6 changed files with 115 additions and 8 deletions

View File

@ -84,8 +84,11 @@ class Image(resource.Resource):
name = resource.Body('name')
#: The ID of the owner, or project, of the image.
owner_id = resource.Body('owner')
# TODO(mordred) This is not how this works in v2. I mean, it's how it
# should work, but it's not. We need to fix properties. They work right
# in shade, so we can draw some logic from there.
#: Properties, if any, that are associated with the image.
properties = resource.Body('properties', type=dict)
properties = resource.Body('properties')
#: The size of the image data, in bytes.
size = resource.Body('size', type=int)
#: When present, Glance will attempt to store the disk image data in the

View File

@ -62,7 +62,8 @@ class Proxy(six.with_metaclass(_meta.ProxyMeta, _adapter.OpenStackSDKAdapter)):
if value is None:
# Create a bare resource
res = resource_type.new(**attrs)
elif isinstance(value, dict):
elif (isinstance(value, dict)
and not isinstance(value, resource.Resource)):
res = resource_type._from_munch(value)
res._update(**attrs)
elif not isinstance(value, resource_type):

View File

@ -38,6 +38,7 @@ from keystoneauth1 import adapter
from keystoneauth1 import discover
import munch
from requests import structures
import six
from openstack import _log
from openstack import exceptions
@ -124,7 +125,7 @@ class _BaseComponent(object):
def __get__(self, instance, owner):
if instance is None:
return None
return self
attributes = getattr(instance, self.key)
@ -318,7 +319,16 @@ class QueryParameters(object):
return result
class Resource(object):
class Resource(dict):
# TODO(mordred) While this behaves mostly like a munch for the purposes
# we need, sub-resources, such as Server.security_groups, which is a list
# of dicts, will contain lists of real dicts, not lists of munch-like dict
# objects. We should probably figure out a Resource class, perhaps
# SubResource or something, that we can use to define the data-model of
# complex object attributes where those attributes are not already covered
# by a different resource such as Server.image which should ultimately
# be an Image. We subclass dict so that things like json.dumps and pprint
# will work properly.
#: Singular form of key for resource.
resource_key = None
@ -419,6 +429,13 @@ class Resource(object):
attributes=computed,
synchronized=_synchronized)
# TODO(mordred) This is terrible, but is a hack at the moment to ensure
# json.dumps works. The json library does basically if not obj: and
# obj.items() ... but I think the if not obj: is short-circuiting down
# in the C code and thus since we don't store the data in self[] it's
# always False even if we override __len__ or __bool__.
dict.update(self, self.to_dict())
def __repr__(self):
pairs = [
"%s=%s" % (k, v if v is not None else 'None')
@ -462,6 +479,49 @@ class Resource(object):
else:
return object.__getattribute__(self, name)
def __getitem__(self, name):
"""Provide dictionary access for elements of the data model."""
# Check the class, since BaseComponent is a descriptor and thus
# behaves like its wrapped content. If we get it on the class,
# it returns the BaseComponent itself, not the results of __get__.
real_item = getattr(self.__class__, name, None)
if isinstance(real_item, _BaseComponent):
return getattr(self, name)
raise KeyError(name)
def __delitem__(self, name):
delattr(self, name)
def __setitem__(self, name, value):
real_item = getattr(self.__class__, name, None)
if isinstance(real_item, _BaseComponent):
self.__setattr__(name, value)
else:
raise KeyError(
"{name} is not found. {module}.{cls} objects do not support"
" setting arbitrary keys through the"
" dict interface.".format(
module=self.__module__,
cls=self.__class__.__name__,
name=name))
def keys(self):
# NOTE(mordred) In python2, dict.keys returns a list. In python3 it
# returns a dict_keys view. For 2, we can return a list from the
# itertools chain. In 3, return the chain so it's at least an iterator.
# It won't strictly speaking be an actual dict_keys, so it's possible
# we may want to get more clever, but for now let's see how far this
# will take us.
underlying_keys = itertools.chain(
self._body.attributes.keys(),
self._header.attributes.keys(),
self._uri.attributes.keys(),
self._computed.attributes.keys())
if six.PY2:
return list(underlying_keys)
else:
return underlying_keys
def _update(self, **attrs):
"""Given attributes, update them on this instance
@ -477,6 +537,13 @@ class Resource(object):
self._uri.update(uri)
self._computed.update(computed)
# TODO(mordred) This is terrible, but is a hack at the moment to ensure
# json.dumps works. The json library does basically if not obj: and
# obj.items() ... but I think the if not obj: is short-circuiting down
# in the C code and thus since we don't store the data in self[] it's
# always False even if we override __len__ or __bool__.
dict.update(self, self.to_dict())
def _collect_attrs(self, attrs):
"""Given attributes, return a dict per type of attribute
@ -740,16 +807,22 @@ class Resource(object):
continue
if isinstance(value, Resource):
mapping[key] = value.to_dict()
elif (value and isinstance(value, list)
and isinstance(value[0], Resource)):
elif value and isinstance(value, list):
converted = []
for raw in value:
converted.append(raw.to_dict())
if isinstance(raw, Resource):
converted.append(raw.to_dict())
else:
converted.append(raw)
mapping[key] = converted
else:
mapping[key] = value
return mapping
# Compatibility with the munch.Munch.toDict method
toDict = to_dict
# Make the munch copy method use to_dict
copy = to_dict
def _to_munch(self):
"""Convert this resource into a Munch compatible with shade."""

View File

@ -32,6 +32,11 @@ class TestImage(base.BaseFunctionalTest):
name=TEST_IMAGE_NAME,
disk_format='raw',
container_format='bare',
# TODO(mordred): This is not doing what people think it is doing.
# This is EPICLY broken. However, rather than fixing it as it is,
# we need to just replace the image upload code with the stuff
# from shade. Figuring out mapping the crap-tastic arbitrary
# extra key-value pairs into Resource is going to be fun.
properties='{"description": "This is not an image"}',
data=open('CONTRIBUTING.rst', 'r')
)

View File

@ -10,6 +10,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import json
from openstack.object_store.v1 import container
from openstack.tests.unit import base
@ -165,6 +167,29 @@ class TestContainer(base.TestCase):
self.assertEqual(sot_dict['id'], self.container)
self.assertEqual(sot_dict['name'], self.container)
def test_to_json(self):
sot = container.Container.new(name=self.container)
self.assertEqual(
{
'bytes': None,
'bytes_used': None,
'content_type': None,
'count': None,
'id': self.container,
'if_none_match': None,
'is_content_type_detected': None,
'is_newest': None,
'location': None,
'name': self.container,
'object_count': None,
'read_ACL': None,
'sync_key': None,
'sync_to': None,
'timestamp': None,
'versions_location': None,
'write_ACL': None,
}, json.loads(json.dumps(sot)))
def _test_no_headers(self, sot, sot_call, sess_method):
headers = {}
data = {}

View File

@ -66,7 +66,7 @@ class TestComponent(base.TestCase):
# Test that we short-circuit everything when given no instance.
result = sot.__get__(None, None)
self.assertIsNone(result)
self.assertIs(sot, result)
# NOTE: Some tests will use a default=1 setting when testing result
# values that should be None because the default-for-default is also None.