From 5805b461eb78dce4a807010af95615057e361229 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Sat, 5 Sep 2020 14:13:40 +0200 Subject: [PATCH] Add additional compute flavor operations In order to proceed with switching flavor in OSC from novaclient/self api onto SDK few new methods need to be supported. Those include flavor access and set of extra_specs related operations. While extra_specs in terms of SDK should be a separate resource, it is not easy to implement it this way. On the other hand all those operations are definive subset of flavor related operations. In addition also merge FlavorDetail into Flavor class Change-Id: Ia4f60acce5e0e5665e2ba704fe4cd0ec81437eb5 --- doc/source/user/proxies/compute.rst | 7 +- openstack/compute/v2/_proxy.py | 127 +++++++++++-- openstack/compute/v2/flavor.py | 121 +++++++++++- .../functional/compute/v2/test_flavor.py | 97 +++++++++- .../tests/unit/compute/v2/test_flavor.py | 176 +++++++++++++++++- openstack/tests/unit/compute/v2/test_proxy.py | 157 +++++++++++++++- ...d-compute-flavor-ops-12149e58299c413e.yaml | 7 + 7 files changed, 652 insertions(+), 40 deletions(-) create mode 100644 releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml diff --git a/doc/source/user/proxies/compute.rst b/doc/source/user/proxies/compute.rst index cc59d322c..eaa904271 100644 --- a/doc/source/user/proxies/compute.rst +++ b/doc/source/user/proxies/compute.rst @@ -66,7 +66,12 @@ Flavor Operations .. autoclass:: openstack.compute.v2._proxy.Proxy :noindex: - :members: create_flavor, delete_flavor, get_flavor, find_flavor, flavors + :members: create_flavor, delete_flavor, get_flavor, find_flavor, flavors, + flavor_add_tenant_access, flavor_remove_tenant_access, + get_flavor_access, fetch_flavor_extra_specs, + create_flavor_extra_specs, get_flavor_extra_specs_property, + update_flavor_extra_specs_property, + delete_flavor_extra_specs_property Service Operations ^^^^^^^^^^^^^^^^^^ diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index fb2aa5464..b6d3c7a2e 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -58,7 +58,10 @@ class Proxy(proxy.Proxy): """ return self._list(extension.Extension) - def find_flavor(self, name_or_id, ignore_missing=True): + # ========== Flavors ========== + + def find_flavor(self, name_or_id, ignore_missing=True, + get_extra_specs=False): """Find a single flavor :param name_or_id: The name or ID of a flavor. @@ -69,8 +72,11 @@ class Proxy(proxy.Proxy): attempting to find a nonexistent resource. :returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None """ - return self._find(_flavor.Flavor, name_or_id, - ignore_missing=ignore_missing) + flavor = self._find(_flavor.Flavor, name_or_id, + ignore_missing=ignore_missing) + if flavor and get_extra_specs and not flavor.extra_specs: + flavor = flavor.fetch_extra_specs(self) + return flavor def create_flavor(self, **attrs): """Create a new flavor from attributes @@ -99,7 +105,7 @@ class Proxy(proxy.Proxy): """ self._delete(_flavor.Flavor, flavor, ignore_missing=ignore_missing) - def get_flavor(self, flavor): + def get_flavor(self, flavor, get_extra_specs=False): """Get a single flavor :param flavor: The value can be the ID of a flavor or a @@ -109,22 +115,121 @@ class Proxy(proxy.Proxy): :raises: :class:`~openstack.exceptions.ResourceNotFound` when no resource can be found. """ - return self._get(_flavor.Flavor, flavor) + flavor = self._get(_flavor.Flavor, flavor) + if get_extra_specs and not flavor.extra_specs: + flavor = flavor.fetch_extra_specs(self) + return flavor def flavors(self, details=True, **query): """Return a generator of flavors :param bool details: When ``True``, returns - :class:`~openstack.compute.v2.flavor.FlavorDetail` objects, - otherwise :class:`~openstack.compute.v2.flavor.Flavor`. - *Default: ``True``* + :class:`~openstack.compute.v2.flavor.Flavor` objects, + with additional attributes filled. :param kwargs query: Optional query parameters to be sent to limit - the flavors being returned. + the flavors being returned. :returns: A generator of flavor objects """ - flv = _flavor.FlavorDetail if details else _flavor.Flavor - return self._list(flv, **query) + base_path = '/flavors/detail' if details else '/flavors' + return self._list(_flavor.Flavor, base_path=base_path, **query) + + def flavor_add_tenant_access(self, flavor, tenant): + """Adds tenant/project access to flavor. + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str tenant: The UUID of the tenant. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.add_tenant_access(self, tenant) + + def flavor_remove_tenant_access(self, flavor, tenant): + """Removes tenant/project access to flavor. + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str tenant: The UUID of the tenant. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.remove_tenant_access(self, tenant) + + def get_flavor_access(self, flavor): + """Lists tenants who have access to private flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + + :returns: List of dicts with flavor_id and tenant_id attributes. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.get_access(self) + + def fetch_flavor_extra_specs(self, flavor): + """Lists Extra Specs of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.fetch_extra_specs(self) + + def create_flavor_extra_specs(self, flavor, extra_specs): + """Lists Extra Specs of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param dict extra_specs: dict of extra specs + + :returns: One :class:`~openstack.compute.v2.flavor.Flavor` + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.create_extra_specs(self, specs=extra_specs) + + def get_flavor_extra_specs_property(self, flavor, prop): + """Get specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + + :returns: String value of the requested property. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.get_extra_specs_property(self, prop) + + def update_flavor_extra_specs_property(self, flavor, prop, val): + """Update specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + :param str val: Property value. + + :returns: String value of the requested property. + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.update_extra_specs_property(self, prop, val) + + def delete_flavor_extra_specs_property(self, flavor, prop): + """Delete specific Extra Spec property of a flavor + + :param flavor: Either the ID of a flavor or a + :class:`~openstack.compute.v2.flavor.Flavor` instance. + :param str prop: Property name. + + :returns: None + """ + flavor = self._get_resource(_flavor.Flavor, flavor) + return flavor.delete_extra_specs_property(self, prop) + + # ========== Aggregates ========== def aggregates(self, **query): """Return a generator of aggregate diff --git a/openstack/compute/v2/flavor.py b/openstack/compute/v2/flavor.py index 1e6c65eb9..00c9ec54b 100644 --- a/openstack/compute/v2/flavor.py +++ b/openstack/compute/v2/flavor.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack import exceptions from openstack import resource +from openstack import utils class Flavor(resource.Resource): @@ -62,12 +64,117 @@ class Flavor(resource.Resource): #: A dictionary of the flavor's extra-specs key-and-value pairs. extra_specs = resource.Body('extra_specs', type=dict) + @classmethod + def list(cls, session, paginated=True, base_path='/flavors/detail', + allow_unknown_params=False, **params): + # Find will invoke list when name was passed. Since we want to return + # flavor with details (same as direct get) we need to swap default here + # and list with "/flavors" if no details explicitely requested + if 'is_public' not in params or params['is_public'] is None: + # is_public is ternary - None means give all flavors. + # Force it to string to avoid requests skipping it. + params['is_public'] = 'None' + return super(Flavor, cls).list( + session, paginated=paginated, + base_path=base_path, + allow_unknown_params=allow_unknown_params, + **params) -class FlavorDetail(Flavor): - base_path = '/flavors/detail' + def _action(self, session, body, microversion=None): + """Preform flavor actions given the message body.""" + url = utils.urljoin(Flavor.base_path, self.id, 'action') + headers = {'Accept': ''} + attrs = {} + if microversion: + # Do not reset microversion if it is set on a session level + attrs['microversion'] = microversion + response = session.post( + url, json=body, headers=headers, **attrs) + exceptions.raise_from_response(response) + return response - allow_create = False - allow_fetch = False - allow_commit = False - allow_delete = False - allow_list = True + def add_tenant_access(self, session, tenant): + """Adds flavor access to a tenant and flavor.""" + body = {'addTenantAccess': {'tenant': tenant}} + self._action(session, body) + + def remove_tenant_access(self, session, tenant): + """Removes flavor access to a tenant and flavor.""" + body = {'removeTenantAccess': {'tenant': tenant}} + self._action(session, body) + + def get_access(self, session): + """Lists tenants who have access to a private flavor and adds private + flavor access to and removes private flavor access from tenants. By + default, only administrators can manage private flavor access. A + private flavor has is_public set to false while a public flavor has + is_public set to true. + + :return: List of dicts with flavor_id and tenant_id attributes + """ + url = utils.urljoin(Flavor.base_path, self.id, 'os-flavor-access') + response = session.get(url) + exceptions.raise_from_response(response) + return response.json().get('flavor_access', []) + + def fetch_extra_specs(self, session): + """Fetch extra_specs of the flavor + Starting with 2.61 extra_specs are returned with the flavor details, + before that a separate call is required + """ + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('extra_specs', {}) + self._update(extra_specs=specs) + return self + + def create_extra_specs(self, session, specs): + """Creates extra specs for a flavor""" + url = utils.urljoin(Flavor.base_path, self.id, 'os-extra_specs') + microversion = self._get_microversion_for(session, 'create') + response = session.post( + url, + json={'extra_specs': specs}, + microversion=microversion) + exceptions.raise_from_response(response) + specs = response.json().get('extra_specs', {}) + self._update(extra_specs=specs) + return self + + def get_extra_specs_property(self, session, prop): + """Get individual extra_spec property""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'fetch') + response = session.get(url, microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def update_extra_specs_property(self, session, prop, val): + """Update An Extra Spec For A Flavor""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'commit') + response = session.put( + url, + json={prop: val}, + microversion=microversion) + exceptions.raise_from_response(response) + val = response.json().get(prop) + return val + + def delete_extra_specs_property(self, session, prop): + """Delete An Extra Spec For A Flavor""" + url = utils.urljoin(Flavor.base_path, self.id, + 'os-extra_specs', prop) + microversion = self._get_microversion_for(session, 'delete') + response = session.delete( + url, + microversion=microversion) + exceptions.raise_from_response(response) + + +FlavorDetail = Flavor diff --git a/openstack/tests/functional/compute/v2/test_flavor.py b/openstack/tests/functional/compute/v2/test_flavor.py index 92d636a4b..13281edcb 100644 --- a/openstack/tests/functional/compute/v2/test_flavor.py +++ b/openstack/tests/functional/compute/v2/test_flavor.py @@ -9,7 +9,7 @@ # 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 uuid from openstack import exceptions from openstack.tests.functional import base @@ -19,7 +19,7 @@ class TestFlavor(base.BaseFunctionalTest): def setUp(self): super(TestFlavor, self).setUp() - + self.new_item_name = self.getUniqueString('flavor') self.one_flavor = list(self.conn.compute.flavors())[0] def test_flavors(self): @@ -50,3 +50,96 @@ class TestFlavor(base.BaseFunctionalTest): self.assertRaises(exceptions.ResourceNotFound, self.conn.compute.find_flavor, "not a flavor", ignore_missing=False) + + def test_list_flavors(self): + pub_flavor_name = self.new_item_name + '_public' + priv_flavor_name = self.new_item_name + '_private' + public_kwargs = dict( + name=pub_flavor_name, ram=1024, vcpus=2, disk=10, is_public=True + ) + private_kwargs = dict( + name=priv_flavor_name, ram=1024, vcpus=2, disk=10, is_public=False + ) + + # Create a public and private flavor. We expect both to be listed + # for an operator. + self.operator_cloud.compute.create_flavor(**public_kwargs) + self.operator_cloud.compute.create_flavor(**private_kwargs) + + flavors = self.operator_cloud.compute.flavors() + + # Flavor list will include the standard devstack flavors. We just want + # to make sure both of the flavors we just created are present. + found = [] + for f in flavors: + # extra_specs should be added within list_flavors() + self.assertIn('extra_specs', f) + if f['name'] in (pub_flavor_name, priv_flavor_name): + found.append(f) + self.assertEqual(2, len(found)) + + def test_flavor_access(self): + flavor_name = uuid.uuid4().hex + flv = self.operator_cloud.compute.create_flavor( + is_public=False, + name=flavor_name, + ram=128, + vcpus=1, + disk=0) + self.addCleanup(self.conn.compute.delete_flavor, flv.id) + # Validate the 'demo' user cannot see the new flavor + flv_cmp = self.user_cloud.compute.find_flavor(flavor_name) + self.assertIsNone(flv_cmp) + + # Validate we can see the new flavor ourselves + flv_cmp = self.operator_cloud.compute.find_flavor(flavor_name) + self.assertIsNotNone(flv_cmp) + self.assertEqual(flavor_name, flv_cmp.name) + + project = self.operator_cloud.get_project('demo') + self.assertIsNotNone(project) + + # Now give 'demo' access + self.operator_cloud.compute.flavor_add_tenant_access( + flv.id, project['id']) + + # Now see if the 'demo' user has access to it + flv_cmp = self.user_cloud.compute.find_flavor( + flavor_name) + self.assertIsNotNone(flv_cmp) + + # Now remove 'demo' access and check we can't find it + self.operator_cloud.compute.flavor_remove_tenant_access( + flv.id, project['id']) + + flv_cmp = self.user_cloud.compute.find_flavor( + flavor_name) + self.assertIsNone(flv_cmp) + + def test_extra_props_calls(self): + flavor_name = uuid.uuid4().hex + flv = self.conn.compute.create_flavor( + is_public=False, + name=flavor_name, + ram=128, + vcpus=1, + disk=0) + self.addCleanup(self.conn.compute.delete_flavor, flv.id) + # Create extra_specs + specs = { + 'a': 'b' + } + self.conn.compute.create_flavor_extra_specs(flv, extra_specs=specs) + # verify specs + flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + self.assertDictEqual(specs, flv_cmp.extra_specs) + # update + self.conn.compute.update_flavor_extra_specs_property(flv, 'c', 'd') + val_cmp = self.conn.compute.get_flavor_extra_specs_property(flv, 'c') + # fetch single prop + self.assertEqual('d', val_cmp) + # drop new prop + self.conn.compute.delete_flavor_extra_specs_property(flv, 'c') + # re-fetch and ensure prev state + flv_cmp = self.conn.compute.fetch_flavor_extra_specs(flv) + self.assertDictEqual(specs, flv_cmp.extra_specs) diff --git a/openstack/tests/unit/compute/v2/test_flavor.py b/openstack/tests/unit/compute/v2/test_flavor.py index c781711d2..bdaff9b79 100644 --- a/openstack/tests/unit/compute/v2/test_flavor.py +++ b/openstack/tests/unit/compute/v2/test_flavor.py @@ -9,6 +9,9 @@ # 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 unittest import mock + +from keystoneauth1 import adapter from openstack.tests.unit import base @@ -33,6 +36,12 @@ BASIC_EXAMPLE = { class TestFlavor(base.TestCase): + def setUp(self): + super(TestFlavor, self).setUp() + self.sess = mock.Mock(spec=adapter.Adapter) + self.sess.default_microversion = 1 + self.sess._get_connection = mock.Mock(return_value=self.cloud) + def test_basic(self): sot = flavor.Flavor() self.assertEqual('flavor', sot.resource_key) @@ -71,13 +80,160 @@ class TestFlavor(base.TestCase): sot.is_disabled) self.assertEqual(BASIC_EXAMPLE['rxtx_factor'], sot.rxtx_factor) - def test_detail(self): - sot = flavor.FlavorDetail() - self.assertEqual('flavor', sot.resource_key) - self.assertEqual('flavors', sot.resources_key) - self.assertEqual('/flavors/detail', sot.base_path) - self.assertFalse(sot.allow_create) - self.assertFalse(sot.allow_fetch) - self.assertFalse(sot.allow_commit) - self.assertFalse(sot.allow_delete) - self.assertTrue(sot.allow_list) + def test_add_tenant_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + sot.add_tenant_access(self.sess, 'fake_tenant') + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/action', + json={ + 'addTenantAccess': { + 'tenant': 'fake_tenant'}}, + headers={'Accept': ''} + ) + + def test_remove_tenant_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + sot.remove_tenant_access(self.sess, 'fake_tenant') + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/action', + json={ + 'removeTenantAccess': { + 'tenant': 'fake_tenant'}}, + headers={'Accept': ''} + ) + + def test_get_flavor_access(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = {'flavor_access': [ + {'flavor_id': 'fake_flavor', + 'tenant_id': 'fake_tenant'} + ]} + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_access(self.sess) + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-flavor-access', + ) + + self.assertEqual(resp.body['flavor_access'], rsp) + + def test_fetch_extra_specs(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'extra_specs': + {'a': 'b', + 'c': 'd'} + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.fetch_extra_specs(self.sess) + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs', + microversion=self.sess.default_microversion + ) + + self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) + self.assertIsInstance(rsp, flavor.Flavor) + + def test_create_extra_specs(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + specs = { + 'a': 'b', + 'c': 'd' + } + resp = mock.Mock() + resp.body = { + 'extra_specs': specs + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.post = mock.Mock(return_value=resp) + + rsp = sot.create_extra_specs(self.sess, specs) + + self.sess.post.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs', + json={'extra_specs': specs}, + microversion=self.sess.default_microversion + ) + + self.assertEqual(resp.body['extra_specs'], rsp.extra_specs) + self.assertIsInstance(rsp, flavor.Flavor) + + def test_get_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'a': 'b' + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.get = mock.Mock(return_value=resp) + + rsp = sot.get_extra_specs_property(self.sess, 'a') + + self.sess.get.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + microversion=self.sess.default_microversion + ) + + self.assertEqual('b', rsp) + + def test_update_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = { + 'a': 'b' + } + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.put = mock.Mock(return_value=resp) + + rsp = sot.update_extra_specs_property(self.sess, 'a', 'b') + + self.sess.put.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + json={'a': 'b'}, + microversion=self.sess.default_microversion + ) + + self.assertEqual('b', rsp) + + def test_delete_extra_specs_property(self): + sot = flavor.Flavor(**BASIC_EXAMPLE) + resp = mock.Mock() + resp.body = None + resp.json = mock.Mock(return_value=resp.body) + resp.status_code = 200 + self.sess.delete = mock.Mock(return_value=resp) + + rsp = sot.delete_extra_specs_property(self.sess, 'a') + + self.sess.delete.assert_called_with( + 'flavors/IDENTIFIER/os-extra_specs/a', + microversion=self.sess.default_microversion + ) + + self.assertIsNone(rsp) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 3ccf1233b..516f4dfae 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -32,12 +32,8 @@ class TestComputeProxy(test_proxy_base.TestProxyBase): super(TestComputeProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def test_extension_find(self): - self.verify_find(self.proxy.find_extension, extension.Extension) - - def test_extensions(self): - self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) +class TestFlavor(TestComputeProxy): def test_flavor_create(self): self.verify_create(self.proxy.create_flavor, flavor.Flavor) @@ -50,18 +46,161 @@ class TestComputeProxy(test_proxy_base.TestProxyBase): def test_flavor_find(self): self.verify_find(self.proxy.find_flavor, flavor.Flavor) - def test_flavor_get(self): - self.verify_get(self.proxy.get_flavor, flavor.Flavor) + def test_flavor_find_fetch_extra(self): + """fetch extra_specs is triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._find', + self.proxy.find_flavor, + method_args=['res', True, True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'], + expected_kwargs={'ignore_missing': True} + ) + mocked.assert_called_once() + + def test_flavor_find_skip_fetch_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor(extra_specs={'a': 'b'}) + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._find', + self.proxy.find_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'], + expected_kwargs={'ignore_missing': True} + ) + mocked.assert_not_called() + + def test_flavor_get_no_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res'], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_not_called() + + def test_flavor_get_fetch_extra(self): + """fetch extra_specs is triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor() + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_called_once() + + def test_flavor_get_skip_fetch_extra(self): + """fetch extra_specs not triggered""" + with mock.patch( + 'openstack.compute.v2.flavor.Flavor.fetch_extra_specs' + ) as mocked: + res = flavor.Flavor(extra_specs={'a': 'b'}) + mocked.return_value = res + self._verify2( + 'openstack.proxy.Proxy._get', + self.proxy.get_flavor, + method_args=['res', True], + expected_result=res, + expected_args=[flavor.Flavor, 'res'] + ) + mocked.assert_not_called() def test_flavors_detailed(self): self.verify_list(self.proxy.flavors, flavor.FlavorDetail, method_kwargs={"details": True, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/flavors/detail"}) def test_flavors_not_detailed(self): self.verify_list(self.proxy.flavors, flavor.Flavor, method_kwargs={"details": False, "query": 1}, - expected_kwargs={"query": 1}) + expected_kwargs={"query": 1, + "base_path": "/flavors"}) + + def test_flavor_get_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.get_access", + self.proxy.get_flavor_access, + method_args=["value"], + expected_args=[]) + + def test_flavor_add_tenant_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.add_tenant_access", + self.proxy.flavor_add_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=["fake-tenant"]) + + def test_flavor_remove_tenant_access(self): + self._verify("openstack.compute.v2.flavor.Flavor.remove_tenant_access", + self.proxy.flavor_remove_tenant_access, + method_args=["value", "fake-tenant"], + expected_args=["fake-tenant"]) + + def test_flavor_fetch_extra_specs(self): + self._verify("openstack.compute.v2.flavor.Flavor.fetch_extra_specs", + self.proxy.fetch_flavor_extra_specs, + method_args=["value"], + expected_args=[]) + + def test_create_flavor_extra_specs(self): + specs = { + 'a': 'b' + } + self._verify("openstack.compute.v2.flavor.Flavor.create_extra_specs", + self.proxy.create_flavor_extra_specs, + method_args=["value", specs], + expected_kwargs={"specs": specs}) + + def test_get_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.get_extra_specs_property", + self.proxy.get_flavor_extra_specs_property, + method_args=["value", "prop"], + expected_args=["prop"]) + + def test_update_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.update_extra_specs_property", + self.proxy.update_flavor_extra_specs_property, + method_args=["value", "prop", "val"], + expected_args=["prop", "val"]) + + def test_delete_flavor_extra_specs_prop(self): + self._verify( + "openstack.compute.v2.flavor.Flavor.delete_extra_specs_property", + self.proxy.delete_flavor_extra_specs_property, + method_args=["value", "prop"], + expected_args=["prop"]) + + +class TestCompute(TestComputeProxy): + def test_extension_find(self): + self.verify_find(self.proxy.find_extension, extension.Extension) + + def test_extensions(self): + self.verify_list_no_kwargs(self.proxy.extensions, extension.Extension) def test_image_delete(self): self.verify_delete(self.proxy.delete_image, image.Image, False) diff --git a/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml new file mode 100644 index 000000000..fcb4e0345 --- /dev/null +++ b/releasenotes/notes/add-compute-flavor-ops-12149e58299c413e.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add additional compute flavor operations (flavor_add_tenant_access, flavor_remove_tenant_access, get_flavor_access, extra_specs fetching/updating). +other: + - | + Merge FlavorDetails into Flavor class.