Merge "Switch flavor ops in the cloud layer to proxy"

This commit is contained in:
Zuul 2020-12-24 11:05:48 +00:00 committed by Gerrit Code Review
commit eefd1609ac
9 changed files with 173 additions and 146 deletions

View File

@ -159,29 +159,13 @@ class ComputeCloudMixin(_normalize.Normalizer):
:returns: A list of flavor ``munch.Munch``.
"""
data = proxy._json_response(
self.compute.get(
'/flavors/detail', params=dict(is_public='None')),
error_message="Error fetching flavor list")
flavors = self._normalize_flavors(
self._get_and_munchify('flavors', data))
data = self.compute.flavors(details=True)
flavors = []
for flavor in flavors:
for flavor in data:
if not flavor.extra_specs and get_extra:
endpoint = "/flavors/{id}/os-extra_specs".format(
id=flavor.id)
try:
data = proxy._json_response(
self.compute.get(endpoint),
error_message="Error fetching flavor extra specs")
flavor.extra_specs = self._get_and_munchify(
'extra_specs', data)
except exc.OpenStackCloudHTTPError as e:
flavor.extra_specs = {}
self.log.debug(
'Fetching extra specs for flavor failed:'
' %(msg)s', {'msg': str(e)})
flavor.fetch_extra_specs(self.compute)
flavors.append(flavor._to_munch(original_names=False))
return flavors
def list_server_security_groups(self, server):
@ -441,9 +425,12 @@ class ComputeCloudMixin(_normalize.Normalizer):
found.
"""
search_func = functools.partial(
self.search_flavors, get_extra=get_extra)
return _utils._get_entity(self, search_func, name_or_id, filters)
if not filters:
filters = {}
flavor = self.compute.find_flavor(
name_or_id, get_extra_specs=get_extra, **filters)
if flavor:
return flavor._to_munch(original_names=False)
def get_flavor_by_id(self, id, get_extra=False):
""" Get a flavor by ID
@ -454,29 +441,8 @@ class ComputeCloudMixin(_normalize.Normalizer):
specs.
:returns: A flavor ``munch.Munch``.
"""
data = proxy._json_response(
self.compute.get('/flavors/{id}'.format(id=id)),
error_message="Error getting flavor with ID {id}".format(id=id)
)
flavor = self._normalize_flavor(
self._get_and_munchify('flavor', data))
if not flavor.extra_specs and get_extra:
endpoint = "/flavors/{id}/os-extra_specs".format(
id=flavor.id)
try:
data = proxy._json_response(
self.compute.get(endpoint),
error_message="Error fetching flavor extra specs")
flavor.extra_specs = self._get_and_munchify(
'extra_specs', data)
except exc.OpenStackCloudHTTPError as e:
flavor.extra_specs = {}
self.log.debug(
'Fetching extra specs for flavor failed:'
' %(msg)s', {'msg': str(e)})
return flavor
flavor = self.compute.get_flavor(id, get_extra_specs=get_extra)
return flavor._to_munch(original_names=False)
def get_server_console(self, server, length=None):
"""Get the console log for a server.
@ -1412,27 +1378,23 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
with _utils.shade_exceptions("Failed to create flavor {name}".format(
name=name)):
payload = {
'disk': disk,
'OS-FLV-EXT-DATA:ephemeral': ephemeral,
'id': flavorid,
'os-flavor-access:is_public': is_public,
'name': name,
'ram': ram,
'rxtx_factor': rxtx_factor,
'swap': swap,
'vcpus': vcpus,
}
if flavorid == 'auto':
payload['id'] = None
data = proxy._json_response(self.compute.post(
'/flavors',
json=dict(flavor=payload)))
attrs = {
'disk': disk,
'ephemeral': ephemeral,
'id': flavorid,
'is_public': is_public,
'name': name,
'ram': ram,
'rxtx_factor': rxtx_factor,
'swap': swap,
'vcpus': vcpus,
}
if flavorid == 'auto':
attrs['id'] = None
return self._normalize_flavor(
self._get_and_munchify('flavor', data))
flavor = self.compute.create_flavor(**attrs)
return flavor._to_munch(original_names=False)
def delete_flavor(self, name_or_id):
"""Delete a flavor
@ -1443,19 +1405,17 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
flavor = self.get_flavor(name_or_id, get_extra=False)
if flavor is None:
self.log.debug(
"Flavor %s not found for deleting", name_or_id)
return False
proxy._json_response(
self.compute.delete(
'/flavors/{id}'.format(id=flavor['id'])),
error_message="Unable to delete flavor {name}".format(
name=name_or_id))
return True
try:
flavor = self.compute.find_flavor(name_or_id)
if not flavor:
self.log.debug(
"Flavor %s not found for deleting", name_or_id)
return False
self.compute.delete_flavor(flavor)
return True
except exceptions.SDKException:
raise exceptions.OpenStackCloudException(
"Unable to delete flavor {name}".format(name=name_or_id))
def set_flavor_specs(self, flavor_id, extra_specs):
"""Add extra specs to a flavor
@ -1466,11 +1426,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
:raises: OpenStackCloudResourceNotFound if flavor ID is not found.
"""
proxy._json_response(
self.compute.post(
"/flavors/{id}/os-extra_specs".format(id=flavor_id),
json=dict(extra_specs=extra_specs)),
error_message="Unable to set flavor specs")
self.compute.create_flavor_extra_specs(flavor_id, extra_specs)
def unset_flavor_specs(self, flavor_id, keys):
"""Delete extra specs from a flavor
@ -1482,24 +1438,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudResourceNotFound if flavor ID is not found.
"""
for key in keys:
proxy._json_response(
self.compute.delete(
"/flavors/{id}/os-extra_specs/{key}".format(
id=flavor_id, key=key)),
error_message="Unable to delete flavor spec {0}".format(key))
def _mod_flavor_access(self, action, flavor_id, project_id):
"""Common method for adding and removing flavor access
"""
with _utils.shade_exceptions("Error trying to {action} access from "
"flavor ID {flavor}".format(
action=action, flavor=flavor_id)):
endpoint = '/flavors/{id}/action'.format(id=flavor_id)
access = {'tenant': project_id}
access_key = '{action}TenantAccess'.format(action=action)
proxy._json_response(
self.compute.post(endpoint, json={access_key: access}))
self.compute.delete_flavor_extra_specs_property(flavor_id, key)
def add_flavor_access(self, flavor_id, project_id):
"""Grant access to a private flavor for a project/tenant.
@ -1509,7 +1448,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
self._mod_flavor_access('add', flavor_id, project_id)
self.compute.flavor_add_tenant_access(flavor_id, project_id)
def remove_flavor_access(self, flavor_id, project_id):
"""Revoke access from a private flavor for a project/tenant.
@ -1519,7 +1458,7 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
self._mod_flavor_access('remove', flavor_id, project_id)
self.compute.flavor_remove_tenant_access(flavor_id, project_id)
def list_flavor_access(self, flavor_id):
"""List access from a private flavor for a project/tenant.
@ -1530,14 +1469,8 @@ class ComputeCloudMixin(_normalize.Normalizer):
:raises: OpenStackCloudException on operation error.
"""
data = proxy._json_response(
self.compute.get(
'/flavors/{id}/os-flavor-access'.format(id=flavor_id)),
error_message=(
"Error trying to list access from flavorID {flavor}".format(
flavor=flavor_id)))
return _utils.normalize_flavor_accesses(
self._get_and_munchify('flavor_access', data))
access = self.compute.get_flavor_access(flavor_id)
return _utils.normalize_flavor_accesses(access)
def list_hypervisors(self, filters={}):
"""List all hypervisors

View File

@ -62,7 +62,7 @@ class Proxy(proxy.Proxy):
# ========== Flavors ==========
def find_flavor(self, name_or_id, ignore_missing=True,
get_extra_specs=False):
get_extra_specs=False, **query):
"""Find a single flavor
:param name_or_id: The name or ID of a flavor.
@ -73,10 +73,14 @@ class Proxy(proxy.Proxy):
:param bool get_extra_specs: When set to ``True`` and extra_specs not
present in the response will invoke additional API call to fetch
extra_specs.
:param kwargs query: Optional query parameters to be sent to limit
the flavors being returned.
:returns: One :class:`~openstack.compute.v2.flavor.Flavor` or None
"""
flavor = self._find(_flavor.Flavor, name_or_id,
ignore_missing=ignore_missing)
flavor = self._find(
_flavor.Flavor, name_or_id, ignore_missing=ignore_missing, **query)
if flavor and get_extra_specs and not flavor.extra_specs:
flavor = flavor.fetch_extra_specs(self)
return flavor
@ -139,19 +143,25 @@ class Proxy(proxy.Proxy):
flavor = flavor.fetch_extra_specs(self)
return flavor
def flavors(self, details=True, **query):
def flavors(self, details=True, get_extra_specs=False, **query):
"""Return a generator of flavors
:param bool details: When ``True``, returns
:class:`~openstack.compute.v2.flavor.Flavor` objects,
with additional attributes filled.
:param bool get_extra_specs: When set to ``True`` and extra_specs not
present in the response will invoke additional API call to fetch
extra_specs.
:param kwargs query: Optional query parameters to be sent to limit
the flavors being returned.
:returns: A generator of flavor objects
"""
base_path = '/flavors/detail' if details else '/flavors'
return self._list(_flavor.Flavor, base_path=base_path, **query)
for flv in self._list(_flavor.Flavor, base_path=base_path, **query):
if get_extra_specs and not flv.extra_specs:
flv = flv.fetch_extra_specs(self)
yield flv
def flavor_add_tenant_access(self, flavor, tenant):
"""Adds tenant/project access to flavor.

View File

@ -63,7 +63,7 @@ class Flavor(resource.Resource):
# TODO(mordred) extra_specs can historically also come from
# OS-FLV-WITH-EXT-SPECS:extra_specs. Do we care?
#: A dictionary of the flavor's extra-specs key-and-value pairs.
extra_specs = resource.Body('extra_specs', type=dict)
extra_specs = resource.Body('extra_specs', type=dict, default={})
@classmethod
def list(cls, session, paginated=True, base_path='/flavors/detail',

View File

@ -66,10 +66,8 @@ class TestFlavor(base.BaseFunctionalTest):
# We should also always have ephemeral and public attributes
self.assertIn('ephemeral', flavor)
self.assertIn('OS-FLV-EXT-DATA:ephemeral', flavor)
self.assertEqual(5, flavor['ephemeral'])
self.assertIn('is_public', flavor)
self.assertIn('os-flavor-access:is_public', flavor)
self.assertTrue(flavor['is_public'])
for key in flavor_kwargs.keys():

View File

@ -18,6 +18,7 @@ from testscenarios import load_tests_apply_scenarios as load_tests # noqa
import openstack
import openstack.cloud
from openstack.cloud import meta
from openstack.compute.v2 import flavor as _flavor
from openstack import exceptions
from openstack.tests import fakes
from openstack.tests.unit import base
@ -436,10 +437,16 @@ class TestMemoryCache(base.TestCase):
endpoint=fakes.COMPUTE_ENDPOINT)
uris_to_mock = [
dict(method='GET', uri=mock_uri, json={'flavors': []}),
dict(method='GET', uri=mock_uri,
validate=dict(
headers={'OpenStack-API-Version': 'compute 2.53'}),
json={'flavors': []}),
dict(method='GET', uri=mock_uri,
validate=dict(
headers={'OpenStack-API-Version': 'compute 2.53'}),
json={'flavors': fakes.FAKE_FLAVOR_LIST})
]
self.use_compute_discovery()
self.register_uris(uris_to_mock)
@ -447,8 +454,11 @@ class TestMemoryCache(base.TestCase):
self.assertEqual([], self.cloud.list_flavors())
fake_flavor_dicts = self.cloud._normalize_flavors(
fakes.FAKE_FLAVOR_LIST)
fake_flavor_dicts = [
_flavor.Flavor(connection=self.cloud, **f)
for f in fakes.FAKE_FLAVOR_LIST
]
self.cloud.list_flavors.invalidate(self.cloud)
self.assertEqual(fake_flavor_dicts, self.cloud.list_flavors())

View File

@ -791,11 +791,12 @@ class TestCreateServer(base.TestCase):
dict(method='GET',
uri='https://image.example.com/v2/images',
json=fake_image_search_return),
self.get_nova_discovery_mock_dict(),
dict(method='GET',
uri=self.get_mock_url(
'compute', 'public', append=['flavors', 'detail'],
qs_elements=['is_public=None']),
json={'flavors': fakes.FAKE_FLAVOR_LIST}),
'compute', 'public', append=['flavors', 'vanilla'],
qs_elements=[]),
json=fakes.FAKE_FLAVOR),
dict(method='POST',
uri=self.get_mock_url(
'compute', 'public', append=['servers']),

View File

@ -18,8 +18,13 @@ from openstack.tests.unit import base
class TestFlavors(base.TestCase):
def setUp(self):
super(TestFlavors, self).setUp()
# self.use_compute_discovery()
def test_create_flavor(self):
self.use_compute_discovery()
self.register_uris([
dict(method='POST',
uri='{endpoint}/flavors'.format(
@ -44,11 +49,12 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_delete_flavor(self):
self.use_compute_discovery()
self.register_uris([
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
uri='{endpoint}/flavors/vanilla'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
json={'flavors': fakes.FAKE_FLAVOR_LIST}),
json=fakes.FAKE_FLAVOR),
dict(method='DELETE',
uri='{endpoint}/flavors/{id}'.format(
endpoint=fakes.COMPUTE_ENDPOINT, id=fakes.FLAVOR_ID))])
@ -57,7 +63,12 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_delete_flavor_not_found(self):
self.use_compute_discovery()
self.register_uris([
dict(method='GET',
uri='{endpoint}/flavors/invalid'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
status_code=404),
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
@ -68,7 +79,12 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_delete_flavor_exception(self):
self.use_compute_discovery()
self.register_uris([
dict(method='GET',
uri='{endpoint}/flavors/vanilla'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
json=fakes.FAKE_FLAVOR),
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
@ -82,6 +98,7 @@ class TestFlavors(base.TestCase):
self.cloud.delete_flavor, 'vanilla')
def test_list_flavors(self):
self.use_compute_discovery()
uris_to_mock = [
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
@ -106,6 +123,7 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_list_flavors_with_extra(self):
self.use_compute_discovery()
uris_to_mock = [
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
@ -136,6 +154,7 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_get_flavor_by_ram(self):
self.use_compute_discovery()
uris_to_mock = [
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
@ -154,6 +173,7 @@ class TestFlavors(base.TestCase):
self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id'])
def test_get_flavor_by_ram_and_include(self):
self.use_compute_discovery()
uris_to_mock = [
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
@ -171,6 +191,7 @@ class TestFlavors(base.TestCase):
self.assertEqual(fakes.STRAWBERRY_FLAVOR_ID, flavor['id'])
def test_get_flavor_by_ram_not_found(self):
self.use_compute_discovery()
self.register_uris([
dict(method='GET',
uri='{endpoint}/flavors/detail?is_public=None'.format(
@ -182,19 +203,19 @@ class TestFlavors(base.TestCase):
ram=100)
def test_get_flavor_string_and_int(self):
flavor_list_uri = '{endpoint}/flavors/detail?is_public=None'.format(
endpoint=fakes.COMPUTE_ENDPOINT)
self.use_compute_discovery()
flavor_resource_uri = '{endpoint}/flavors/1/os-extra_specs'.format(
endpoint=fakes.COMPUTE_ENDPOINT)
flavor_list_json = {'flavors': [fakes.make_fake_flavor(
'1', 'vanilla')]}
flavor = fakes.make_fake_flavor('1', 'vanilla')
flavor_json = {'extra_specs': {}}
self.register_uris([
dict(method='GET', uri=flavor_list_uri, json=flavor_list_json),
dict(method='GET',
uri='{endpoint}/flavors/1'.format(
endpoint=fakes.COMPUTE_ENDPOINT),
json=flavor),
dict(method='GET', uri=flavor_resource_uri, json=flavor_json),
dict(method='GET', uri=flavor_list_uri, json=flavor_list_json),
dict(method='GET', uri=flavor_resource_uri, json=flavor_json)])
])
flavor1 = self.cloud.get_flavor('1')
self.assertEqual('1', flavor1['id'])
@ -202,6 +223,7 @@ class TestFlavors(base.TestCase):
self.assertEqual('1', flavor2['id'])
def test_set_flavor_specs(self):
self.use_compute_discovery()
extra_specs = dict(key1='value1')
self.register_uris([
dict(method='POST',
@ -213,6 +235,7 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_unset_flavor_specs(self):
self.use_compute_discovery()
keys = ['key1', 'key2']
self.register_uris([
dict(method='DELETE',
@ -262,6 +285,7 @@ class TestFlavors(base.TestCase):
self.assert_calls()
def test_get_flavor_by_id(self):
self.use_compute_discovery()
flavor_uri = '{endpoint}/flavors/1'.format(
endpoint=fakes.COMPUTE_ENDPOINT)
flavor_json = {'flavor': fakes.make_fake_flavor('1', 'vanilla')}
@ -278,6 +302,7 @@ class TestFlavors(base.TestCase):
self.assertEqual({}, flavor2.extra_specs)
def test_get_flavor_with_extra_specs(self):
self.use_compute_discovery()
flavor_uri = '{endpoint}/flavors/1'.format(
endpoint=fakes.COMPUTE_ENDPOINT)
flavor_extra_uri = '{endpoint}/flavors/1/os-extra_specs'.format(

View File

@ -51,6 +51,13 @@ class TestFlavor(TestComputeProxy):
def test_flavor_find(self):
self.verify_find(self.proxy.find_flavor, flavor.Flavor)
def test_flavor_find_query(self):
self.verify_find(
self.proxy.find_flavor, flavor.Flavor,
method_kwargs={"a": "b"},
expected_kwargs={"a": "b", "ignore_missing": False}
)
def test_flavor_find_fetch_extra(self):
"""fetch extra_specs is triggered"""
with mock.patch(
@ -133,17 +140,56 @@ class TestFlavor(TestComputeProxy):
)
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,
"base_path": "/flavors/detail"})
@mock.patch("openstack.proxy.Proxy._list", auto_spec=True)
@mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs",
auto_spec=True)
def test_flavors_detailed(self, fetch_mock, list_mock):
res = self.proxy.flavors(details=True)
for r in res:
self.assertIsNotNone(r)
fetch_mock.assert_not_called()
list_mock.assert_called_with(
flavor.Flavor,
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,
"base_path": "/flavors"})
@mock.patch("openstack.proxy.Proxy._list", auto_spec=True)
@mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs",
auto_spec=True)
def test_flavors_not_detailed(self, fetch_mock, list_mock):
res = self.proxy.flavors(details=False)
for r in res:
self.assertIsNotNone(r)
fetch_mock.assert_not_called()
list_mock.assert_called_with(
flavor.Flavor,
base_path="/flavors"
)
@mock.patch("openstack.proxy.Proxy._list", auto_spec=True)
@mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs",
auto_spec=True)
def test_flavors_query(self, fetch_mock, list_mock):
res = self.proxy.flavors(details=False, get_extra_specs=True, a="b")
for r in res:
fetch_mock.assert_called_with(self.proxy)
list_mock.assert_called_with(
flavor.Flavor,
base_path="/flavors",
a="b"
)
@mock.patch("openstack.proxy.Proxy._list", auto_spec=True)
@mock.patch("openstack.compute.v2.flavor.Flavor.fetch_extra_specs",
auto_spec=True)
def test_flavors_get_extra(self, fetch_mock, list_mock):
res = self.proxy.flavors(details=False, get_extra_specs=True)
for r in res:
fetch_mock.assert_called_with(self.proxy)
list_mock.assert_called_with(
flavor.Flavor,
base_path="/flavors"
)
def test_flavor_get_access(self):
self._verify("openstack.compute.v2.flavor.Flavor.get_access",

View File

@ -0,0 +1,4 @@
---
other:
- Flavor operations of the cloud layer are switched to the rely on
the proxy layer