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:
parent
d4db52f2fa
commit
2f97394847
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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')
|
||||
)
|
||||
|
|
|
@ -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 = {}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue