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