diff --git a/neutronclient/osc/utils.py b/neutronclient/osc/utils.py new file mode 100644 index 000000000..c9aecc1e3 --- /dev/null +++ b/neutronclient/osc/utils.py @@ -0,0 +1,195 @@ +# Copyright 2016 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""This module should contain OSC plugin generic methods. + +Methods in this module are candidates adopted to osc-lib. + +Stuffs specific to neutronclient OSC plugin should not be added +to this module. They should go to neutronclient.osc.v2.utils. +""" + +import operator + +from keystoneclient import exceptions as identity_exc +from keystoneclient.v3 import domains +from keystoneclient.v3 import projects +from osc_lib import utils + +from neutronclient._i18n import _ + + +LIST_BOTH = 'both' +LIST_SHORT_ONLY = 'short_only' +LIST_LONG_ONLY = 'long_only' + + +def get_column_definitions(attr_map, long_listing): + """Return table headers and column names for a listing table. + + :param attr_map: a list of table entry definitions. + Each entry should be a tuple consisting of + (API attribute name, header name, listing mode). For example: + (('id', 'ID', LIST_BOTH), + ('name', 'Name', LIST_BOTH), + ('tenant_id', 'Project', LIST_LONG_ONLY)) + The third field of each tuple must be one of LIST_BOTH, + LIST_LONG_ONLY (a corresponding column is shown only in a long mode), or + LIST_SHORT_ONLY (a corresponding column is shown only in a short mode). + :param long_listing: A boolean value which indicates a long listing + or not. In most cases, parsed_args.long is passed to this argument. + :return: A tuple of a list of table headers and a list of column names. + """ + + if long_listing: + headers = [hdr for col, hdr, listing_mode in attr_map + if listing_mode in (LIST_BOTH, LIST_LONG_ONLY)] + columns = [col for col, hdr, listing_mode in attr_map + if listing_mode in (LIST_BOTH, LIST_LONG_ONLY)] + else: + headers = [hdr for col, hdr, listing_mode in attr_map if listing_mode + if listing_mode in (LIST_BOTH, LIST_SHORT_ONLY)] + columns = [col for col, hdr, listing_mode in attr_map if listing_mode + if listing_mode in (LIST_BOTH, LIST_SHORT_ONLY)] + + return headers, columns + + +def get_columns(item, attr_map=None): + """Return pair of resource attributes and corresponding display names. + + Assume the following item and attr_map are passed. + item: {'id': 'myid', 'name': 'myname', + 'foo': 'bar', 'tenant_id': 'mytenan'} + attr_map: + (('id', 'ID', LIST_BOTH), + ('name', 'Name', LIST_BOTH), + ('tenant_id', 'Project', LIST_LONG_ONLY)) + + This method returns: + + (('id', 'name', 'tenant_id', 'foo'), # attributes + ('ID', 'Name', 'Project', 'foo') # display names + + Both tuples of attributes and display names are sorted by display names + in the alphabetical order. + Attributes not found in a given attr_map are kept as-is. + + :param item: a dictionary which represents a resource. + Keys of the dictionary are expected to be attributes of the resource. + Values are not referred to by this method. + :param attr_map: a list of mapping from attribute to display name. + The same format is used as for get_column_definitions attr_map. + :return: A pair of tuple of attributes and tuple of display names. + """ + attr_map = attr_map or tuple([]) + _attr_map_dict = dict((col, hdr) for col, hdr, listing_mode in attr_map) + + columns = [(column, _attr_map_dict.get(column, column)) + for column in item.keys()] + columns = sorted(columns, key=operator.itemgetter(1)) + return (tuple(col[0] for col in columns), + tuple(col[1] for col in columns)) + + +# TODO(amotoki): Use osc-lib version once osc-lib provides this. +def add_project_owner_option_to_parser(parser): + """Register project and project domain options. + + :param parser: argparse.Argument parser object. + + """ + parser.add_argument( + '--project', + metavar='', + help=_("Owner's project (name or ID)") + ) + # Borrowed from openstackclient.identity.common + # as it is not exposed officially. + parser.add_argument( + '--project-domain', + metavar='', + help=_('Domain the project belongs to (name or ID). ' + 'This can be used in case collisions between project names ' + 'exist.'), + ) + + +# The following methods are borrowed from openstackclient.identity.common +# as it is not exposed officially. +# TODO(amotoki): Use osc-lib version once osc-lib provides this. + + +def find_domain(identity_client, name_or_id): + return _find_identity_resource(identity_client.domains, name_or_id, + domains.Domain) + + +def find_project(identity_client, name_or_id, domain_name_or_id=None): + domain_id = _get_domain_id_if_requested(identity_client, domain_name_or_id) + if not domain_id: + return _find_identity_resource(identity_client.projects, name_or_id, + projects.Project) + else: + return _find_identity_resource(identity_client.projects, name_or_id, + projects.Project, domain_id=domain_id) + + +def _get_domain_id_if_requested(identity_client, domain_name_or_id): + if not domain_name_or_id: + return None + domain = find_domain(identity_client, domain_name_or_id) + return domain.id + + +def _find_identity_resource(identity_client_manager, name_or_id, + resource_type, **kwargs): + """Find a specific identity resource. + + Using keystoneclient's manager, attempt to find a specific resource by its + name or ID. If Forbidden to find the resource (a common case if the user + does not have permission), then return the resource by creating a local + instance of keystoneclient's Resource. + + The parameter identity_client_manager is a keystoneclient manager, + for example: keystoneclient.v3.users or keystoneclient.v3.projects. + + The parameter resource_type is a keystoneclient resource, for example: + keystoneclient.v3.users.User or keystoneclient.v3.projects.Project. + + :param identity_client_manager: the manager that contains the resource + :type identity_client_manager: `keystoneclient.base.CrudManager` + :param name_or_id: the resources's name or ID + :type name_or_id: string + :param resource_type: class that represents the resource type + :type resource_type: `keystoneclient.base.Resource` + + :returns: the resource in question + :rtype: `keystoneclient.base.Resource` + + """ + + try: + identity_resource = utils.find_resource(identity_client_manager, + name_or_id, **kwargs) + if identity_resource is not None: + return identity_resource + except identity_exc.Forbidden: + pass + + return resource_type(None, {'id': name_or_id, 'name': name_or_id}) + + +# The above are borrowed from openstackclient.identity.common. +# DO NOT ADD original methods in neutronclient repo to the above area. diff --git a/neutronclient/osc/v2/trunk/network_trunk.py b/neutronclient/osc/v2/trunk/network_trunk.py index 70f791fd8..acd6208db 100644 --- a/neutronclient/osc/v2/trunk/network_trunk.py +++ b/neutronclient/osc/v2/trunk/network_trunk.py @@ -20,12 +20,11 @@ import logging from osc_lib.cli import parseractions from osc_lib.command import command from osc_lib import exceptions -from osc_lib import utils - -# TODO(abhiraut): Switch to neutronclients identity utils -from openstackclient.identity import common as identity_common +from osc_lib import utils as osc_utils from neutronclient._i18n import _ +from neutronclient.osc import utils as nc_osc_utils +from neutronclient.osc.v2 import utils as v2_utils LOG = logging.getLogger(__name__) @@ -75,12 +74,7 @@ class CreateNetworkTrunk(command.ShowOne): action='store_true', help=_("Disable trunk") ) - parser.add_argument( - '--project', - metavar='', - help=_("Owner's project (name or ID)") - ) - identity_common.add_project_domain_option_to_parser(parser) + nc_osc_utils.add_project_owner_option_to_parser(parser) return parser def take_action(self, parsed_args): @@ -90,8 +84,8 @@ class CreateNetworkTrunk(command.ShowOne): body = {TRUNK: attrs} obj = client.create_trunk(body) columns = _get_columns(obj[TRUNK]) - data = utils.get_dict_properties(obj[TRUNK], columns, - formatters=_formatters) + data = osc_utils.get_dict_properties(obj[TRUNK], columns, + formatters=_formatters) return columns, data @@ -169,7 +163,7 @@ class ListNetworkTrunk(command.Lister): 'updated_at' ) return (headers, - (utils.get_dict_properties( + (osc_utils.get_dict_properties( s, columns, formatters=_formatters, ) for s in data[TRUNKS])) @@ -254,8 +248,8 @@ class ShowNetworkTrunk(command.ShowOne): trunk_id = _get_id(client, parsed_args.trunk, TRUNK) obj = client.show_trunk(trunk_id) columns = _get_columns(obj[TRUNK]) - data = utils.get_dict_properties(obj[TRUNK], columns, - formatters=_formatters) + data = osc_utils.get_dict_properties(obj[TRUNK], columns, + formatters=_formatters) return columns, data @@ -279,7 +273,7 @@ class ListNetworkSubport(command.Lister): headers = ('Port', 'Segmentation Type', 'Segmentation ID') columns = ('port_id', 'segmentation_type', 'segmentation_id') return (headers, - (utils.get_dict_properties( + (osc_utils.get_dict_properties( s, columns, ) for s in data[SUB_PORTS])) @@ -311,13 +305,9 @@ class UnsetNetworkTrunk(command.Command): client.trunk_remove_subports(trunk_id, attrs) -def _format_admin_state(item): - return 'UP' if item else 'DOWN' - - _formatters = { - 'admin_state_up': _format_admin_state, - 'sub_ports': utils.format_list_of_dicts, + 'admin_state_up': v2_utils.format_admin_state, + 'sub_ports': osc_utils.format_list_of_dicts, } @@ -346,7 +336,7 @@ def _get_attrs_for_trunk(client_manager, parsed_args): # "trunk set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: identity_client = client_manager.identity - project_id = identity_common.find_project( + project_id = nc_osc_utils.find_project( identity_client, parsed_args.project, parsed_args.project_domain, diff --git a/neutronclient/osc/v2/utils.py b/neutronclient/osc/v2/utils.py new file mode 100644 index 000000000..9e9040179 --- /dev/null +++ b/neutronclient/osc/v2/utils.py @@ -0,0 +1,21 @@ +# Copyright 2016 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""This module is intended to contain methods specific +to Networking v2 API and its extensions. +""" + + +def format_admin_state(state): + return 'UP' if state else 'DOWN' diff --git a/neutronclient/tests/unit/osc/test_utils.py b/neutronclient/tests/unit/osc/test_utils.py new file mode 100644 index 000000000..c9857beda --- /dev/null +++ b/neutronclient/tests/unit/osc/test_utils.py @@ -0,0 +1,60 @@ +# Copyright 2016 NEC Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import testtools + +from neutronclient.osc import utils + + +class TestUtils(testtools.TestCase): + + def test_get_column_definitions(self): + attr_map = ( + ('id', 'ID', utils.LIST_BOTH), + ('tenant_id', 'Project', utils.LIST_LONG_ONLY), + ('name', 'Name', utils.LIST_BOTH), + ('summary', 'Summary', utils.LIST_SHORT_ONLY), + ) + headers, columns = utils.get_column_definitions(attr_map, + long_listing=False) + self.assertEqual(['id', 'name', 'summary'], columns) + self.assertEqual(['ID', 'Name', 'Summary'], headers) + + def test_get_column_definitions_long(self): + attr_map = ( + ('id', 'ID', utils.LIST_BOTH), + ('tenant_id', 'Project', utils.LIST_LONG_ONLY), + ('name', 'Name', utils.LIST_BOTH), + ('summary', 'Summary', utils.LIST_SHORT_ONLY), + ) + headers, columns = utils.get_column_definitions(attr_map, + long_listing=True) + self.assertEqual(['id', 'tenant_id', 'name'], columns) + self.assertEqual(['ID', 'Project', 'Name'], headers) + + def test_get_columns(self): + item = { + 'id': 'test-id', + 'tenant_id': 'test-tenant_id', + # 'name' is not included + 'foo': 'bar', # unknown attribute + } + attr_map = ( + ('id', 'ID', utils.LIST_BOTH), + ('tenant_id', 'Project', utils.LIST_LONG_ONLY), + ('name', 'Name', utils.LIST_BOTH), + ) + columns, display_names = utils.get_columns(item, attr_map) + self.assertEqual(tuple(['id', 'tenant_id', 'foo']), columns) + self.assertEqual(tuple(['ID', 'Project', 'foo']), display_names) diff --git a/neutronclient/tests/unit/osc/v2/trunk/test_network_trunk.py b/neutronclient/tests/unit/osc/v2/trunk/test_network_trunk.py index c8ed19ac7..31c446ce1 100644 --- a/neutronclient/tests/unit/osc/v2/trunk/test_network_trunk.py +++ b/neutronclient/tests/unit/osc/v2/trunk/test_network_trunk.py @@ -22,6 +22,7 @@ from osc_lib.tests import utils as tests_utils from osc_lib import utils from neutronclient.osc.v2.trunk import network_trunk as trunk +from neutronclient.osc.v2 import utils as v2_utils from neutronclient.tests.unit.osc.v2 import fakes as test_fakes from neutronclient.tests.unit.osc.v2.trunk import fakes @@ -47,7 +48,7 @@ class TestCreateNetworkTrunk(test_fakes.TestNeutronClientOSCV2): def get_data(self): return ( - trunk._format_admin_state(self._trunk['admin_state_up']), + v2_utils.format_admin_state(self._trunk['admin_state_up']), self._trunk['description'], self._trunk['id'], self._trunk['name'], @@ -248,7 +249,7 @@ class TestShowNetworkTrunk(test_fakes.TestNeutronClientOSCV2): 'sub_ports', ) data = ( - trunk._format_admin_state(_trunk['admin_state_up']), + v2_utils.format_admin_state(_trunk['admin_state_up']), _trunk['description'], _trunk['id'], _trunk['name'], @@ -327,7 +328,7 @@ class TestListNetworkTrunk(test_fakes.TestNeutronClientOSCV2): t['port_id'], t['description'], t['status'], - trunk._format_admin_state(t['admin_state_up']), + v2_utils.format_admin_state(t['admin_state_up']), '2001-01-01 00:00:00', '2001-01-01 00:00:00', )) @@ -384,7 +385,7 @@ class TestSetNetworkTrunk(test_fakes.TestNeutronClientOSCV2): 'sub_ports', ) data = ( - trunk._format_admin_state(_trunk['admin_state_up']), + v2_utils.format_admin_state(_trunk['admin_state_up']), _trunk['id'], _trunk['name'], _trunk['description'], @@ -620,7 +621,7 @@ class TestUnsetNetworkTrunk(test_fakes.TestNeutronClientOSCV2): 'sub_ports', ) data = ( - trunk._format_admin_state(_trunk['admin_state_up']), + v2_utils.format_admin_state(_trunk['admin_state_up']), _trunk['id'], _trunk['name'], _trunk['port_id'], diff --git a/requirements.txt b/requirements.txt index d2a806c96..9fdcda393 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,9 @@ oslo.serialization>=1.10.0 # Apache-2.0 oslo.utils>=3.17.0 # Apache-2.0 os-client-config>=1.22.0 # Apache-2.0 keystoneauth1>=2.14.0 # Apache-2.0 +# keystoneclient is used only by neutronclient.osc.utils +# TODO(amotoki): Drop this after osc.utils has no dependency on keystoneclient +python-keystoneclient>=3.8.0 # Apache-2.0 requests>=2.10.0 # Apache-2.0 simplejson>=2.2.0 # MIT six>=1.9.0 # MIT