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 1c979fa44..9046add12 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -59,7 +59,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. @@ -70,8 +73,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 @@ -100,7 +106,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 @@ -110,22 +116,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 086702619..33a821715 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -33,12 +33,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) @@ -51,18 +47,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.