diff --git a/doc/source/command-objects/port.rst b/doc/source/command-objects/port.rst index 78677332b..414b7437f 100644 --- a/doc/source/command-objects/port.rst +++ b/doc/source/command-objects/port.rst @@ -19,3 +19,19 @@ Delete port(s) .. describe:: Port(s) to delete (name or ID) + +port show +--------- + +Display port details + +.. program:: port show +.. code:: bash + + os port show + + +.. _port_show-port: +.. describe:: + + Port to display (name or ID) diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py index 3ae30c8f7..451484c85 100644 --- a/openstackclient/common/utils.py +++ b/openstackclient/common/utils.py @@ -169,6 +169,16 @@ def format_list(data, separator=', '): return separator.join(sorted(data)) +def format_list_of_dicts(data): + """Return a formatted string of key value pairs for each dict + + :param data: a list of dicts + :rtype: a string formatted to key='value' with dicts separated by new line + """ + + return '\n'.join(format_dict(i) for i in data) + + def get_field(item, field): try: if isinstance(item, dict): diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 0d5b183e4..46cb031f9 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -14,6 +14,42 @@ """Port action implementations""" from openstackclient.common import command +from openstackclient.common import utils + + +def _format_admin_state(state): + return 'UP' if state else 'DOWN' + + +_formatters = { + 'admin_state_up': _format_admin_state, + 'allowed_address_pairs': utils.format_list_of_dicts, + 'binding_profile': utils.format_dict, + 'binding_vif_details': utils.format_dict, + 'dns_assignment': utils.format_list_of_dicts, + 'extra_dhcp_opts': utils.format_list_of_dicts, + 'fixed_ips': utils.format_list_of_dicts, + 'security_groups': utils.format_list, +} + + +def _get_columns(item): + columns = item.keys() + if 'tenant_id' in columns: + columns.remove('tenant_id') + columns.append('project_id') + binding_columns = [ + 'binding:host_id', + 'binding:profile', + 'binding:vif_details', + 'binding:vif_type', + 'binding:vnic_type', + ] + for binding_column in binding_columns: + if binding_column in columns: + columns.remove(binding_column) + columns.append(binding_column.replace('binding:', 'binding_', 1)) + return sorted(columns) class DeletePort(command.Command): @@ -35,3 +71,23 @@ class DeletePort(command.Command): for port in parsed_args.port: res = client.find_port(port) client.delete_port(res) + + +class ShowPort(command.ShowOne): + """Display port details""" + + def get_parser(self, prog_name): + parser = super(ShowPort, self).get_parser(prog_name) + parser.add_argument( + 'port', + metavar="", + help="Port to display (name or ID)" + ) + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_port(parsed_args.port, ignore_missing=False) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (tuple(columns), data) diff --git a/openstackclient/tests/common/test_utils.py b/openstackclient/tests/common/test_utils.py index 064ad417e..62e3638ee 100644 --- a/openstackclient/tests/common/test_utils.py +++ b/openstackclient/tests/common/test_utils.py @@ -348,6 +348,15 @@ class TestFindResource(test_utils.TestCase): self.assertEqual(expected, utils.format_list(['a', 'b', 'c'])) self.assertEqual(expected, utils.format_list(['c', 'b', 'a'])) + def test_format_list_of_dicts(self): + expected = "a='b', c='d'\ne='f'" + sorted_data = [{'a': 'b', 'c': 'd'}, {'e': 'f'}] + unsorted_data = [{'c': 'd', 'a': 'b'}, {'e': 'f'}] + self.assertEqual(expected, utils.format_list_of_dicts(sorted_data)) + self.assertEqual(expected, utils.format_list_of_dicts(unsorted_data)) + self.assertEqual('', utils.format_list_of_dicts([])) + self.assertEqual('', utils.format_list_of_dicts([{}])) + def test_format_list_separator(self): expected = 'a\nb\nc' actual_pre_sorted = utils.format_list(['a', 'b', 'c'], separator='\n') diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index 4c862bd33..a73ae5aad 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -172,15 +172,31 @@ class FakePort(object): :param Dictionary methods: A dictionary with all methods :return: - A FakeResource object, with id, name, admin_state_up, - status, tenant_id + A FakeResource object, with id, name, etc. """ + # Set default attributes. port_attrs = { - 'id': 'port-id-' + uuid.uuid4().hex, - 'name': 'port-name-' + uuid.uuid4().hex, - 'status': 'ACTIVE', 'admin_state_up': True, + 'allowed_address_pairs': [{}], + 'binding:host_id': 'binding-host-id-' + uuid.uuid4().hex, + 'binding:profile': {}, + 'binding:vif_details': {}, + 'binding:vif_type': 'ovs', + 'binding:vnic_type': 'normal', + 'device_id': 'device-id-' + uuid.uuid4().hex, + 'device_owner': 'compute:nova', + 'dns_assignment': [{}], + 'dns_name': 'dns-name-' + uuid.uuid4().hex, + 'extra_dhcp_opts': [{}], + 'fixed_ips': [{}], + 'id': 'port-id-' + uuid.uuid4().hex, + 'mac_address': 'fa:16:3e:a9:4e:72', + 'name': 'port-name-' + uuid.uuid4().hex, + 'network_id': 'network-id-' + uuid.uuid4().hex, + 'port_security_enabled': True, + 'security_groups': [], + 'status': 'ACTIVE', 'tenant_id': 'project-id-' + uuid.uuid4().hex, } @@ -188,7 +204,16 @@ class FakePort(object): port_attrs.update(attrs) # Set default methods. - port_methods = {} + port_methods = { + 'keys': ['admin_state_up', 'allowed_address_pairs', + 'binding:host_id', 'binding:profile', + 'binding:vif_details', 'binding:vif_type', + 'binding:vnic_type', 'device_id', 'device_owner', + 'dns_assignment', 'dns_name', 'extra_dhcp_opts', + 'fixed_ips', 'id', 'mac_address', 'name', + 'network_id', 'port_security_enabled', + 'security_groups', 'status', 'tenant_id'], + } # Overwrite default methods. port_methods.update(methods) @@ -196,6 +221,15 @@ class FakePort(object): port = fakes.FakeResource(info=copy.deepcopy(port_attrs), methods=copy.deepcopy(port_methods), loaded=True) + + # Set attributes with special mappings. + port.project_id = port_attrs['tenant_id'] + port.binding_host_id = port_attrs['binding:host_id'] + port.binding_profile = port_attrs['binding:profile'] + port.binding_vif_details = port_attrs['binding:vif_details'] + port.binding_vif_type = port_attrs['binding:vif_type'] + port.binding_vnic_type = port_attrs['binding:vnic_type'] + return port @staticmethod diff --git a/openstackclient/tests/network/v2/test_port.py b/openstackclient/tests/network/v2/test_port.py index a1ddefa1d..bc246bd87 100644 --- a/openstackclient/tests/network/v2/test_port.py +++ b/openstackclient/tests/network/v2/test_port.py @@ -13,8 +13,10 @@ import mock +from openstackclient.common import utils from openstackclient.network.v2 import port from openstackclient.tests.network.v2 import fakes as network_fakes +from openstackclient.tests import utils as tests_utils class TestPort(network_fakes.TestNetworkV2): @@ -51,3 +53,88 @@ class TestDeletePort(TestPort): result = self.cmd.take_action(parsed_args) self.network.delete_port.assert_called_with(self._port) self.assertIsNone(result) + + +class TestShowPort(TestPort): + + # The port to show. + _port = network_fakes.FakePort.create_one_port() + + columns = ( + 'admin_state_up', + 'allowed_address_pairs', + 'binding_host_id', + 'binding_profile', + 'binding_vif_details', + 'binding_vif_type', + 'binding_vnic_type', + 'device_id', + 'device_owner', + 'dns_assignment', + 'dns_name', + 'extra_dhcp_opts', + 'fixed_ips', + 'id', + 'mac_address', + 'name', + 'network_id', + 'port_security_enabled', + 'project_id', + 'security_groups', + 'status', + ) + + data = ( + port._format_admin_state(_port.admin_state_up), + utils.format_list_of_dicts(_port.allowed_address_pairs), + _port.binding_host_id, + utils.format_dict(_port.binding_profile), + utils.format_dict(_port.binding_vif_details), + _port.binding_vif_type, + _port.binding_vnic_type, + _port.device_id, + _port.device_owner, + utils.format_list_of_dicts(_port.dns_assignment), + _port.dns_name, + utils.format_list_of_dicts(_port.extra_dhcp_opts), + utils.format_list_of_dicts(_port.fixed_ips), + _port.id, + _port.mac_address, + _port.name, + _port.network_id, + _port.port_security_enabled, + _port.project_id, + utils.format_list(_port.security_groups), + _port.status, + ) + + def setUp(self): + super(TestShowPort, self).setUp() + + self.network.find_port = mock.Mock(return_value=self._port) + + # Get the command object to test + self.cmd = port.ShowPort(self.app, self.namespace) + + def test_show_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_show_all_options(self): + arglist = [ + self._port.name, + ] + verifylist = [ + ('port', self._port.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_port.assert_called_with(self._port.name, + ignore_missing=False) + self.assertEqual(tuple(self.columns), columns) + self.assertEqual(self.data, data) diff --git a/releasenotes/notes/add-port-show-command-de0a599017189a21.yaml b/releasenotes/notes/add-port-show-command-de0a599017189a21.yaml new file mode 100644 index 000000000..cb6526635 --- /dev/null +++ b/releasenotes/notes/add-port-show-command-de0a599017189a21.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for the ``port show`` command. + [Bug `1519909 `_] diff --git a/setup.cfg b/setup.cfg index 2751eb317..d24e7aeaa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -333,6 +333,7 @@ openstack.network.v2 = network_set = openstackclient.network.v2.network:SetNetwork network_show = openstackclient.network.v2.network:ShowNetwork port_delete = openstackclient.network.v2.port:DeletePort + port_show = openstackclient.network.v2.port:ShowPort router_create = openstackclient.network.v2.router:CreateRouter router_delete = openstackclient.network.v2.router:DeleteRouter router_list = openstackclient.network.v2.router:ListRouter