compute: Fix 'server * -f yaml' output
Make use of 'FormattableColumn'-derived formatters, which provide better output than what we were using before, particularly for the YAML output format. For example, compare before for the 'server show' command: $ openstack --os-compute-api-version 2.79 server show test-server -f yaml ... addresses: private=fdff:77e3:9bb4:0:f816:3eff:fe6d:a944, 10.0.0.44 flavor: disk='1', ephemeral='0', extra_specs.hw_rng:allowed='True', original_name='m1.tiny', ram='512', swap='0', vcpus='1' ... To after: $ openstack --os-compute-api-version 2.79 server show test-server -f yaml ... addresses: private: - fdff:77e3:9bb4:0:f816:3eff:fe6d:a944 - 10.0.0.44 flavor: disk: 1 ephemeral: 0 extra_specs: hw_rng:allowed: 'True' original_name: m1.tiny ram: 512 swap: 0 vcpus: 1 ... Similarly, compare before for 'server list': $ openstack --os-compute-api-version 2.79 server list -f yaml - ... Networks: private=fdff:77e3:9bb4:0:f816:3eff:fe6d:a944, 10.0.0.44 Power State: Running Properties: '' ... To after: $ openstack --os-compute-api-version 2.79 server list -f yaml - ... Networks: private: - fdff:77e3:9bb4:0:f816:3eff:fe6d:a944 - 10.0.0.44 Power State: 1 Properties: {} ... We also fix the human-readable output for the 'tags' field. Before: $ openstack --os-compute-api-version 2.79 server list ... | tags | ['bar', 'foo'] | After: $ openstack --os-compute-api-version 2.79 server list ... | tags | bar, foo | Change-Id: I7a8349106e211c57c4577b75326b39b88bd9ac1e Signed-off-by: Stephen Finucane <sfinucan@redhat.com>
This commit is contained in:
parent
f2c49142f0
commit
03776d82e5
@ -21,6 +21,7 @@ import io
|
||||
import logging
|
||||
import os
|
||||
|
||||
from cliff import columns as cliff_columns
|
||||
import iso8601
|
||||
from novaclient import api_versions
|
||||
from novaclient.v2 import servers
|
||||
@ -41,28 +42,9 @@ LOG = logging.getLogger(__name__)
|
||||
IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)'
|
||||
|
||||
|
||||
def _format_servers_list_networks(networks):
|
||||
"""Return a formatted string of a server's networks
|
||||
class PowerStateColumn(cliff_columns.FormattableColumn):
|
||||
"""Generate a formatted string of a server's power state."""
|
||||
|
||||
:param networks: a Server.networks field
|
||||
:rtype: a string of formatted network addresses
|
||||
"""
|
||||
output = []
|
||||
for (network, addresses) in networks.items():
|
||||
if not addresses:
|
||||
continue
|
||||
addresses_csv = ', '.join(addresses)
|
||||
group = "%s=%s" % (network, addresses_csv)
|
||||
output.append(group)
|
||||
return '; '.join(output)
|
||||
|
||||
|
||||
def _format_servers_list_power_state(state):
|
||||
"""Return a formatted string of a server's power state
|
||||
|
||||
:param state: the power state number of a server
|
||||
:rtype: a string mapped to the power state number
|
||||
"""
|
||||
power_states = [
|
||||
'NOSTATE', # 0x00
|
||||
'Running', # 0x01
|
||||
@ -74,8 +56,9 @@ def _format_servers_list_power_state(state):
|
||||
'Suspended' # 0x07
|
||||
]
|
||||
|
||||
def human_readable(self):
|
||||
try:
|
||||
return power_states[state]
|
||||
return self.power_states[self._value]
|
||||
except Exception:
|
||||
return 'N/A'
|
||||
|
||||
@ -169,7 +152,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
|
||||
except Exception:
|
||||
info['flavor'] = flavor_id
|
||||
else:
|
||||
info['flavor'] = utils.format_dict(flavor_info)
|
||||
info['flavor'] = format_columns.DictColumn(flavor_info)
|
||||
|
||||
if 'os-extended-volumes:volumes_attached' in info:
|
||||
info.update(
|
||||
@ -185,19 +168,15 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
|
||||
info.pop('security_groups'))
|
||||
}
|
||||
)
|
||||
if 'tags' in info:
|
||||
info.update({'tags': format_columns.ListColumn(info.pop('tags'))})
|
||||
|
||||
# NOTE(dtroyer): novaclient splits these into separate entries...
|
||||
# Format addresses in a useful way
|
||||
info['addresses'] = _format_servers_list_networks(server.networks)
|
||||
info['addresses'] = format_columns.DictListColumn(server.networks)
|
||||
|
||||
# Map 'metadata' field to 'properties'
|
||||
if not info['metadata']:
|
||||
info.update(
|
||||
{'properties': utils.format_dict(info.pop('metadata'))}
|
||||
)
|
||||
else:
|
||||
info.update(
|
||||
{'properties': format_columns.DictColumn(info.pop('metadata'))}
|
||||
)
|
||||
info['properties'] = format_columns.DictColumn(info.pop('metadata'))
|
||||
|
||||
# Migrate tenant_id to project_id naming
|
||||
if 'tenant_id' in info:
|
||||
@ -205,7 +184,7 @@ def _prep_server_detail(compute_client, image_client, server, refresh=True):
|
||||
|
||||
# Map power state num to meaningful string
|
||||
if 'OS-EXT-STS:power_state' in info:
|
||||
info['OS-EXT-STS:power_state'] = _format_servers_list_power_state(
|
||||
info['OS-EXT-STS:power_state'] = PowerStateColumn(
|
||||
info['OS-EXT-STS:power_state'])
|
||||
|
||||
# Remove values that are long and not too useful
|
||||
@ -1873,10 +1852,9 @@ class ListServer(command.Lister):
|
||||
s, columns,
|
||||
mixed_case_fields=mixed_case_fields,
|
||||
formatters={
|
||||
'OS-EXT-STS:power_state':
|
||||
_format_servers_list_power_state,
|
||||
'Networks': _format_servers_list_networks,
|
||||
'Metadata': utils.format_dict,
|
||||
'OS-EXT-STS:power_state': PowerStateColumn,
|
||||
'Networks': format_columns.DictListColumn,
|
||||
'Metadata': format_columns.DictColumn,
|
||||
},
|
||||
) for s in data
|
||||
),
|
||||
|
@ -10,6 +10,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import time
|
||||
import uuid
|
||||
@ -346,6 +347,14 @@ class ServerTests(common.ComputeTestCase):
|
||||
# DevStack without cells.
|
||||
self.skipTest("No Network service present")
|
||||
|
||||
def _chain_addresses(addresses):
|
||||
# Flatten a dict of lists mapping network names to IP addresses,
|
||||
# returning only the IP addresses
|
||||
#
|
||||
# >>> _chain_addresses({'private': ['10.1.0.32', '172.24.5.41']})
|
||||
# ['10.1.0.32', '172.24.5.41']
|
||||
return itertools.chain(*[*addresses.values()])
|
||||
|
||||
cmd_output = self.server_create()
|
||||
name = cmd_output['name']
|
||||
self.wait_for_status(name, "ACTIVE")
|
||||
@ -387,7 +396,7 @@ class ServerTests(common.ComputeTestCase):
|
||||
'server show -f json ' +
|
||||
name
|
||||
))
|
||||
if floating_ip not in cmd_output['addresses']:
|
||||
if floating_ip not in _chain_addresses(cmd_output['addresses']):
|
||||
# Hang out for a bit and try again
|
||||
print('retrying floating IP check')
|
||||
wait_time += 10
|
||||
@ -397,7 +406,7 @@ class ServerTests(common.ComputeTestCase):
|
||||
|
||||
self.assertIn(
|
||||
floating_ip,
|
||||
cmd_output['addresses'],
|
||||
_chain_addresses(cmd_output['addresses']),
|
||||
)
|
||||
|
||||
# detach ip
|
||||
@ -417,7 +426,7 @@ class ServerTests(common.ComputeTestCase):
|
||||
'server show -f json ' +
|
||||
name
|
||||
))
|
||||
if floating_ip in cmd_output['addresses']:
|
||||
if floating_ip in _chain_addresses(cmd_output['addresses']):
|
||||
# Hang out for a bit and try again
|
||||
print('retrying floating IP check')
|
||||
wait_time += 10
|
||||
@ -431,7 +440,7 @@ class ServerTests(common.ComputeTestCase):
|
||||
))
|
||||
self.assertNotIn(
|
||||
floating_ip,
|
||||
cmd_output['addresses'],
|
||||
_chain_addresses(cmd_output['addresses']),
|
||||
)
|
||||
|
||||
def test_server_reboot(self):
|
||||
@ -856,8 +865,7 @@ class ServerTests(common.ComputeTestCase):
|
||||
server = json.loads(self.openstack(
|
||||
'server show -f json ' + server_name
|
||||
))
|
||||
self.assertIsNotNone(server['addresses'])
|
||||
self.assertEqual('', server['addresses'])
|
||||
self.assertEqual({}, server['addresses'])
|
||||
|
||||
def test_server_create_with_security_group(self):
|
||||
"""Test server create with security group ID and name"""
|
||||
|
@ -22,6 +22,7 @@ from unittest.mock import call
|
||||
import iso8601
|
||||
from novaclient import api_versions
|
||||
from openstack import exceptions as sdk_exceptions
|
||||
from osc_lib.cli import format_columns
|
||||
from osc_lib import exceptions
|
||||
from osc_lib import utils as common_utils
|
||||
|
||||
@ -33,6 +34,29 @@ from openstackclient.tests.unit import utils
|
||||
from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes
|
||||
|
||||
|
||||
class TestPowerStateColumn(utils.TestCase):
|
||||
|
||||
def test_human_readable(self):
|
||||
self.assertEqual(
|
||||
'NOSTATE', server.PowerStateColumn(0x00).human_readable())
|
||||
self.assertEqual(
|
||||
'Running', server.PowerStateColumn(0x01).human_readable())
|
||||
self.assertEqual(
|
||||
'', server.PowerStateColumn(0x02).human_readable())
|
||||
self.assertEqual(
|
||||
'Paused', server.PowerStateColumn(0x03).human_readable())
|
||||
self.assertEqual(
|
||||
'Shutdown', server.PowerStateColumn(0x04).human_readable())
|
||||
self.assertEqual(
|
||||
'', server.PowerStateColumn(0x05).human_readable())
|
||||
self.assertEqual(
|
||||
'Crashed', server.PowerStateColumn(0x06).human_readable())
|
||||
self.assertEqual(
|
||||
'Suspended', server.PowerStateColumn(0x07).human_readable())
|
||||
self.assertEqual(
|
||||
'N/A', server.PowerStateColumn(0x08).human_readable())
|
||||
|
||||
|
||||
class TestServer(compute_fakes.TestComputev2):
|
||||
|
||||
def setUp(self):
|
||||
@ -1015,15 +1039,15 @@ class TestServerCreate(TestServer):
|
||||
|
||||
def datalist(self):
|
||||
datalist = (
|
||||
server._format_servers_list_power_state(
|
||||
server.PowerStateColumn(
|
||||
getattr(self.new_server, 'OS-EXT-STS:power_state')),
|
||||
'',
|
||||
format_columns.DictListColumn({}),
|
||||
self.flavor.name + ' (' + self.new_server.flavor.get('id') + ')',
|
||||
self.new_server.id,
|
||||
self.image.name + ' (' + self.new_server.image.get('id') + ')',
|
||||
self.new_server.name,
|
||||
self.new_server.networks,
|
||||
'',
|
||||
format_columns.DictColumn(self.new_server.metadata),
|
||||
)
|
||||
return datalist
|
||||
|
||||
@ -3041,7 +3065,7 @@ class TestServerList(TestServer):
|
||||
s.id,
|
||||
s.name,
|
||||
s.status,
|
||||
server._format_servers_list_networks(s.networks),
|
||||
format_columns.DictListColumn(s.networks),
|
||||
# Image will be an empty string if boot-from-volume
|
||||
self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
|
||||
self.flavor.name,
|
||||
@ -3051,10 +3075,10 @@ class TestServerList(TestServer):
|
||||
s.name,
|
||||
s.status,
|
||||
getattr(s, 'OS-EXT-STS:task_state'),
|
||||
server._format_servers_list_power_state(
|
||||
server.PowerStateColumn(
|
||||
getattr(s, 'OS-EXT-STS:power_state')
|
||||
),
|
||||
server._format_servers_list_networks(s.networks),
|
||||
format_columns.DictListColumn(s.networks),
|
||||
# Image will be an empty string if boot-from-volume
|
||||
self.image.name if s.image else server.IMAGE_STRING_FOR_BFV,
|
||||
s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
|
||||
@ -3062,13 +3086,13 @@ class TestServerList(TestServer):
|
||||
s.flavor['id'],
|
||||
getattr(s, 'OS-EXT-AZ:availability_zone'),
|
||||
getattr(s, 'OS-EXT-SRV-ATTR:host'),
|
||||
s.Metadata,
|
||||
format_columns.DictColumn({}),
|
||||
))
|
||||
self.data_no_name_lookup.append((
|
||||
s.id,
|
||||
s.name,
|
||||
s.status,
|
||||
server._format_servers_list_networks(s.networks),
|
||||
format_columns.DictListColumn(s.networks),
|
||||
# Image will be an empty string if boot-from-volume
|
||||
s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV,
|
||||
s.flavor['id']
|
||||
@ -3128,7 +3152,7 @@ class TestServerList(TestServer):
|
||||
|
||||
self.servers_mock.list.assert_called_with(**self.kwargs)
|
||||
self.assertEqual(self.columns_long, columns)
|
||||
self.assertEqual(tuple(self.data_long), tuple(data))
|
||||
self.assertCountEqual(tuple(self.data_long), tuple(data))
|
||||
|
||||
def test_server_list_column_option(self):
|
||||
arglist = [
|
||||
@ -3429,9 +3453,11 @@ class TestServerList(TestServer):
|
||||
def test_server_list_v269_with_partial_constructs(self):
|
||||
self.app.client_manager.compute.api_version = \
|
||||
api_versions.APIVersion('2.69')
|
||||
|
||||
arglist = []
|
||||
verifylist = []
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
# include "partial results" from non-responsive part of
|
||||
# infrastructure.
|
||||
server_dict = {
|
||||
@ -3454,10 +3480,10 @@ class TestServerList(TestServer):
|
||||
# it will fail at formatting the networks info later on.
|
||||
"networks": {}
|
||||
}
|
||||
server = compute_fakes.fakes.FakeResource(
|
||||
_server = compute_fakes.fakes.FakeResource(
|
||||
info=server_dict,
|
||||
)
|
||||
self.servers.append(server)
|
||||
self.servers.append(_server)
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
# get the first three servers out since our interest is in the partial
|
||||
# server.
|
||||
@ -3466,8 +3492,13 @@ class TestServerList(TestServer):
|
||||
next(data)
|
||||
partial_server = next(data)
|
||||
expected_row = (
|
||||
'server-id-95a56bfc4xxxxxx28d7e418bfd97813a', '',
|
||||
'UNKNOWN', '', '', '')
|
||||
'server-id-95a56bfc4xxxxxx28d7e418bfd97813a',
|
||||
'',
|
||||
'UNKNOWN',
|
||||
format_columns.DictListColumn({}),
|
||||
'',
|
||||
'',
|
||||
)
|
||||
self.assertEqual(expected_row, partial_server)
|
||||
|
||||
def test_server_list_with_tag(self):
|
||||
@ -6292,15 +6323,16 @@ class TestServerShow(TestServer):
|
||||
)
|
||||
|
||||
self.data = (
|
||||
'Running',
|
||||
'public=10.20.30.40, 2001:db8::f',
|
||||
server.PowerStateColumn(
|
||||
getattr(self.server, 'OS-EXT-STS:power_state')),
|
||||
format_columns.DictListColumn(self.server.networks),
|
||||
self.flavor.name + " (" + self.flavor.id + ")",
|
||||
self.server.id,
|
||||
self.image.name + " (" + self.image.id + ")",
|
||||
self.server.name,
|
||||
{'public': ['10.20.30.40', '2001:db8::f']},
|
||||
'tenant-id-xxx',
|
||||
'',
|
||||
format_columns.DictColumn({}),
|
||||
)
|
||||
|
||||
def test_show_no_options(self):
|
||||
@ -6323,7 +6355,7 @@ class TestServerShow(TestServer):
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(self.columns, columns)
|
||||
self.assertEqual(self.data, data)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
def test_show_embedded_flavor(self):
|
||||
# Tests using --os-compute-api-version >= 2.47 where the flavor
|
||||
@ -6350,7 +6382,7 @@ class TestServerShow(TestServer):
|
||||
self.assertEqual(self.columns, columns)
|
||||
# Since the flavor details are in a dict we can't be sure of the
|
||||
# ordering so just assert that one of the keys is in the output.
|
||||
self.assertIn('original_name', data[2])
|
||||
self.assertIn('original_name', data[2]._value)
|
||||
|
||||
def test_show_diagnostics(self):
|
||||
arglist = [
|
||||
@ -6719,50 +6751,6 @@ class TestServerGeneral(TestServer):
|
||||
self.assertRaises(exceptions.CommandError,
|
||||
server._get_ip_address, self.OLD, 'private', [6])
|
||||
|
||||
def test_format_servers_list_power_state(self):
|
||||
self.assertEqual("NOSTATE",
|
||||
server._format_servers_list_power_state(0x00))
|
||||
self.assertEqual("Running",
|
||||
server._format_servers_list_power_state(0x01))
|
||||
self.assertEqual("",
|
||||
server._format_servers_list_power_state(0x02))
|
||||
self.assertEqual("Paused",
|
||||
server._format_servers_list_power_state(0x03))
|
||||
self.assertEqual("Shutdown",
|
||||
server._format_servers_list_power_state(0x04))
|
||||
self.assertEqual("",
|
||||
server._format_servers_list_power_state(0x05))
|
||||
self.assertEqual("Crashed",
|
||||
server._format_servers_list_power_state(0x06))
|
||||
self.assertEqual("Suspended",
|
||||
server._format_servers_list_power_state(0x07))
|
||||
self.assertEqual("N/A",
|
||||
server._format_servers_list_power_state(0x08))
|
||||
|
||||
def test_format_servers_list_networks(self):
|
||||
# Setup network info to test.
|
||||
networks = {
|
||||
u'public': [u'10.20.30.40', u'2001:db8::f'],
|
||||
u'private': [u'2001:db8::f', u'10.20.30.40'],
|
||||
}
|
||||
|
||||
# Prepare expected data.
|
||||
# Since networks is a dict, whose items are in random order, there
|
||||
# could be two results after formatted.
|
||||
data_1 = (u'private=2001:db8::f, 10.20.30.40; '
|
||||
u'public=10.20.30.40, 2001:db8::f')
|
||||
data_2 = (u'public=10.20.30.40, 2001:db8::f; '
|
||||
u'private=2001:db8::f, 10.20.30.40')
|
||||
|
||||
# Call _format_servers_list_networks().
|
||||
networks_format = server._format_servers_list_networks(networks)
|
||||
|
||||
msg = ('Network string is not formatted correctly.\n'
|
||||
'reference = %s or %s\n'
|
||||
'actual = %s\n' %
|
||||
(data_1, data_2, networks_format))
|
||||
self.assertIn(networks_format, (data_1, data_2), msg)
|
||||
|
||||
@mock.patch('osc_lib.utils.find_resource')
|
||||
def test_prep_server_detail(self, find_resource):
|
||||
# Setup mock method return value. utils.find_resource() will be called
|
||||
@ -6789,14 +6777,14 @@ class TestServerGeneral(TestServer):
|
||||
info = {
|
||||
'id': _server.id,
|
||||
'name': _server.name,
|
||||
'addresses': u'public=10.20.30.40, 2001:db8::f',
|
||||
'flavor': u'%s (%s)' % (_flavor.name, _flavor.id),
|
||||
'image': u'%s (%s)' % (_image.name, _image.id),
|
||||
'project_id': u'tenant-id-xxx',
|
||||
'properties': '',
|
||||
'OS-EXT-STS:power_state': server._format_servers_list_power_state(
|
||||
'image': '%s (%s)' % (_image.name, _image.id),
|
||||
'flavor': '%s (%s)' % (_flavor.name, _flavor.id),
|
||||
'OS-EXT-STS:power_state': server.PowerStateColumn(
|
||||
getattr(_server, 'OS-EXT-STS:power_state')),
|
||||
'properties': '',
|
||||
'volumes_attached': [{"id": "6344fe9d-ef20-45b2-91a6"}],
|
||||
'addresses': format_columns.DictListColumn(_server.networks),
|
||||
'project_id': 'tenant-id-xxx',
|
||||
}
|
||||
|
||||
# Call _prep_server_detail().
|
||||
@ -6809,4 +6797,4 @@ class TestServerGeneral(TestServer):
|
||||
server_detail.pop('networks')
|
||||
|
||||
# Check the results.
|
||||
self.assertEqual(info, server_detail)
|
||||
self.assertCountEqual(info, server_detail)
|
||||
|
@ -0,0 +1,10 @@
|
||||
---
|
||||
fixes:
|
||||
- |
|
||||
The ``addresses`` and ``flavor`` fields of the ``server show`` command will
|
||||
now be correctly rendered as a list of objects and an object, respectively.
|
||||
- |
|
||||
The ``networks`` and ``properties`` fields of the ``server list`` command
|
||||
will now be rendered as objects. In addition, the ``power_state`` field
|
||||
will now be humanized and rendered as a string value when using the table
|
||||
formatter.
|
Loading…
Reference in New Issue
Block a user