# Copyright (c) 2019, Intel Corporation. # All Rights Reserved. # # 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. # """Network segment action implementations""" import itertools import logging from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common LOG = logging.getLogger(__name__) def _get_columns(item): column_map = {} hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, column_map, hidden_columns ) def _get_ranges(item): item = sorted([int(i) for i in item]) for a, b in itertools.groupby(enumerate(item), lambda xy: xy[1] - xy[0]): b = list(b) yield "%s-%s" % (b[0][1], b[-1][1]) if b[0][1] != b[-1][1] else \ str(b[0][1]) def _hack_tuple_value_update_by_index(tup, index, value): lot = list(tup) lot[index] = value return tuple(lot) def _is_prop_empty(columns, props, prop_name): return True if not props[columns.index(prop_name)] else False def _exchange_dict_keys_with_values(orig_dict): updated_dict = dict() for k, v in orig_dict.items(): k = [k] if not updated_dict.get(v): updated_dict[v] = k else: updated_dict[v].extend(k) return updated_dict def _update_available_from_props(columns, props): index_available = columns.index('available') props = _hack_tuple_value_update_by_index( props, index_available, list(_get_ranges(props[index_available]))) return props def _update_used_from_props(columns, props): index_used = columns.index('used') updated_used = _exchange_dict_keys_with_values(props[index_used]) for k, v in updated_used.items(): updated_used[k] = list(_get_ranges(v)) props = _hack_tuple_value_update_by_index( props, index_used, updated_used) return props def _update_additional_fields_from_props(columns, props): props = _update_available_from_props(columns, props) props = _update_used_from_props(columns, props) return props class CreateNetworkSegmentRange(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create new network segment range") def get_parser(self, prog_name): parser = super(CreateNetworkSegmentRange, self).get_parser(prog_name) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( "--private", dest="private", action="store_true", help=_('Network segment range is assigned specifically to the ' 'project'), ) shared_group.add_argument( "--shared", dest="shared", action="store_true", help=_('Network segment range is shared with other projects'), ) parser.add_argument( 'name', metavar='<name>', help=_('Name of new network segment range') ) parser.add_argument( '--project', metavar='<project>', help=_('Network segment range owner (name or ID). Optional when ' 'the segment range is shared'), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--network-type', metavar='<network-type>', choices=['geneve', 'gre', 'vlan', 'vxlan'], required=True, help=_('Network type of this network segment range ' '(geneve, gre, vlan or vxlan)'), ) parser.add_argument( '--physical-network', metavar='<physical-network-name>', help=_('Physical network name of this network segment range'), ) parser.add_argument( '--minimum', metavar='<minimum-segmentation-id>', type=int, required=True, help=_('Minimum segment identifier for this network segment ' 'range which is based on the network type, VLAN ID for ' 'vlan network type and tunnel ID for geneve, gre and vxlan ' 'network types'), ) parser.add_argument( '--maximum', metavar='<maximum-segmentation-id>', type=int, required=True, help=_('Maximum segment identifier for this network segment ' 'range which is based on the network type, VLAN ID for ' 'vlan network type and tunnel ID for geneve, gre and vxlan ' 'network types'), ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network try: # Verify that the extension exists. network_client.find_extension('network-segment-range', ignore_missing=False) except Exception as e: msg = (_('Network segment range create not supported by ' 'Network API: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) identity_client = self.app.client_manager.identity if not parsed_args.private and parsed_args.project: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) if (parsed_args.network_type.lower() != 'vlan' and parsed_args.physical_network): msg = _("--physical-network is only allowed with --network-type " "vlan") raise exceptions.CommandError(msg) attrs = {} if parsed_args.shared or parsed_args.private: attrs['shared'] = parsed_args.shared else: # default to be ``shared`` if not specified attrs['shared'] = True attrs['network_type'] = parsed_args.network_type attrs['minimum'] = parsed_args.minimum attrs['maximum'] = parsed_args.maximum if parsed_args.name: attrs['name'] = parsed_args.name if parsed_args.project: project_id = identity_common.find_project( identity_client, parsed_args.project, parsed_args.project_domain, ).id if project_id: attrs['project_id'] = project_id else: msg = (_("Failed to create the network segment range for " "project %(project_id)s") % parsed_args.project_id) raise exceptions.CommandError(msg) elif not attrs['shared']: # default to the current project if no project specified and shared # is not specified. # Get the project id from the current auth. attrs['project_id'] = self.app.client_manager.auth_ref.project_id if parsed_args.physical_network: attrs['physical_network'] = parsed_args.physical_network attrs.update( self._parse_extra_properties(parsed_args.extra_properties)) obj = network_client.create_network_segment_range(**attrs) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data) class DeleteNetworkSegmentRange(command.Command): _description = _("Delete network segment range(s)") def get_parser(self, prog_name): parser = super(DeleteNetworkSegmentRange, self).get_parser(prog_name) parser.add_argument( 'network_segment_range', metavar='<network-segment-range>', nargs='+', help=_('Network segment range(s) to delete (name or ID)'), ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network try: # Verify that the extension exists. network_client.find_extension('network-segment-range', ignore_missing=False) except Exception as e: msg = (_('Network segment range delete not supported by ' 'Network API: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) result = 0 for network_segment_range in parsed_args.network_segment_range: try: obj = network_client.find_network_segment_range( network_segment_range, ignore_missing=False) network_client.delete_network_segment_range(obj) except Exception as e: result += 1 LOG.error(_("Failed to delete network segment range with " "ID '%(network_segment_range)s': %(e)s"), {'network_segment_range': network_segment_range, 'e': e}) if result > 0: total = len(parsed_args.network_segment_range) msg = (_("%(result)s of %(total)s network segment ranges failed " "to delete.") % {'result': result, 'total': total}) raise exceptions.CommandError(msg) class ListNetworkSegmentRange(command.Lister): _description = _("List network segment ranges") def get_parser(self, prog_name): parser = super(ListNetworkSegmentRange, self).get_parser(prog_name) parser.add_argument( '--long', action='store_true', help=_('List additional fields in output'), ) used_group = parser.add_mutually_exclusive_group() used_group.add_argument( '--used', action='store_true', help=_('List network segment ranges that have segments in use'), ) used_group.add_argument( '--unused', action='store_true', help=_('List network segment ranges that have segments ' 'not in use'), ) available_group = parser.add_mutually_exclusive_group() available_group.add_argument( '--available', action='store_true', help=_('List network segment ranges that have available segments'), ) available_group.add_argument( '--unavailable', action='store_true', help=_('List network segment ranges without available segments'), ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network try: # Verify that the extension exists. network_client.find_extension('network-segment-range', ignore_missing=False) except Exception as e: msg = (_('Network segment ranges list not supported by ' 'Network API: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) filters = {} data = network_client.network_segment_ranges(**filters) headers = ( 'ID', 'Name', 'Default', 'Shared', 'Project ID', 'Network Type', 'Physical Network', 'Minimum ID', 'Maximum ID' ) columns = ( 'id', 'name', 'default', 'shared', 'project_id', 'network_type', 'physical_network', 'minimum', 'maximum', ) if parsed_args.available or parsed_args.unavailable or \ parsed_args.used or parsed_args.unused: # If one of `--available`, `--unavailable`, `--used`, # `--unused` is specified, we assume that additional fields # should be listed in output. parsed_args.long = True if parsed_args.long: headers = headers + ( 'Used', 'Available', ) columns = columns + ( 'used', 'available', ) display_props = tuple() for s in data: props = utils.get_item_properties(s, columns) if parsed_args.available and \ _is_prop_empty(columns, props, 'available') or \ parsed_args.unavailable and \ not _is_prop_empty(columns, props, 'available') or \ parsed_args.used and _is_prop_empty(columns, props, 'used') or \ parsed_args.unused and \ not _is_prop_empty(columns, props, 'used'): continue if parsed_args.long: props = _update_additional_fields_from_props(columns, props) display_props += (props,) return headers, display_props class SetNetworkSegmentRange(common.NeutronCommandWithExtraArgs): _description = _("Set network segment range properties") def get_parser(self, prog_name): parser = super(SetNetworkSegmentRange, self).get_parser(prog_name) parser.add_argument( 'network_segment_range', metavar='<network-segment-range>', help=_('Network segment range to modify (name or ID)'), ) parser.add_argument( '--name', metavar='<name>', help=_('Set network segment name'), ) parser.add_argument( '--minimum', metavar='<minimum-segmentation-id>', type=int, help=_('Set network segment range minimum segment identifier'), ) parser.add_argument( '--maximum', metavar='<maximum-segmentation-id>', type=int, help=_('Set network segment range maximum segment identifier'), ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network try: # Verify that the extension exists. network_client.find_extension('network-segment-range', ignore_missing=False) except Exception as e: msg = (_('Network segment range set not supported by ' 'Network API: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) if (parsed_args.minimum and not parsed_args.maximum) or \ (parsed_args.maximum and not parsed_args.minimum): msg = _("--minimum and --maximum are both required") raise exceptions.CommandError(msg) obj = network_client.find_network_segment_range( parsed_args.network_segment_range, ignore_missing=False) attrs = {} if parsed_args.name: attrs['name'] = parsed_args.name if parsed_args.minimum: attrs['minimum'] = parsed_args.minimum if parsed_args.maximum: attrs['maximum'] = parsed_args.maximum attrs.update( self._parse_extra_properties(parsed_args.extra_properties)) network_client.update_network_segment_range(obj, **attrs) class ShowNetworkSegmentRange(command.ShowOne): _description = _("Display network segment range details") def get_parser(self, prog_name): parser = super(ShowNetworkSegmentRange, self).get_parser(prog_name) parser.add_argument( 'network_segment_range', metavar='<network-segment-range>', help=_('Network segment range to display (name or ID)'), ) return parser def take_action(self, parsed_args): network_client = self.app.client_manager.network try: # Verify that the extension exists. network_client.find_extension('network-segment-range', ignore_missing=False) except Exception as e: msg = (_('Network segment range show not supported by ' 'Network API: %(e)s') % {'e': e}) raise exceptions.CommandError(msg) obj = network_client.find_network_segment_range( parsed_args.network_segment_range, ignore_missing=False ) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) data = _update_additional_fields_from_props(columns, props=data) return (display_columns, data)