From b60915aab3ee0348f3e3cc8aa548f94d2a68b7eb Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Tue, 10 Nov 2020 13:44:08 +0100 Subject: [PATCH] Complete compute aggregate functions In order to switch next part of OSC towards SDK we need to add some missing bits to the aggregates. Since this is another example when API returns 400 if we try accessing resource with name and it doesn't support - modify general behavior for also skipping 400 in the find method Change-Id: Ia6711b1c27514d0698fec1efedaefeeb93722b9d --- openstack/compute/v2/_proxy.py | 34 +++++++++++ openstack/compute/v2/aggregate.py | 31 +++++++--- openstack/resource.py | 4 +- .../tests/unit/compute/v2/test_aggregate.py | 24 +++++--- openstack/tests/unit/compute/v2/test_proxy.py | 58 +++++++++++++++++++ ...-aggregate-functions-45d5f2beeeac2b48.yaml | 6 ++ 6 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index b3bbe51b3..32669ee13 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -261,6 +261,20 @@ class Proxy(proxy.Proxy): """ return self._get(_aggregate.Aggregate, aggregate) + def find_aggregate(self, name_or_id, ignore_missing=True): + """Find a single aggregate + + :param name_or_id: The name or ID of an aggregate. + :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 resource. + :returns: One :class:`~openstack.compute.v2.aggregate.Aggregate` + or None + """ + return self._find(_aggregate.Aggregate, name_or_id, + ignore_missing=ignore_missing) + def create_aggregate(self, **attrs): """Create a new host aggregate from attributes @@ -345,6 +359,26 @@ class Proxy(proxy.Proxy): aggregate = self._get_resource(_aggregate.Aggregate, aggregate) return aggregate.set_metadata(self, metadata) + def aggregate_precache_images(self, aggregate, images): + """Requests image precaching on an aggregate + + :param aggregate: Either the ID of a aggregate or a + :class:`~openstack.compute.v2.aggregate.Aggregate` instance. + :param images: Single image id or list of image ids. + + :returns: ``None`` + """ + aggregate = self._get_resource(_aggregate.Aggregate, aggregate) + # We need to ensure we pass list of image IDs + if isinstance(images, str): + images = [images] + image_data = [] + for img in images: + image_data.append({'id': img}) + return aggregate.precache_images(self, image_data) + + # ========== Images ========== + def delete_image(self, image, ignore_missing=True): """Delete an image diff --git a/openstack/compute/v2/aggregate.py b/openstack/compute/v2/aggregate.py index f0d8d8501..b256be3ec 100644 --- a/openstack/compute/v2/aggregate.py +++ b/openstack/compute/v2/aggregate.py @@ -10,7 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. - +from openstack import exceptions from openstack import resource from openstack import utils @@ -30,25 +30,31 @@ class Aggregate(resource.Resource): # Properties #: Availability zone of aggregate availability_zone = resource.Body('availability_zone') + #: The date and time when the resource was created. + created_at = resource.Body('created_at') + #: The date and time when the resource was deleted. + deleted_at = resource.Body('deleted_at') #: Deleted? - deleted = resource.Body('deleted') + is_deleted = resource.Body('deleted', type=bool) #: Name of aggregate name = resource.Body('name') #: Hosts - hosts = resource.Body('hosts') + hosts = resource.Body('hosts', type=list) #: Metadata - metadata = resource.Body('metadata') + metadata = resource.Body('metadata', type=dict) + #: The date and time when the resource was updated + updated_at = resource.Body('updated_at') #: UUID uuid = resource.Body('uuid') - # uuid introduced in 2.41 - _max_microversion = '2.41' + # Image pre-caching introduced in 2.81 + _max_microversion = '2.81' def _action(self, session, body, microversion=None): """Preform aggregate actions given the message body.""" url = utils.urljoin(self.base_path, self.id, 'action') - headers = {'Accept': ''} response = session.post( - url, json=body, headers=headers, microversion=microversion) + url, json=body, microversion=microversion) + exceptions.raise_from_response(response) aggregate = Aggregate() aggregate._translate_response(response=response) return aggregate @@ -67,3 +73,12 @@ class Aggregate(resource.Resource): """Creates or replaces metadata for an aggregate.""" body = {'set_metadata': {'metadata': metadata}} return self._action(session, body) + + def precache_images(self, session, images): + """Requests image pre-caching""" + body = {'cache': images} + url = utils.urljoin(self.base_path, self.id, 'images') + response = session.post( + url, json=body, microversion=self._max_microversion) + exceptions.raise_from_response(response) + # This API has no result diff --git a/openstack/resource.py b/openstack/resource.py index 618183c67..5010a92fb 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -1895,7 +1895,9 @@ class Resource(dict): connection=session._get_connection(), **params) return match.fetch(session, **params) - except exceptions.NotFoundException: + except (exceptions.NotFoundException, exceptions.BadRequestException): + # NOTE(gtema): There are few places around openstack that return + # 400 if we try to GET resource and it doesn't exist. pass if ('name' in cls._query_mapping._mapping.keys() diff --git a/openstack/tests/unit/compute/v2/test_aggregate.py b/openstack/tests/unit/compute/v2/test_aggregate.py index b43dee5d9..220587dbb 100644 --- a/openstack/tests/unit/compute/v2/test_aggregate.py +++ b/openstack/tests/unit/compute/v2/test_aggregate.py @@ -60,7 +60,10 @@ class TestAggregate(base.TestCase): sot = aggregate.Aggregate(**EXAMPLE) self.assertEqual(EXAMPLE['name'], sot.name) self.assertEqual(EXAMPLE['availability_zone'], sot.availability_zone) - self.assertEqual(EXAMPLE['deleted'], sot.deleted) + self.assertEqual(EXAMPLE['deleted'], sot.is_deleted) + self.assertEqual(EXAMPLE['deleted_at'], sot.deleted_at) + self.assertEqual(EXAMPLE['created_at'], sot.created_at) + self.assertEqual(EXAMPLE['updated_at'], sot.updated_at) self.assertEqual(EXAMPLE['hosts'], sot.hosts) self.assertEqual(EXAMPLE['id'], sot.id) self.assertEqual(EXAMPLE['uuid'], sot.uuid) @@ -73,9 +76,8 @@ class TestAggregate(base.TestCase): url = 'os-aggregates/4/action' body = {"add_host": {"host": "host1"}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) def test_remove_host(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -84,9 +86,8 @@ class TestAggregate(base.TestCase): url = 'os-aggregates/4/action' body = {"remove_host": {"host": "host1"}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) def test_set_metadata(self): sot = aggregate.Aggregate(**EXAMPLE) @@ -95,6 +96,15 @@ class TestAggregate(base.TestCase): url = 'os-aggregates/4/action' body = {"set_metadata": {"metadata": {"key: value"}}} - headers = {'Accept': ''} self.sess.post.assert_called_with( - url, json=body, headers=headers, microversion=None) + url, json=body, microversion=None) + + def test_precache_image(self): + sot = aggregate.Aggregate(**EXAMPLE) + + sot.precache_images(self.sess, ['1']) + + url = 'os-aggregates/4/images' + body = {"cache": ['1']} + self.sess.post.assert_called_with( + url, json=body, microversion=sot._max_microversion) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 77d223d1c..4a36606ee 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -12,6 +12,7 @@ from unittest import mock from openstack.compute.v2 import _proxy +from openstack.compute.v2 import aggregate from openstack.compute.v2 import availability_zone as az from openstack.compute.v2 import extension from openstack.compute.v2 import flavor @@ -245,6 +246,63 @@ class TestKeyPair(TestComputeProxy): ) +class TestAggregate(TestComputeProxy): + def test_aggregate_create(self): + self.verify_create(self.proxy.create_aggregate, aggregate.Aggregate) + + def test_aggregate_delete(self): + self.verify_delete( + self.proxy.delete_aggregate, aggregate.Aggregate, False) + + def test_aggregate_delete_ignore(self): + self.verify_delete( + self.proxy.delete_aggregate, aggregate.Aggregate, True) + + def test_aggregate_find(self): + self.verify_find(self.proxy.find_aggregate, aggregate.Aggregate) + + def test_aggregates(self): + self.verify_list_no_kwargs(self.proxy.aggregates, aggregate.Aggregate) + + def test_aggregate_get(self): + self.verify_get(self.proxy.get_aggregate, aggregate.Aggregate) + + def test_aggregate_update(self): + self.verify_update(self.proxy.update_aggregate, aggregate.Aggregate) + + def test_aggregate_add_host(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.add_host", + self.proxy.add_host_to_aggregate, + method_args=["value", "host"], + expected_args=["host"]) + + def test_aggregate_remove_host(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.remove_host", + self.proxy.remove_host_from_aggregate, + method_args=["value", "host"], + expected_args=["host"]) + + def test_aggregate_set_metadata(self): + self._verify("openstack.compute.v2.aggregate.Aggregate.set_metadata", + self.proxy.set_aggregate_metadata, + method_args=["value", {'a': 'b'}], + expected_args=[{'a': 'b'}]) + + def test_aggregate_precache_image(self): + self._verify( + "openstack.compute.v2.aggregate.Aggregate.precache_images", + self.proxy.aggregate_precache_images, + method_args=["value", '1'], + expected_args=[[{'id': '1'}]]) + + def test_aggregate_precache_images(self): + self._verify( + "openstack.compute.v2.aggregate.Aggregate.precache_images", + self.proxy.aggregate_precache_images, + method_args=["value", ['1', '2']], + expected_args=[[{'id': '1'}, {'id': '2'}]]) + + class TestCompute(TestComputeProxy): def test_extension_find(self): self.verify_find(self.proxy.find_extension, extension.Extension) diff --git a/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml b/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml new file mode 100644 index 000000000..3fafe6188 --- /dev/null +++ b/releasenotes/notes/complete-aggregate-functions-45d5f2beeeac2b48.yaml @@ -0,0 +1,6 @@ +--- +features: + - Complete compute.aggregate functions to the latest state +fixes: + - aggregate.deleted property is renamed to 'is_deleted' to comply with the + naming convention