2.47: Show flavor info in server details
This adds support for microversion 2.47 which directly embeds the flavor information in the server details. With this change, CLI requests with microversion >= 2.47 will no longer need to do additional queries to get the flavor and flavor extra_specs information. Instead, the flavor information will be output as separate key/value pairs with the keys namespaced with the "flavor:" prefix. As one would expect, these keys can also be specified as output fields when listing servers. Change-Id: Ic00ec95485485dff0fd4dcf8cad6ca56a481d512 Implements: blueprint instance-flavor-api Depends-On: If646149efb7eec8c90bf7d07c39ff4c495349941
This commit is contained in:
parent
c23324ef48
commit
78986dcae2
@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
|
|||||||
# when client supported the max version, and bumped sequentially, otherwise
|
# when client supported the max version, and bumped sequentially, otherwise
|
||||||
# the client may break due to server side new version may include some
|
# the client may break due to server side new version may include some
|
||||||
# backward incompatible change.
|
# backward incompatible change.
|
||||||
API_MAX_VERSION = api_versions.APIVersion("2.46")
|
API_MAX_VERSION = api_versions.APIVersion("2.47")
|
||||||
|
@ -159,3 +159,15 @@ class TestServersListNovaClient(base.ClientTestBase):
|
|||||||
# Cut header and footer of the table
|
# Cut header and footer of the table
|
||||||
for server in precreated_servers:
|
for server in precreated_servers:
|
||||||
self.assertIn(server.id, output)
|
self.assertIn(server.id, output)
|
||||||
|
|
||||||
|
def test_list_minimal(self):
|
||||||
|
name = uuidutils.generate_uuid()
|
||||||
|
uuid = self._create_server(name).id
|
||||||
|
server_output = self.nova("list --minimal")
|
||||||
|
# The only fields output are "ID" and "Name"
|
||||||
|
output_uuid = self._get_column_value_from_single_row_table(
|
||||||
|
server_output, 'ID')
|
||||||
|
output_name = self._get_column_value_from_single_row_table(
|
||||||
|
server_output, 'Name')
|
||||||
|
self.assertEqual(output_uuid, uuid)
|
||||||
|
self.assertEqual(output_name, name)
|
||||||
|
@ -263,3 +263,66 @@ class TestServersAutoAllocateNetworkCLI(base.ClientTestBase):
|
|||||||
network = self._find_network_in_table(server_info)
|
network = self._find_network_in_table(server_info)
|
||||||
self.assertIsNone(
|
self.assertIsNone(
|
||||||
network, 'Unexpected network allocation: %s' % server_info)
|
network, 'Unexpected network allocation: %s' % server_info)
|
||||||
|
|
||||||
|
|
||||||
|
class TestServersDetailsFlavorInfo(base.ClientTestBase):
|
||||||
|
|
||||||
|
COMPUTE_API_VERSION = '2.47'
|
||||||
|
|
||||||
|
def _validate_flavor_details(self, flavor_details, server_details):
|
||||||
|
# This is a mapping between the keys used in the flavor GET response
|
||||||
|
# and the keys used for the flavor information embedded in the server
|
||||||
|
# details.
|
||||||
|
flavor_key_mapping = {
|
||||||
|
"OS-FLV-EXT-DATA:ephemeral": "flavor:ephemeral",
|
||||||
|
"disk": "flavor:disk",
|
||||||
|
"extra_specs": "flavor:extra_specs",
|
||||||
|
"name": "flavor:original_name",
|
||||||
|
"ram": "flavor:ram",
|
||||||
|
"swap": "flavor:swap",
|
||||||
|
"vcpus": "flavor:vcpus",
|
||||||
|
}
|
||||||
|
|
||||||
|
for key in flavor_key_mapping:
|
||||||
|
flavor_val = self._get_value_from_the_table(
|
||||||
|
flavor_details, key)
|
||||||
|
server_flavor_val = self._get_value_from_the_table(
|
||||||
|
server_details, flavor_key_mapping[key])
|
||||||
|
if key is "swap" and flavor_val is "":
|
||||||
|
# "flavor-show" displays zero swap as empty string.
|
||||||
|
flavor_val = '0'
|
||||||
|
self.assertEqual(flavor_val, server_flavor_val)
|
||||||
|
|
||||||
|
def _setup_extra_specs(self, flavor_id):
|
||||||
|
extra_spec_key = "dummykey"
|
||||||
|
self.nova('flavor-key', params=('%(flavor)s set %(key)s=dummyval' %
|
||||||
|
{'flavor': flavor_id,
|
||||||
|
'key': extra_spec_key}))
|
||||||
|
unset_params = ('%(flavor)s unset %(key)s' %
|
||||||
|
{'flavor': flavor_id, 'key': extra_spec_key})
|
||||||
|
self.addCleanup(self.nova, 'flavor-key', params=unset_params)
|
||||||
|
|
||||||
|
def test_show(self):
|
||||||
|
self._setup_extra_specs(self.flavor.id)
|
||||||
|
uuid = self._create_server().id
|
||||||
|
server_output = self.nova("show %s" % uuid)
|
||||||
|
flavor_output = self.nova("flavor-show %s" % self.flavor.id)
|
||||||
|
self._validate_flavor_details(flavor_output, server_output)
|
||||||
|
|
||||||
|
def test_show_minimal(self):
|
||||||
|
uuid = self._create_server().id
|
||||||
|
server_output = self.nova("show --minimal %s" % uuid)
|
||||||
|
server_output_flavor = self._get_value_from_the_table(
|
||||||
|
server_output, 'flavor')
|
||||||
|
self.assertEqual(self.flavor.name, server_output_flavor)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
self._setup_extra_specs(self.flavor.id)
|
||||||
|
self._create_server()
|
||||||
|
server_output = self.nova("list --fields flavor:disk")
|
||||||
|
# namespaced fields get reformatted slightly as column names
|
||||||
|
server_flavor_val = self._get_column_value_from_single_row_table(
|
||||||
|
server_output, 'flavor: Disk')
|
||||||
|
flavor_output = self.nova("flavor-show %s" % self.flavor.id)
|
||||||
|
flavor_val = self._get_value_from_the_table(flavor_output, 'disk')
|
||||||
|
self.assertEqual(flavor_val, server_flavor_val)
|
||||||
|
@ -364,6 +364,7 @@ class ShellTest(utils.TestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(ShellTest, self).setUp()
|
super(ShellTest, self).setUp()
|
||||||
self.mock_client = mock.MagicMock()
|
self.mock_client = mock.MagicMock()
|
||||||
|
self.mock_client.return_value.api_version = novaclient.API_MIN_VERSION
|
||||||
self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client',
|
self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client',
|
||||||
self.mock_client))
|
self.mock_client))
|
||||||
self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start()
|
self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start()
|
||||||
|
@ -2974,6 +2974,8 @@ class ShellTest(utils.TestCase):
|
|||||||
44, # There are no version-wrapped shell method changes for this.
|
44, # There are no version-wrapped shell method changes for this.
|
||||||
45, # There are no version-wrapped shell method changes for this.
|
45, # There are no version-wrapped shell method changes for this.
|
||||||
46, # There are no version-wrapped shell method changes for this.
|
46, # There are no version-wrapped shell method changes for this.
|
||||||
|
47, # NOTE(cfriesen): 47 adds support for flavor details embedded
|
||||||
|
# within the server details
|
||||||
])
|
])
|
||||||
versions_supported = set(range(0,
|
versions_supported = set(range(0,
|
||||||
novaclient.API_MAX_VERSION.ver_minor + 1))
|
novaclient.API_MAX_VERSION.ver_minor + 1))
|
||||||
|
@ -971,6 +971,21 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states,
|
|||||||
time.sleep(poll_period)
|
time.sleep(poll_period)
|
||||||
|
|
||||||
|
|
||||||
|
def _expand_dict_attr(collection, attr):
|
||||||
|
"""Expand item attribute whose value is a dict.
|
||||||
|
|
||||||
|
Take a collection of items where the named attribute is known to have a
|
||||||
|
dictionary value and replace the named attribute with multiple attributes
|
||||||
|
whose names are the keys of the dictionary namespaced with the original
|
||||||
|
attribute name.
|
||||||
|
"""
|
||||||
|
for item in collection:
|
||||||
|
field = getattr(item, attr)
|
||||||
|
delattr(item, attr)
|
||||||
|
for subkey in field.keys():
|
||||||
|
setattr(item, attr + ':' + subkey, field[subkey])
|
||||||
|
|
||||||
|
|
||||||
def _translate_keys(collection, convert):
|
def _translate_keys(collection, convert):
|
||||||
for item in collection:
|
for item in collection:
|
||||||
keys = item.__dict__.keys()
|
keys = item.__dict__.keys()
|
||||||
@ -1503,8 +1518,15 @@ def do_list(cs, args):
|
|||||||
if arg in args:
|
if arg in args:
|
||||||
search_opts[arg] = getattr(args, arg)
|
search_opts[arg] = getattr(args, arg)
|
||||||
|
|
||||||
filters = {'flavor': lambda f: f['id'],
|
filters = {'security_groups': utils.format_security_groups}
|
||||||
'security_groups': utils.format_security_groups}
|
|
||||||
|
# In microversion 2.47 we started embedding flavor info in server details.
|
||||||
|
have_embedded_flavor_info = (
|
||||||
|
cs.api_version >= api_versions.APIVersion('2.47'))
|
||||||
|
# If we don't have embedded flavor info then we only report the flavor id
|
||||||
|
# rather than looking up the rest of the information.
|
||||||
|
if not have_embedded_flavor_info:
|
||||||
|
filters['flavor'] = lambda f: f['id']
|
||||||
|
|
||||||
id_col = 'ID'
|
id_col = 'ID'
|
||||||
|
|
||||||
@ -1548,6 +1570,11 @@ def do_list(cs, args):
|
|||||||
cols = []
|
cols = []
|
||||||
fmts = {}
|
fmts = {}
|
||||||
|
|
||||||
|
# For detailed lists, if we have embedded flavor information then replace
|
||||||
|
# the "flavor" attribute with more detailed information.
|
||||||
|
if detailed and have_embedded_flavor_info:
|
||||||
|
_expand_dict_attr(servers, 'flavor')
|
||||||
|
|
||||||
if servers:
|
if servers:
|
||||||
cols, fmts = _get_list_table_columns_and_formatters(
|
cols, fmts = _get_list_table_columns_and_formatters(
|
||||||
args.fields, servers, exclude_fields=('id',), filters=filters)
|
args.fields, servers, exclude_fields=('id',), filters=filters)
|
||||||
@ -2131,15 +2158,31 @@ def _print_server(cs, args, server=None, wrap=0):
|
|||||||
info['%s network' % network_label] = ', '.join(address_list)
|
info['%s network' % network_label] = ', '.join(address_list)
|
||||||
|
|
||||||
flavor = info.get('flavor', {})
|
flavor = info.get('flavor', {})
|
||||||
flavor_id = flavor.get('id', '')
|
if cs.api_version >= api_versions.APIVersion('2.47'):
|
||||||
if minimal:
|
# The "flavor" field is a JSON representation of a dict containing the
|
||||||
info['flavor'] = flavor_id
|
# flavor information used at boot.
|
||||||
|
if minimal:
|
||||||
|
# To retain something similar to the previous behaviour, keep the
|
||||||
|
# 'flavor' field name but just output the original name.
|
||||||
|
info['flavor'] = flavor['original_name']
|
||||||
|
else:
|
||||||
|
# Replace the "flavor" field with individual namespaced fields.
|
||||||
|
del info['flavor']
|
||||||
|
for key in flavor.keys():
|
||||||
|
info['flavor:' + key] = flavor[key]
|
||||||
else:
|
else:
|
||||||
try:
|
# Prior to microversion 2.47 we just have the ID of the flavor so we
|
||||||
info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name,
|
# need to retrieve the flavor information (which may have changed
|
||||||
flavor_id)
|
# since the instance was booted).
|
||||||
except Exception:
|
flavor_id = flavor.get('id', '')
|
||||||
info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id)
|
if minimal:
|
||||||
|
info['flavor'] = flavor_id
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name,
|
||||||
|
flavor_id)
|
||||||
|
except Exception:
|
||||||
|
info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id)
|
||||||
|
|
||||||
if 'security_groups' in info:
|
if 'security_groups' in info:
|
||||||
# when we have multiple nics the info will include the
|
# when we have multiple nics the info will include the
|
||||||
|
18
releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml
Normal file
18
releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Added support for microversion 2.47 which returns the flavor details
|
||||||
|
directly embedded in the server details when listing or showing servers.
|
||||||
|
With this change, CLI requests with microversion >= 2.47 will no longer
|
||||||
|
need to do additional queries to get the flavor and flavor extra_specs
|
||||||
|
information. Instead, the flavor information will be output as
|
||||||
|
separate key/value pairs with the keys namespaced with the
|
||||||
|
"flavor:" prefix. As one would expect, these keys can also be
|
||||||
|
specified as output fields when listing servers, like this:
|
||||||
|
|
||||||
|
``nova list --fields name,flavor:original_name``
|
||||||
|
|
||||||
|
When displaying details of a single server, the ``--minimal`` option will
|
||||||
|
display a ``flavor`` field with a value of the ``original_name`` of the
|
||||||
|
flavor. Prior to this microversion the value was the ``id`` of the flavor.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user