BaseProxy refactoring for new Resource

To work with the changes in https://review.openstack.org/#/c/289998/
there are some follow-on changes necessary in the BaseProxy. This is
primarily the removal of `path_args` arguments from BaseProxy
signatures in favor of using keyword arguments, and passing those
keyword arguments down into Resource methods as well.

As with the Resource refactoring, as this is a major undertaking when it
comes to the entirety of the services affected, this is going to
temporarily live in a secondary "proxy2" module so that we can apply the
changes in waves. There is one temporary change in
openstack/connection.py that allows us to load proxy classes that are
subclassed from openstack.proxy2. The rest of it will eventually live on
as the only proxy class once we have completed all services being
transitioned onto the new one.

Change-Id: I918a31078f157354b5ccad313c51ace50241ede4
This commit is contained in:
Brian Curtin 2016-03-23 14:25:47 -04:00
parent 130698b34f
commit d20970df8e
3 changed files with 729 additions and 1 deletions

View File

@ -65,6 +65,7 @@ import os_client_config
from openstack import profile as _profile
from openstack import proxy
from openstack import proxy2
from openstack import session as _session
from openstack import utils
@ -226,7 +227,8 @@ class Connection(object):
try:
__import__(module)
proxy_class = getattr(sys.modules[module], "Proxy")
if not issubclass(proxy_class, proxy.BaseProxy):
if not (issubclass(proxy_class, proxy.BaseProxy) or
issubclass(proxy_class, proxy2.BaseProxy)):
raise TypeError("%s.Proxy must inherit from BaseProxy" %
proxy_class.__module__)
setattr(self, attr_name, proxy_class(self.session))

308
openstack/proxy2.py Normal file
View File

@ -0,0 +1,308 @@
# 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 openstack import exceptions
from openstack import resource2
# The _check_resource decorator is used on BaseProxy methods to ensure that
# the `actual` argument is in fact the type of the `expected` argument.
# It does so under two cases:
# 1. When strict=False, if and only if `actual` is a Resource instance,
# it is checked to see that it's an instance of the `expected` class.
# This allows `actual` to be other types, such as strings, when it makes
# sense to accept a raw id value.
# 2. When strict=True, `actual` must be an instance of the `expected` class.
def _check_resource(strict=False):
def wrap(method):
def check(self, expected, actual=None, *args, **kwargs):
if (strict and actual is not None and not
isinstance(actual, resource2.Resource)):
raise ValueError("A %s must be passed" % expected.__name__)
elif (isinstance(actual, resource2.Resource) and not
isinstance(actual, expected)):
raise ValueError("Expected %s but received %s" % (
expected.__name__, actual.__class__.__name__))
return method(self, expected, actual, *args, **kwargs)
return check
return wrap
class BaseProxy(object):
def __init__(self, session):
self.session = session
def _get_resource(self, resource_type, value, **attrs):
"""Get a resource object to work on
:param resource_type: The type of resource to operate on. This should
be a subclass of
:class:`~openstack.resource2.Resource` with a
``from_id`` method.
:param value: The ID of a resource or an object of ``resource_type``
class if using an existing instance, or None to create a
new instance.
:param path_args: A dict containing arguments for forming the request
URL, if needed.
"""
if value is None:
# Create a bare resource
res = resource_type.new(**attrs)
elif not isinstance(value, resource_type):
# Create from an ID
res = resource_type.new(id=value, **attrs)
else:
# An existing resource instance
res = value
res._update(**attrs)
return res
def _get_uri_attribute(self, child, parent, name):
"""Get a value to be associated with a URI attribute
`child` will not be None here as it's a required argument
on the proxy method. `parent` is allowed to be None if `child`
is an actual resource, but when an ID is given for the child
one must also be provided for the parent. An example of this
is that a parent is a Server and a child is a ServerInterface.
"""
if parent is None:
value = getattr(child, name)
else:
value = resource2.Resource._get_id(parent)
return value
def _find(self, resource_type, name_or_id, ignore_missing=True,
**attrs):
"""Find a resource
:param name_or_id: The name or ID of a resource to find.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, None will be returned when
attempting to find a nonexistent resource2.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.find`
method, such as query parameters.
:returns: An instance of ``resource_type`` or None
"""
return resource_type.find(self.session, name_or_id,
ignore_missing=ignore_missing,
**attrs)
@_check_resource(strict=False)
def _delete(self, resource_type, value, ignore_missing=True, **attrs):
"""Delete a resource
:param resource_type: The type of resource to delete. This should
be a :class:`~openstack.resource2.Resource`
subclass with a ``from_id`` method.
:param value: The value to delete. Can be either the ID of a
resource or a :class:`~openstack.resource2.Resource`
subclass.
:param bool ignore_missing: When set to ``False``
:class:`~openstack.exceptions.ResourceNotFound` will be
raised when the resource does not exist.
When set to ``True``, no exception will be set when
attempting to delete a nonexistent resource2.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.delete`
method, such as the ID of a parent resource.
:returns: The result of the ``delete``
:raises: ``ValueError`` if ``value`` is a
:class:`~openstack.resource2.Resource` that doesn't match
the ``resource_type``.
:class:`~openstack.exceptions.ResourceNotFound` when
ignore_missing if ``False`` and a nonexistent resource
is attempted to be deleted.
"""
res = self._get_resource(resource_type, value, **attrs)
try:
rv = res.delete(self.session)
except exceptions.NotFoundException as e:
if ignore_missing:
return None
else:
# Reraise with a more specific type and message
raise exceptions.ResourceNotFound(
message="No %s found for %s" %
(resource_type.__name__, value),
details=e.details, response=e.response,
request_id=e.request_id, url=e.url, method=e.method,
http_status=e.http_status, cause=e.cause)
return rv
@_check_resource(strict=False)
def _update(self, resource_type, value, **attrs):
"""Update a resource
:param resource_type: The type of resource to update.
:type resource_type: :class:`~openstack.resource2.Resource`
:param value: The resource to update. This must either be a
:class:`~openstack.resource2.Resource` or an id
that corresponds to a resource2.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.update`
method to be updated. These should correspond
to either :class:`~openstack.resource2.Body`
or :class:`~openstack.resource2.Header`
values on this resource.
:returns: The result of the ``update``
:rtype: :class:`~openstack.resource2.Resource`
"""
res = self._get_resource(resource_type, value, **attrs)
return res.update(self.session)
def _create(self, resource_type, **attrs):
"""Create a resource from attributes
:param resource_type: The type of resource to create.
:type resource_type: :class:`~openstack.resource2.Resource`
:param path_args: A dict containing arguments for forming the request
URL, if needed.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.create`
method to be created. These should correspond
to either :class:`~openstack.resource2.Body`
or :class:`~openstack.resource2.Header`
values on this resource.
:returns: The result of the ``create``
:rtype: :class:`~openstack.resource2.Resource`
"""
res = resource_type.new(**attrs)
return res.create(self.session)
@_check_resource(strict=False)
def _get(self, resource_type, value=None, **attrs):
"""Get a resource
:param resource_type: The type of resource to get.
:type resource_type: :class:`~openstack.resource2.Resource`
:param value: The value to get. Can be either the ID of a
resource or a :class:`~openstack.resource2.Resource`
subclass.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.get`
method. These should correspond
to either :class:`~openstack.resource2.Body`
or :class:`~openstack.resource2.Header`
values on this resource.
:returns: The result of the ``get``
:rtype: :class:`~openstack.resource2.Resource`
"""
res = self._get_resource(resource_type, value, **attrs)
try:
return res.get(self.session)
except exceptions.NotFoundException as e:
raise exceptions.ResourceNotFound(
message="No %s found for %s" %
(resource_type.__name__, value),
details=e.details, response=e.response,
request_id=e.request_id, url=e.url, method=e.method,
http_status=e.http_status, cause=e.cause)
def _list(self, resource_type, value=None, paginated=False, **attrs):
"""List a resource
:param resource_type: The type of resource to delete. This should
be a :class:`~openstack.resource2.Resource`
subclass with a ``from_id`` method.
:param value: The resource to list. It can be the ID of a resource, or
a :class:`~openstack.resource2.Resource` object. When set
to None, a new bare resource is created.
:param bool paginated: When set to ``False``, expect all of the data
to be returned in one response. When set to
``True``, the resource supports data being
returned across multiple pages.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.list` method. These should
correspond to either :class:`~openstack.resource2.URI` values
or appear in :data:`~openstack.resource2.Resource._query_mapping`.
:returns: A generator of Resource objects.
:raises: ``ValueError`` if ``value`` is a
:class:`~openstack.resource2.Resource` that doesn't match
the ``resource_type``.
"""
res = self._get_resource(resource_type, value, **attrs)
return res.list(self.session, paginated=paginated, **attrs)
def _head(self, resource_type, value=None, **attrs):
"""Retrieve a resource's header
:param resource_type: The type of resource to retrieve.
:type resource_type: :class:`~openstack.resource2.Resource`
:param value: The value of a specific resource to retreive headers
for. Can be either the ID of a resource,
a :class:`~openstack.resource2.Resource` subclass,
or ``None``.
:param dict attrs: Attributes to be passed onto the
:meth:`~openstack.resource2.Resource.head` method.
These should correspond to
:class:`~openstack.resource2.URI` values.
:returns: The result of the ``head`` call
:rtype: :class:`~openstack.resource2.Resource`
"""
res = self._get_resource(resource_type, value, **attrs)
return res.head(self.session)
def wait_for_status(self, value, status, failures=[], interval=2,
wait=120):
"""Wait for a resource to be in a particular status.
:param value: The resource to wait on to reach the status. The
resource must have a status attribute.
:type value: :class:`~openstack.resource2.Resource`
:param status: Desired status of the resource2.
:param list failures: Statuses that would indicate the transition
failed such as 'ERROR'.
:param interval: Number of seconds to wait between checks.
:param wait: Maximum number of seconds to wait for the change.
:return: Method returns resource on success.
:raises: :class:`~openstack.exceptions.ResourceTimeout` transition
to status failed to occur in wait seconds.
:raises: :class:`~openstack.exceptions.ResourceFailure` resource
transitioned to one of the failure states.
:raises: :class:`~AttributeError` if the resource does not have a
status attribute
"""
return resource2.wait_for_status(self.session, value, status,
failures, interval, wait)
def wait_for_delete(self, value, interval=2, wait=120):
"""Wait for the resource to be deleted.
:param value: The resource to wait on to be deleted.
:type value: :class:`~openstack.resource2.Resource`
:param interval: Number of seconds to wait between checks.
:param wait: Maximum number of seconds to wait for the delete.
:return: Method returns resource on success.
:raises: :class:`~openstack.exceptions.ResourceTimeout` transition
to delete failed to occur in wait seconds.
"""
return resource2.wait_for_delete(self.session, value, interval, wait)

View File

@ -0,0 +1,418 @@
# 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.
import mock
import testtools
from openstack import exceptions
from openstack import proxy2
from openstack import resource2
class DeleteableResource(resource2.Resource):
allow_delete = True
class UpdateableResource(resource2.Resource):
allow_update = True
class CreateableResource(resource2.Resource):
allow_create = True
class RetrieveableResource(resource2.Resource):
allow_retrieve = True
class ListableResource(resource2.Resource):
allow_list = True
class HeadableResource(resource2.Resource):
allow_head = True
class TestProxyPrivate(testtools.TestCase):
def setUp(self):
super(TestProxyPrivate, self).setUp()
def method(self, expected_type, value):
return value
self.sot = mock.Mock()
self.sot.method = method
self.fake_proxy = proxy2.BaseProxy("session")
def _test_correct(self, value):
decorated = proxy2._check_resource(strict=False)(self.sot.method)
rv = decorated(self.sot, resource2.Resource, value)
self.assertEqual(value, rv)
def test__check_resource_correct_resource(self):
res = resource2.Resource()
self._test_correct(res)
def test__check_resource_notstrict_id(self):
self._test_correct("abc123-id")
def test__check_resource_strict_id(self):
decorated = proxy2._check_resource(strict=True)(self.sot.method)
self.assertRaisesRegexp(ValueError, "A Resource must be passed",
decorated, self.sot, resource2.Resource,
"this-is-not-a-resource")
def test__check_resource_incorrect_resource(self):
class OneType(resource2.Resource):
pass
class AnotherType(resource2.Resource):
pass
value = AnotherType()
decorated = proxy2._check_resource(strict=False)(self.sot.method)
self.assertRaisesRegexp(ValueError,
"Expected OneType but received AnotherType",
decorated, self.sot, OneType, value)
def test__get_uri_attribute_no_parent(self):
class Child(resource2.Resource):
something = resource2.Body("something")
attr = "something"
value = "nothing"
child = Child(something=value)
result = self.fake_proxy._get_uri_attribute(child, None, attr)
self.assertEqual(value, result)
def test__get_uri_attribute_with_parent(self):
class Parent(resource2.Resource):
pass
value = "nothing"
parent = Parent(id=value)
result = self.fake_proxy._get_uri_attribute("child", parent, "attr")
self.assertEqual(value, result)
def test__get_resource_new(self):
value = "hello"
fake_type = mock.Mock(spec=resource2.Resource)
fake_type.new = mock.Mock(return_value=value)
attrs = {"first": "Brian", "last": "Curtin"}
result = self.fake_proxy._get_resource(fake_type, None, **attrs)
fake_type.new.assert_called_with(**attrs)
self.assertEqual(value, result)
def test__get_resource_from_id(self):
id = "eye dee"
value = "hello"
attrs = {"first": "Brian", "last": "Curtin"}
# The isinstance check needs to take a type, not an instance,
# so the mock.assert_called_with method isn't helpful here since
# we can't pass in a mocked object. This class is a crude version
# of that same behavior to let us check that `new` gets
# called with the expected arguments.
class Fake(object):
call = {}
@classmethod
def new(cls, **kwargs):
cls.call = kwargs
return value
result = self.fake_proxy._get_resource(Fake, id, **attrs)
self.assertDictEqual(dict(id=id, **attrs), Fake.call)
self.assertEqual(value, result)
def test__get_resource_from_resource(self):
res = mock.Mock(spec=resource2.Resource)
res._update = mock.Mock()
attrs = {"first": "Brian", "last": "Curtin"}
result = self.fake_proxy._get_resource(resource2.Resource,
res, **attrs)
res._update.assert_called_once_with(**attrs)
self.assertEqual(result, res)
class TestProxyDelete(testtools.TestCase):
def setUp(self):
super(TestProxyDelete, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.res = mock.Mock(spec=DeleteableResource)
self.res.id = self.fake_id
self.res.delete = mock.Mock()
self.sot = proxy2.BaseProxy(self.session)
DeleteableResource.new = mock.Mock(return_value=self.res)
def test_delete(self):
self.sot._delete(DeleteableResource, self.res)
self.res.delete.assert_called_with(self.session)
self.sot._delete(DeleteableResource, self.fake_id)
DeleteableResource.new.assert_called_with(id=self.fake_id)
self.res.delete.assert_called_with(self.session)
# Delete generally doesn't return anything, so we will normally
# swallow any return from within a service's proxy, but make sure
# we can still return for any cases where values are returned.
self.res.delete.return_value = self.fake_id
rv = self.sot._delete(DeleteableResource, self.fake_id)
self.assertEqual(rv, self.fake_id)
def test_delete_ignore_missing(self):
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
rv = self.sot._delete(DeleteableResource, self.fake_id)
self.assertIsNone(rv)
def test_delete_ResourceNotFound(self):
self.res.delete.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
self.assertRaisesRegexp(
exceptions.ResourceNotFound,
"No %s found for %s" % (DeleteableResource.__name__, self.res),
self.sot._delete, DeleteableResource, self.res,
ignore_missing=False)
def test_delete_HttpException(self):
self.res.delete.side_effect = exceptions.HttpException(
message="test", http_status=500)
self.assertRaises(exceptions.HttpException, self.sot._delete,
DeleteableResource, self.res, ignore_missing=False)
class TestProxyUpdate(testtools.TestCase):
def setUp(self):
super(TestProxyUpdate, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_result = "fake_result"
self.res = mock.Mock(spec=UpdateableResource)
self.res.update = mock.Mock(return_value=self.fake_result)
self.sot = proxy2.BaseProxy(self.session)
self.attrs = {"x": 1, "y": 2, "z": 3}
UpdateableResource.new = mock.Mock(return_value=self.res)
def test_update_resource(self):
rv = self.sot._update(UpdateableResource, self.res, **self.attrs)
self.assertEqual(rv, self.fake_result)
self.res._update.assert_called_once_with(**self.attrs)
self.res.update.assert_called_once_with(self.session)
def test_update_id(self):
rv = self.sot._update(UpdateableResource, self.fake_id, **self.attrs)
self.assertEqual(rv, self.fake_result)
self.res.update.assert_called_once_with(self.session)
class TestProxyCreate(testtools.TestCase):
def setUp(self):
super(TestProxyCreate, self).setUp()
self.session = mock.Mock()
self.fake_result = "fake_result"
self.res = mock.Mock(spec=CreateableResource)
self.res.create = mock.Mock(return_value=self.fake_result)
self.sot = proxy2.BaseProxy(self.session)
def test_create_attributes(self):
CreateableResource.new = mock.Mock(return_value=self.res)
attrs = {"x": 1, "y": 2, "z": 3}
rv = self.sot._create(CreateableResource, **attrs)
self.assertEqual(rv, self.fake_result)
CreateableResource.new.assert_called_once_with(**attrs)
self.res.create.assert_called_once_with(self.session)
class TestProxyGet(testtools.TestCase):
def setUp(self):
super(TestProxyGet, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_name = "fake_name"
self.fake_result = "fake_result"
self.res = mock.Mock(spec=RetrieveableResource)
self.res.id = self.fake_id
self.res.get = mock.Mock(return_value=self.fake_result)
self.sot = proxy2.BaseProxy(self.session)
RetrieveableResource.new = mock.Mock(return_value=self.res)
def test_get_resource(self):
rv = self.sot._get(RetrieveableResource, self.res)
self.res.get.assert_called_with(self.session)
self.assertEqual(rv, self.fake_result)
def test_get_resource_with_args(self):
args = {"key": "value"}
rv = self.sot._get(RetrieveableResource, self.res, **args)
self.res._update.assert_called_once_with(**args)
self.res.get.assert_called_with(self.session)
self.assertEqual(rv, self.fake_result)
def test_get_id(self):
rv = self.sot._get(RetrieveableResource, self.fake_id)
RetrieveableResource.new.assert_called_with(id=self.fake_id)
self.res.get.assert_called_with(self.session)
self.assertEqual(rv, self.fake_result)
def test_get_not_found(self):
self.res.get.side_effect = exceptions.NotFoundException(
message="test", http_status=404)
self.assertRaisesRegexp(
exceptions.ResourceNotFound,
"No %s found for %s" % (RetrieveableResource.__name__, self.res),
self.sot._get, RetrieveableResource, self.res)
class TestProxyList(testtools.TestCase):
def setUp(self):
super(TestProxyList, self).setUp()
self.session = mock.Mock()
self.args = {"a": "A", "b": "B", "c": "C"}
self.fake_response = [resource2.Resource()]
self.sot = proxy2.BaseProxy(self.session)
ListableResource.list = mock.Mock()
ListableResource.list.return_value = self.fake_response
def _test_list(self, paginated):
rv = self.sot._list(ListableResource, paginated=paginated, **self.args)
self.assertEqual(self.fake_response, rv)
ListableResource.list.assert_called_once_with(
self.session, paginated=paginated, **self.args)
def test_list_paginated(self):
self._test_list(True)
def test_list_non_paginated(self):
self._test_list(False)
class TestProxyHead(testtools.TestCase):
def setUp(self):
super(TestProxyHead, self).setUp()
self.session = mock.Mock()
self.fake_id = 1
self.fake_name = "fake_name"
self.fake_result = "fake_result"
self.res = mock.Mock(spec=HeadableResource)
self.res.id = self.fake_id
self.res.head = mock.Mock(return_value=self.fake_result)
self.sot = proxy2.BaseProxy(self.session)
HeadableResource.new = mock.Mock(return_value=self.res)
def test_head_resource(self):
rv = self.sot._head(HeadableResource, self.res)
self.res.head.assert_called_with(self.session)
self.assertEqual(rv, self.fake_result)
def test_head_id(self):
rv = self.sot._head(HeadableResource, self.fake_id)
HeadableResource.new.assert_called_with(id=self.fake_id)
self.res.head.assert_called_with(self.session)
self.assertEqual(rv, self.fake_result)
class TestProxyWaits(testtools.TestCase):
def setUp(self):
super(TestProxyWaits, self).setUp()
self.session = mock.Mock()
self.sot = proxy2.BaseProxy(self.session)
@mock.patch("openstack.resource2.wait_for_status")
def test_wait_for(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_status(mock_resource, 'ACTIVE')
mock_wait.assert_called_once_with(
self.session, mock_resource, 'ACTIVE', [], 2, 120)
@mock.patch("openstack.resource2.wait_for_status")
def test_wait_for_params(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_status(mock_resource, 'ACTIVE', ['ERROR'], 1, 2)
mock_wait.assert_called_once_with(
self.session, mock_resource, 'ACTIVE', ['ERROR'], 1, 2)
@mock.patch("openstack.resource2.wait_for_delete")
def test_wait_for_delete(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_delete(mock_resource)
mock_wait.assert_called_once_with(
self.session, mock_resource, 2, 120)
@mock.patch("openstack.resource2.wait_for_delete")
def test_wait_for_delete_params(self, mock_wait):
mock_resource = mock.Mock()
mock_wait.return_value = mock_resource
self.sot.wait_for_delete(mock_resource, 1, 2)
mock_wait.assert_called_once_with(
self.session, mock_resource, 1, 2)