diff --git a/neutron_lib/clients/placement.py b/neutron_lib/clients/placement.py index 1a15809db..76fe35b0a 100644 --- a/neutron_lib/clients/placement.py +++ b/neutron_lib/clients/placement.py @@ -78,6 +78,8 @@ class PlacementAPIClient(object): self._conf = conf self._ks_filter = {'service_type': 'placement', 'region_name': self._conf.placement.region_name} + self._api_version_header = {API_VERSION_REQUEST_HEADER: + self._openstack_api_version} self._client = None def _create_client(self): @@ -92,19 +94,32 @@ class PlacementAPIClient(object): self._conf, 'placement', auth=auth_plugin, additional_headers={'accept': 'application/json'}) + def _extend_header_with_api_version(self, **kwargs): + headers = kwargs.get('headers', {}) + if API_VERSION_REQUEST_HEADER not in headers: + if 'headers' not in kwargs: + kwargs['headers'] = self._api_version_header + else: + kwargs['headers'].update(self._api_version_header) + return kwargs + def _get(self, url, **kwargs): + kwargs = self._extend_header_with_api_version(**kwargs) return self._client.get(url, endpoint_filter=self._ks_filter, **kwargs) def _post(self, url, data, **kwargs): + kwargs = self._extend_header_with_api_version(**kwargs) return self._client.post(url, json=data, endpoint_filter=self._ks_filter, **kwargs) def _put(self, url, data, **kwargs): + kwargs = self._extend_header_with_api_version(**kwargs) return self._client.put(url, json=data, endpoint_filter=self._ks_filter, **kwargs) def _delete(self, url, **kwargs): + kwargs = self._extend_header_with_api_version(**kwargs) return self._client.delete(url, endpoint_filter=self._ks_filter, **kwargs) @@ -116,9 +131,7 @@ class PlacementAPIClient(object): (required) and the uuid (required). """ url = '/resource_providers' - self._post(url, resource_provider, - headers={API_VERSION_REQUEST_HEADER: - self._openstack_api_version}) + self._post(url, resource_provider) @_check_placement_api_available def delete_resource_provider(self, resource_provider_uuid): @@ -141,8 +154,7 @@ class PlacementAPIClient(object): """ url = '/resource_providers/%s' % resource_provider_uuid try: - return self._get(url, headers={API_VERSION_REQUEST_HEADER: - self._openstack_api_version}).json() + return self._get(url).json() except ks_exc.NotFound: raise n_exc.PlacementResourceProviderNotFound( resource_provider=resource_provider_uuid) @@ -191,9 +203,7 @@ class PlacementAPIClient(object): filters['in_tree'] = in_tree if uuid: filters['uuid'] = uuid - return self._get(url, headers={API_VERSION_REQUEST_HEADER: - self._openstack_api_version}, - **filters).json() + return self._get(url, **filters).json() @_check_placement_api_available def update_resource_provider_inventories( @@ -237,6 +247,48 @@ class PlacementAPIClient(object): resource_provider=resource_provider_uuid, generation=resource_provider_generation) + @_check_placement_api_available + def delete_resource_provider_inventories(self, resource_provider_uuid): + """Delete all inventory records for the resource provider. + + :param resource_provider_uuid: UUID of the resource provider. + :raises PlacementResourceProviderNotFound: If the resource provider + is not found. + """ + url = '/resource_providers/%s/inventories' % ( + resource_provider_uuid) + try: + self._delete(url) + except ks_exc.NotFound as e: + if "No resource provider with uuid" in e.details: + raise n_exc.PlacementResourceProviderNotFound( + resource_provider=resource_provider_uuid) + else: + raise + + @_check_placement_api_available + def delete_resource_provider_inventory(self, resource_provider_uuid, + resource_class): + """Delete inventory of the resource class for a resource provider. + + :param resource_provider_uuid: UUID of the resource provider. + :param resource_class: The name of the resource class + """ + url = '/resource_providers/%s/inventories/%s' % ( + resource_provider_uuid, resource_class) + try: + self._delete(url) + except ks_exc.NotFound as e: + if "No resource provider with uuid" in e.details: + raise n_exc.PlacementResourceProviderNotFound( + resource_provider=resource_provider_uuid) + elif "No inventory of class" in e.details: + raise n_exc.PlacementInventoryNotFound( + resource_provider=resource_provider_uuid, + resource_class=resource_class) + else: + raise + @_check_placement_api_available def get_inventory(self, resource_provider_uuid, resource_class): """Get resource provider inventory. @@ -301,9 +353,7 @@ class PlacementAPIClient(object): provider. """ url = '/resource_providers/%s/aggregates' % resource_provider_uuid - self._put(url, aggregates, - headers={API_VERSION_REQUEST_HEADER: - self._openstack_api_version}) + self._put(url, aggregates) @_check_placement_api_available def list_aggregates(self, resource_provider_uuid): @@ -315,9 +365,155 @@ class PlacementAPIClient(object): """ url = '/resource_providers/%s/aggregates' % resource_provider_uuid try: - return self._get( - url, headers={API_VERSION_REQUEST_HEADER: - self._openstack_api_version}).json() + return self._get(url).json() except ks_exc.NotFound: raise n_exc.PlacementAggregateNotFound( resource_provider=resource_provider_uuid) + + @_check_placement_api_available + def list_traits(self): + """List all traits.""" + url = '/traits' + return self._get(url).json() + + @_check_placement_api_available + def get_trait(self, name): + """Check if a given trait exists + + :param name: name of the trait to check. + :raises PlacementTraitNotFound: If the trait name not found. + """ + url = '/traits/%s' % name + try: + return self._get(url) + except ks_exc.NotFound: + raise n_exc.PlacementTraitNotFound(trait=name) + + @_check_placement_api_available + def update_trait(self, name): + """Insert a single custom trait. + + :param name: name of the trait to create. + """ + url = '/traits/%s' % (name) + return self._put(url, None) + + @_check_placement_api_available + def delete_trait(self, name): + """Delete the specified trait. + + :param name: the name of the trait to be deleted. + """ + url = '/traits/%s' % (name) + try: + self._delete(url) + except ks_exc.NotFound: + raise n_exc.PlacementTraitNotFound(trait=name) + + @_check_placement_api_available + def update_resource_provider_traits( + self, resource_provider_uuid, traits, + resource_provider_generation): + """Update resource provider traits + + :param resource_provider_uuid: UUID of the resource provider for which + to set the traits + :param traits: a list of traits. + :param resource_provider_generation: The generation of the resource + provider. + :raises PlacementResourceProviderNotFound: If the resource provider + is not found. + :raises PlacementTraitNotFound: If any of the specified traits are not + valid. + """ + url = '/resource_providers/%s/traits' % (resource_provider_uuid) + body = { + 'resource_provider_generation': resource_provider_generation, + 'traits': traits + } + try: + return self._put(url, body).json() + except ks_exc.NotFound: + raise n_exc.PlacementResourceProviderNotFound( + resource_provider=resource_provider_uuid) + except ks_exc.BadRequest: + raise n_exc.PlacementTraitNotFound(trait=traits) + + @_check_placement_api_available + def list_resource_provider_traits(self, resource_provider_uuid): + """List all traits associated with a resource provider + + :param resource_provider_uuid: UUID of the resource provider for which + the traits will be listed + :raises PlacementResourceProviderNotFound: If the resource provider + is not found. + """ + url = '/resource_providers/%s/traits' % (resource_provider_uuid) + try: + return self._get(url).json() + except ks_exc.NotFound: + raise n_exc.PlacementResourceProviderNotFound( + resource_provider=resource_provider_uuid) + + @_check_placement_api_available + def delete_resource_provider_traits(self, resource_provider_uuid): + """Delete resource provider traits. + + :param resource_provider_uuid: The UUID of the resource provider for + which to delete all the traits. + """ + url = '/resource_providers/%s/traits' % (resource_provider_uuid) + try: + self._delete(url) + except ks_exc.NotFound: + raise n_exc.PlacementResourceProviderNotFound( + resource_provider=resource_provider_uuid) + + @_check_placement_api_available + def list_resource_classes(self): + """List resource classes""" + url = '/resource_classes' + return self._get(url).json() + + @_check_placement_api_available + def get_resource_class(self, name): + """Show resource class. + + :param name: The name of the resource class to show + """ + url = '/resource_classes/%s' % (name) + try: + return self._get(url).json() + except ks_exc.NotFound: + raise n_exc.PlacementResourceClassNotFound(resource_class=name) + + @_check_placement_api_available + def create_resource_class(self, name): + """Create a custom resource class + + :param name: the name of the resource class + """ + url = '/resource_classes' + body = {'name': name} + self._post(url, body) + + @_check_placement_api_available + def update_resource_class(self, name): + """Create or validate the existence of the resource custom class. + + :param name: the name of the resource class to be updated or validated + """ + url = '/resource_classes/%s' % name + self._put(url) + + @_check_placement_api_available + def delete_resource_class(self, name): + """Delete a custom resource class. + + :param name: The name of the resource class to be deleted. + """ + url = '/resource_classes/%s' % (name) + try: + self._delete(url) + except ks_exc.NotFound: + raise n_exc.PlacementResourceClassNotFound(resource_class=name) diff --git a/neutron_lib/exceptions/placement.py b/neutron_lib/exceptions/placement.py index f1989c963..a609527db 100644 --- a/neutron_lib/exceptions/placement.py +++ b/neutron_lib/exceptions/placement.py @@ -49,6 +49,14 @@ class PlacementAggregateNotFound(exceptions.NotFound): "%(resource_provider)s.") +class PlacementTraitNotFound(exceptions.NotFound): + message = _("Placement trait not found %(trait)s.") + + +class PlacementResourceClassNotFound(exceptions.NotFound): + message = _("Placement resource class not found %(resource_class)s") + + class PlacementAPIVersionIncorrect(exceptions.NotFound): message = _("Placement API version %(current_version)s, do not meet the" "needed version %(needed_version)s.") diff --git a/neutron_lib/tests/unit/clients/test_placement.py b/neutron_lib/tests/unit/clients/test_placement.py index 55dac9f69..afe259316 100644 --- a/neutron_lib/tests/unit/clients/test_placement.py +++ b/neutron_lib/tests/unit/clients/test_placement.py @@ -26,6 +26,7 @@ from neutron_lib.tests import _base as base RESOURCE_PROVIDER_UUID = uuidutils.generate_uuid() RESOURCE_PROVIDER_GENERATION = 1 RESOURCE_CLASS_NAME = 'resource_class_name' +TRAIT_NAME = 'trait_name' INVENTORY = { RESOURCE_CLASS_NAME: { 'total': 42 @@ -45,15 +46,14 @@ class TestPlacementAPIClient(base.BaseTestCase): config, self.openstack_api_version) self.placement_fixture = self.useFixture( fixture.PlacementAPIClientFixture(self.placement_api_client)) - self.headers = {'OpenStack-API-Version': self.openstack_api_version} def test_create_resource_provider(self): self.placement_api_client.create_resource_provider( RESOURCE_PROVIDER_UUID) self.placement_fixture.mock_post.assert_called_once_with( '/resource_providers', - RESOURCE_PROVIDER_UUID, - headers=self.headers) + RESOURCE_PROVIDER_UUID + ) def test_delete_resource_provider(self): self.placement_api_client.delete_resource_provider( @@ -64,8 +64,7 @@ class TestPlacementAPIClient(base.BaseTestCase): def test_get_resource_provider(self): self.placement_api_client.get_resource_provider(RESOURCE_PROVIDER_UUID) self.placement_fixture.mock_get.assert_called_once_with( - '/resource_providers/%s' % RESOURCE_PROVIDER_UUID, - headers=self.headers) + '/resource_providers/%s' % RESOURCE_PROVIDER_UUID) def test_get_resource_provider_no_resource_provider(self): self.placement_fixture.mock_get.side_effect = ks_exc.NotFound() @@ -77,20 +76,20 @@ class TestPlacementAPIClient(base.BaseTestCase): filter_1 = {'name': 'name1', 'in_tree': 'tree1_uuid'} self.placement_api_client.list_resource_providers(**filter_1) self.placement_fixture.mock_get.assert_called_once_with( - '/resource_providers', headers=self.headers, **filter_1) + '/resource_providers', **filter_1) filter_2 = {'member_of': ['aggregate_uuid'], 'uuid': 'uuid_1', 'resources': {'r_class1': 'value1'}} self.placement_fixture.mock_get.reset_mock() self.placement_api_client.list_resource_providers(**filter_2) self.placement_fixture.mock_get.assert_called_once_with( - '/resource_providers', headers=self.headers, **filter_2) + '/resource_providers', **filter_2) filter_1.update(filter_2) self.placement_fixture.mock_get.reset_mock() self.placement_api_client.list_resource_providers(**filter_1) self.placement_fixture.mock_get.assert_called_once_with( - '/resource_providers', headers=self.headers, **filter_1) + '/resource_providers', **filter_1) def test_list_resource_providers_placement_api_version_too_low(self): self.placement_api_client._target_version = (1, 1) @@ -122,6 +121,27 @@ class TestPlacementAPIClient(base.BaseTestCase): self.placement_api_client.update_resource_provider_inventories, RESOURCE_PROVIDER_UUID, INVENTORY, RESOURCE_PROVIDER_GENERATION) + def test_delete_resource_provider_inventory(self): + self.placement_api_client.delete_resource_provider_inventory( + RESOURCE_PROVIDER_UUID, RESOURCE_CLASS_NAME + ) + self.placement_fixture.mock_delete.assert_called_once_with( + '/resource_providers/%(rp_uuid)s/inventories/%(rc_name)s' % + {'rp_uuid': RESOURCE_PROVIDER_UUID, + 'rc_name': RESOURCE_CLASS_NAME} + ) + + def test_delete_resource_provider_inventory_no_rp(self): + self.placement_fixture.mock_delete.side_effect = ks_exc.NotFound( + details='No resource provider with uuid' + ) + self.assertRaises( + n_exc.PlacementResourceProviderNotFound, + self.placement_api_client.delete_resource_provider_inventory, + RESOURCE_PROVIDER_UUID, + RESOURCE_CLASS_NAME + ) + def test_get_inventory(self): self.placement_api_client.get_inventory(RESOURCE_PROVIDER_UUID, RESOURCE_CLASS_NAME) @@ -191,16 +211,146 @@ class TestPlacementAPIClient(base.BaseTestCase): mock.ANY) self.placement_fixture.mock_put.assert_called_once_with( '/resource_providers/%s/aggregates' % RESOURCE_PROVIDER_UUID, - mock.ANY, headers=self.headers) + mock.ANY) def test_list_aggregates(self): self.placement_api_client.list_aggregates(RESOURCE_PROVIDER_UUID) self.placement_fixture.mock_get.assert_called_once_with( - '/resource_providers/%s/aggregates' % RESOURCE_PROVIDER_UUID, - headers=self.headers) + '/resource_providers/%s/aggregates' % RESOURCE_PROVIDER_UUID) def test_list_aggregates_no_resource_provider(self): self.placement_fixture.mock_get.side_effect = ks_exc.NotFound() self.assertRaises(n_exc.PlacementAggregateNotFound, self.placement_api_client.list_aggregates, RESOURCE_PROVIDER_UUID) + + def test_list_traits(self): + self.placement_api_client.list_traits() + self.placement_fixture.mock_get.assert_called_once_with( + '/traits') + + def test_get_trait(self): + self.placement_api_client.get_trait(TRAIT_NAME) + self.placement_fixture.mock_get.assert_called_once_with( + '/traits/%s' % TRAIT_NAME) + + def test_get_trait_no_trait(self): + self.placement_fixture.mock_get.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementTraitNotFound, + self.placement_api_client.get_trait, + TRAIT_NAME) + + def test_create_trait(self): + self.placement_api_client.update_trait(TRAIT_NAME) + self.placement_fixture.mock_put.assert_called_once_with( + '/traits/%s' % TRAIT_NAME, None) + + def test_update_resource_provider_traits(self): + traits = [TRAIT_NAME] + self.placement_api_client.update_resource_provider_traits( + RESOURCE_PROVIDER_UUID, traits, resource_provider_generation=0) + self.placement_fixture.mock_put.assert_called_once_with( + '/resource_providers/%(rp_uuid)s/traits' % + {'rp_uuid': RESOURCE_PROVIDER_UUID}, + {'resource_provider_generation': 0, 'traits': traits}) + + def test_update_resource_provider_traits_no_rp(self): + traits = [TRAIT_NAME] + self.placement_fixture.mock_put.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementResourceProviderNotFound, + self.placement_api_client.update_resource_provider_traits, + RESOURCE_PROVIDER_UUID, traits, resource_provider_generation=0) + + def test_update_resource_provider_traits_trait_not_found(self): + traits = [TRAIT_NAME] + self.placement_fixture.mock_put.side_effect = ks_exc.BadRequest() + self.assertRaises( + n_exc.PlacementTraitNotFound, + self.placement_api_client.update_resource_provider_traits, + RESOURCE_PROVIDER_UUID, traits, resource_provider_generation=0) + + def test_list_resource_provider_traits(self): + self.placement_api_client.list_resource_provider_traits( + RESOURCE_PROVIDER_UUID) + self.placement_fixture.mock_get.assert_called_once_with( + '/resource_providers/%s/traits' % RESOURCE_PROVIDER_UUID) + + def test_list_resource_provider_traits_no_rp(self): + self.placement_fixture.mock_get.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementResourceProviderNotFound, + self.placement_api_client.list_resource_provider_traits, + RESOURCE_PROVIDER_UUID) + + def test_delete_trait(self): + self.placement_api_client.delete_trait(TRAIT_NAME) + self.placement_fixture.mock_delete.assert_called_once_with( + '/traits/%s' % TRAIT_NAME) + + def test_delete_trait_no_trait(self): + self.placement_fixture.mock_delete.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementTraitNotFound, + self.placement_api_client.delete_trait, + TRAIT_NAME) + + def test_delete_resource_provider_traits(self): + self.placement_api_client.delete_resource_provider_traits( + RESOURCE_PROVIDER_UUID) + self.placement_fixture.mock_delete.assert_called_once_with( + '/resource_providers/%s/traits' % RESOURCE_PROVIDER_UUID) + + def test_delete_resource_provider_traits_no_rp(self): + self.placement_fixture.mock_delete.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementResourceProviderNotFound, + self.placement_api_client.delete_resource_provider_traits, + RESOURCE_PROVIDER_UUID) + + def test_list_resource_classes(self): + self.placement_api_client.list_resource_classes() + self.placement_fixture.mock_get.assert_called_once_with( + '/resource_classes' + ) + + def test_get_resource_class(self): + self.placement_api_client.get_resource_class(RESOURCE_CLASS_NAME) + self.placement_fixture.mock_get.assert_called_once_with( + '/resource_classes/%s' % RESOURCE_CLASS_NAME + ) + + def test_get_resource_class_no_resource_class(self): + self.placement_fixture.mock_get.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementResourceClassNotFound, + self.placement_api_client.get_resource_class, + RESOURCE_CLASS_NAME + ) + + def test_create_resource_class(self): + self.placement_api_client.create_resource_class(RESOURCE_CLASS_NAME) + self.placement_fixture.mock_post.assert_called_once_with( + '/resource_classes', + {'name': RESOURCE_CLASS_NAME}, + ) + + def test_update_resource_class(self): + self.placement_api_client.update_resource_class(RESOURCE_CLASS_NAME) + self.placement_fixture.mock_put.assert_called_once_with( + '/resource_classes/%s' % RESOURCE_CLASS_NAME) + + def test_delete_resource_class(self): + self.placement_api_client.delete_resource_class(RESOURCE_CLASS_NAME) + self.placement_fixture.mock_delete.assert_called_once_with( + '/resource_classes/%s' % RESOURCE_CLASS_NAME + ) + + def test_delete_resource_class_no_resource_class(self): + self.placement_fixture.mock_delete.side_effect = ks_exc.NotFound() + self.assertRaises( + n_exc.PlacementResourceClassNotFound, + self.placement_api_client.delete_resource_class, + RESOURCE_CLASS_NAME + )