From 78986dcae2f12f18ae2380111e06e793998b06c2 Mon Sep 17 00:00:00 2001 From: Chris Friesen Date: Thu, 16 Feb 2017 11:28:54 -0600 Subject: [PATCH] 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 --- novaclient/__init__.py | 2 +- .../functional/v2/legacy/test_servers.py | 12 ++++ .../tests/functional/v2/test_servers.py | 63 +++++++++++++++++++ novaclient/tests/unit/test_shell.py | 1 + novaclient/tests/unit/v2/test_shell.py | 2 + novaclient/v2/shell.py | 63 ++++++++++++++++--- .../microversion-v2_47-4aa54fbbd519e421.yaml | 18 ++++++ 7 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 4a86d365..3b93a464 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1") # when client supported the max version, and bumped sequentially, otherwise # the client may break due to server side new version may include some # backward incompatible change. -API_MAX_VERSION = api_versions.APIVersion("2.46") +API_MAX_VERSION = api_versions.APIVersion("2.47") diff --git a/novaclient/tests/functional/v2/legacy/test_servers.py b/novaclient/tests/functional/v2/legacy/test_servers.py index 8976abd5..c0852a1b 100644 --- a/novaclient/tests/functional/v2/legacy/test_servers.py +++ b/novaclient/tests/functional/v2/legacy/test_servers.py @@ -159,3 +159,15 @@ class TestServersListNovaClient(base.ClientTestBase): # Cut header and footer of the table for server in precreated_servers: 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) diff --git a/novaclient/tests/functional/v2/test_servers.py b/novaclient/tests/functional/v2/test_servers.py index 611ace8d..c2290eb2 100644 --- a/novaclient/tests/functional/v2/test_servers.py +++ b/novaclient/tests/functional/v2/test_servers.py @@ -263,3 +263,66 @@ class TestServersAutoAllocateNetworkCLI(base.ClientTestBase): network = self._find_network_in_table(server_info) self.assertIsNone( 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) diff --git a/novaclient/tests/unit/test_shell.py b/novaclient/tests/unit/test_shell.py index 01a646c2..10de208f 100644 --- a/novaclient/tests/unit/test_shell.py +++ b/novaclient/tests/unit/test_shell.py @@ -364,6 +364,7 @@ class ShellTest(utils.TestCase): def setUp(self): super(ShellTest, self).setUp() self.mock_client = mock.MagicMock() + self.mock_client.return_value.api_version = novaclient.API_MIN_VERSION self.useFixture(fixtures.MonkeyPatch('novaclient.client.Client', self.mock_client)) self.nc_util = mock.patch('novaclient.utils.isunauthenticated').start() diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index 15a267ea..45ee5d52 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2974,6 +2974,8 @@ class ShellTest(utils.TestCase): 44, # 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. + 47, # NOTE(cfriesen): 47 adds support for flavor details embedded + # within the server details ]) versions_supported = set(range(0, novaclient.API_MAX_VERSION.ver_minor + 1)) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index 951f6e9e..1b59e957 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -971,6 +971,21 @@ def _poll_for_status(poll_fn, obj_id, action, final_ok_states, 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): for item in collection: keys = item.__dict__.keys() @@ -1503,8 +1518,15 @@ def do_list(cs, args): if arg in args: search_opts[arg] = getattr(args, arg) - filters = {'flavor': lambda f: f['id'], - 'security_groups': utils.format_security_groups} + filters = {'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' @@ -1548,6 +1570,11 @@ def do_list(cs, args): cols = [] 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: cols, fmts = _get_list_table_columns_and_formatters( 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) flavor = info.get('flavor', {}) - flavor_id = flavor.get('id', '') - if minimal: - info['flavor'] = flavor_id + if cs.api_version >= api_versions.APIVersion('2.47'): + # The "flavor" field is a JSON representation of a dict containing the + # 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: - try: - info['flavor'] = '%s (%s)' % (_find_flavor(cs, flavor_id).name, - flavor_id) - except Exception: - info['flavor'] = '%s (%s)' % (_("Flavor not found"), flavor_id) + # Prior to microversion 2.47 we just have the ID of the flavor so we + # need to retrieve the flavor information (which may have changed + # since the instance was booted). + flavor_id = flavor.get('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: # when we have multiple nics the info will include the diff --git a/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml new file mode 100644 index 00000000..c5945adb --- /dev/null +++ b/releasenotes/notes/microversion-v2_47-4aa54fbbd519e421.yaml @@ -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. +