diff --git a/.zuul.yaml b/.zuul.yaml index e3e52fd338..e1ffb0ebe0 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -113,6 +113,7 @@ # NOTE(amotoki): Some neutron features are enabled by devstack plugin neutron: https://git.openstack.org/openstack/neutron devstack_services: + neutron-network-segment-range: true neutron-segments: true q-metering: true q-qos: true diff --git a/doc/source/cli/command-objects/network_segment_range.rst b/doc/source/cli/command-objects/network_segment_range.rst new file mode 100644 index 0000000000..71155d2224 --- /dev/null +++ b/doc/source/cli/command-objects/network_segment_range.rst @@ -0,0 +1,168 @@ +===================== +network segment range +===================== + +A **network segment range** is a resource for tenant network segment +allocation. +A network segment range exposes the segment range management to be administered +via the Neutron API. In addition, it introduces the ability for the +administrator to control the segment ranges globally or on a per-tenant basis. + +Network v2 + +network segment range create +---------------------------- + +Create new network segment range + +.. program:: network segment range create +.. code:: bash + + openstack network segment range create + (--private | --shared) + [--project [--project-domain ]] + --network-type + [--physical-network ] + --minimum + --maximum + + +.. option:: --private + + Network segment range is assigned specifically to the project + +.. option:: --shared + + Network segment range is shared with other projects + +.. option:: --project + + Network segment range owner (name or ID). Optional when the segment + range is shared + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. option:: --physical-network + + Physical network name of this network segment range + +.. option:: --network-type + + Network type of this network segment range + (geneve, gre, vlan or vxlan) + +.. option:: --minimum + + 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 + +.. option:: --maximum + + 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 + +.. _network_segment_range_create-name: +.. describe:: + + Name of new network segment range + +network segment range delete +---------------------------- + +Delete network segment range(s) + +.. program:: network segment range delete +.. code:: bash + + openstack network segment range delete + [ ...] + +.. _network_segment_range_delete-network-segment-range: +.. describe:: + + Network segment range (s) to delete (name or ID) + +network segment range list +-------------------------- + +List network segment ranges + +.. program:: network segment range list +.. code:: bash + + openstack network segment range list + [--long] + [--used | --unused] + [--available | --unavailable] + +.. option:: --long + + List additional fields in output + +.. option:: --used + + List network segment ranges that have segments in use + +.. option:: --unused + + List network segment ranges that do not have segments not in use + +.. option:: --available + + List network segment ranges that have available segments + +.. option:: --unavailable + + List network segment ranges without available segments + +network segment range set +------------------------- + +Set network segment range properties + +.. program:: network segment range set +.. code:: bash + + openstack network segment range set + [--name ] + [--minimum ] + [--maximum ] + + +.. option:: --name + + Set network segment range name + +.. option:: --minimum + + Set network segment range minimum segment identifier + +.. option:: --maximum + + Set network segment range maximum segment identifier + +.. _network_segment_range_set-network-segment-range: +.. describe:: + + Network segment range to modify (name or ID) + +network segment range show +-------------------------- + +Display network segment range details + +.. program:: network segment range show +.. code:: bash + + openstack network segment range show + + +.. _network_segment_range_show-network-segment-range: +.. describe:: + + Network segment range to display (name or ID) diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index cdd5e63c95..e302fdad05 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -125,6 +125,7 @@ referring to both Compute and Volume quotas. * ``network qos policy``: (**Network**) - a QoS policy for network resources * ``network qos rule type``: (**Network**) - list of QoS available rule types * ``network segment``: (**Network**) - a segment of a virtual network +* ``network segment range``: (**Network**) - a segment range for tenant network segment allocation * ``network service provider``: (**Network**) - a driver providing a network service * ``object``: (**Object Storage**) a single file in the Object Storage * ``object store account``: (**Object Storage**) owns a group of Object Storage resources diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py new file mode 100644 index 0000000000..f5c8ccbc60 --- /dev/null +++ b/openstackclient/network/v2/network_segment_range.py @@ -0,0 +1,458 @@ +# 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 +import six + +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import sdk_utils + + +LOG = logging.getLogger(__name__) + + +def _get_columns(item): + return sdk_utils.get_osc_show_columns_for_sdk_resource(item, {}) + + +def _get_ranges(item): + item = [int(i) if isinstance(i, six.string_types) else 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 six.iteritems(orig_dict): + 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 six.iteritems(updated_used): + 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): + _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='', + help=_('Name of new network segment range') + ) + parser.add_argument( + '--project', + metavar='', + 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='', + 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='', + help=_('Physical network name of this network segment range'), + ) + parser.add_argument( + '--minimum', + metavar='', + 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='', + 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 parsed_args.shared 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 parsed_args.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 + else: + attrs['project_id'] = None + + if parsed_args.physical_network: + attrs['physical_network'] = parsed_args.physical_network + 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='', + 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(command.Command): + _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='', + help=_('Network segment range to modify (name or ID)'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Set network segment name'), + ) + parser.add_argument( + '--minimum', + metavar='', + type=int, + help=_('Set network segment range minimum segment identifier'), + ) + parser.add_argument( + '--maximum', + metavar='', + 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 + 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='', + 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) diff --git a/openstackclient/tests/functional/network/v2/test_network_segment_range.py b/openstackclient/tests/functional/network/v2/test_network_segment_range.py new file mode 100644 index 0000000000..95402dcb3f --- /dev/null +++ b/openstackclient/tests/functional/network/v2/test_network_segment_range.py @@ -0,0 +1,145 @@ +# 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. +# + +import json +import uuid + +from openstackclient.tests.functional.network.v2 import common + + +class NetworkSegmentRangeTests(common.NetworkTests): + """Functional tests for network segment range""" + + def setUp(self): + super(NetworkSegmentRangeTests, self).setUp() + # Nothing in this class works with Nova Network + if not self.haz_network: + self.skipTest("No Network service present") + self.PROJECT_NAME = uuid.uuid4().hex + + def test_network_segment_range_create_delete(self): + # Make a project + project_id = json.loads(self.openstack( + 'project create -f json ' + self.PROJECT_NAME))['id'] + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--private ' + + "--project " + self.PROJECT_NAME + " " + + '--network-type vxlan ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + self.assertEqual( + name, + json_output["name"], + ) + self.assertEqual( + project_id, + json_output["project_id"], + ) + + raw_output = self.openstack( + 'network segment range delete ' + name, + ) + self.assertOutput('', raw_output) + raw_output_project = self.openstack( + 'project delete ' + self.PROJECT_NAME) + self.assertEqual('', raw_output_project) + + def test_network_segment_range_list(self): + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--shared ' + + '--network-type geneve ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + network_segment_range_id = json_output.get('id') + network_segment_range_name = json_output.get('name') + self.addCleanup( + self.openstack, + 'network segment range delete ' + network_segment_range_id + ) + self.assertEqual( + name, + json_output["name"], + ) + + json_output = json.loads(self.openstack( + 'network segment list -f json' + )) + item_map = { + item.get('ID'): item.get('Name') for item in json_output + } + self.assertIn(network_segment_range_id, item_map.keys()) + self.assertIn(network_segment_range_name, item_map.values()) + + def test_network_segment_range_set_show(self): + project_id = json.loads(self.openstack( + 'project create -f json ' + self.PROJECT_NAME))['id'] + name = uuid.uuid4().hex + json_output = json.loads(self.openstack( + ' network segment range create -f json ' + + '--private ' + + "--project " + self.PROJECT_NAME + " " + + '--network-type geneve ' + + '--minimum 2018 ' + + '--maximum 2055 ' + + name + )) + self.addCleanup( + self.openstack, + 'network segment range delete ' + name + ) + self.assertEqual( + name, + json_output["name"], + ) + self.assertEqual( + project_id, + json_output["project_id"], + ) + + new_minimum = '2010' + new_maximum = '2060' + cmd_output = self.openstack( + 'network segment range set ' + + '--minimum ' + new_minimum + ' ' + + '--maximum ' + new_maximum + ' ' + + name + ) + self.assertOutput('', cmd_output) + + json_output = json.loads(self.openstack( + 'network segment range show -f json ' + + name + )) + self.assertEqual( + new_minimum, + json_output["minimum"], + ) + self.assertEqual( + new_maximum, + json_output["maximum"], + ) + + raw_output_project = self.openstack( + 'project delete ' + self.PROJECT_NAME) + self.assertEqual('', raw_output_project) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index ee0919fdd5..153781e869 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -538,6 +538,66 @@ class FakeNetworkSegment(object): return network_segments +class FakeNetworkSegmentRange(object): + """Fake one or more network segment ranges.""" + + @staticmethod + def create_one_network_segment_range(attrs=None): + """Create a fake network segment range. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object faking the network segment range + """ + attrs = attrs or {} + + # Set default attributes. + fake_uuid = uuid.uuid4().hex + network_segment_range_attrs = { + 'id': 'network-segment-range-id-' + fake_uuid, + 'name': 'network-segment-name-' + fake_uuid, + 'default': False, + 'shared': False, + 'project_id': 'project-id-' + fake_uuid, + 'network_type': 'vlan', + 'physical_network': 'physical-network-name-' + fake_uuid, + 'minimum': 100, + 'maximum': 106, + 'used': {104: '3312e4ba67864b2eb53f3f41432f8efc', + 106: '3312e4ba67864b2eb53f3f41432f8efc'}, + 'available': [100, 101, 102, 103, 105], + } + + # Overwrite default attributes. + network_segment_range_attrs.update(attrs) + + network_segment_range = fakes.FakeResource( + info=copy.deepcopy(network_segment_range_attrs), + loaded=True + ) + + return network_segment_range + + @staticmethod + def create_network_segment_ranges(attrs=None, count=2): + """Create multiple fake network segment ranges. + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of network segment ranges to fake + :return: + A list of FakeResource objects faking the network segment ranges + """ + network_segment_ranges = [] + for i in range(0, count): + network_segment_ranges.append( + FakeNetworkSegmentRange.create_one_network_segment_range(attrs) + ) + return network_segment_ranges + + class FakePort(object): """Fake one or more ports.""" diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py new file mode 100644 index 0000000000..6387a28194 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -0,0 +1,552 @@ +# 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. +# + +import mock +from mock import call + +from osc_lib import exceptions + +from openstackclient.network.v2 import network_segment_range +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestNetworkSegmentRange(network_fakes.TestNetworkV2): + + def setUp(self): + super(TestNetworkSegmentRange, self).setUp() + + # Get a shortcut to the network client + self.network = self.app.client_manager.network + + +class TestCreateNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to create. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + + columns = ( + 'available', + 'default', + 'id', + 'maximum', + 'minimum', + 'name', + 'network_type', + 'physical_network', + 'project_id', + 'shared', + 'used', + ) + + data = ( + ['100-103', '105'], + _network_segment_range.default, + _network_segment_range.id, + _network_segment_range.maximum, + _network_segment_range.minimum, + _network_segment_range.name, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.project_id, + _network_segment_range.shared, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ) + + def setUp(self): + super(TestCreateNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.create_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.CreateNetworkSegmentRange( + self.app, + self.namespace + ) + + def test_create_no_options(self): + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, [], []) + + def test_create_invalid_network_type(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', 'foo', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, arglist, []) + + def test_create_shared_with_project_id(self): + arglist = [ + '--shared', + '--project', self._network_segment_range.project_id, + '--network-type', 'vxlan', + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', True), + ('project', self._network_segment_range.project_id), + ('network_type', 'vxlan'), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_create_tunnel_with_physical_network(self): + arglist = [ + '--shared', + '--network-type', 'vxlan', + '--physical-network', self._network_segment_range.physical_network, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', True), + ('network_type', 'vxlan'), + ('physical_network', self._network_segment_range.physical_network), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, + parsed_args) + + def test_create_minimum_options(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', self._network_segment_range.network_type, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', self._network_segment_range.shared), + ('project', self._network_segment_range.project_id), + ('network_type', self._network_segment_range.network_type), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': self._network_segment_range.shared, + 'project_id': mock.ANY, + 'network_type': self._network_segment_range.network_type, + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_all_options(self): + arglist = [ + '--private', + '--project', self._network_segment_range.project_id, + '--network-type', self._network_segment_range.network_type, + '--physical-network', self._network_segment_range.physical_network, + '--minimum', str(self._network_segment_range.minimum), + '--maximum', str(self._network_segment_range.maximum), + self._network_segment_range.name, + ] + verifylist = [ + ('shared', self._network_segment_range.shared), + ('project', self._network_segment_range.project_id), + ('network_type', self._network_segment_range.network_type), + ('physical_network', self._network_segment_range.physical_network), + ('minimum', self._network_segment_range.minimum), + ('maximum', self._network_segment_range.maximum), + ('name', self._network_segment_range.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_network_segment_range.assert_called_once_with(**{ + 'shared': self._network_segment_range.shared, + 'project_id': mock.ANY, + 'network_type': self._network_segment_range.network_type, + 'physical_network': self._network_segment_range.physical_network, + 'minimum': self._network_segment_range.minimum, + 'maximum': self._network_segment_range.maximum, + 'name': self._network_segment_range.name, + }) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestDeleteNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment ranges to delete. + _network_segment_ranges = \ + network_fakes.FakeNetworkSegmentRange.create_network_segment_ranges() + + def setUp(self): + super(TestDeleteNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.delete_network_segment_range = mock.Mock( + return_value=None) + self.network.find_network_segment_range = mock.Mock( + side_effect=self._network_segment_ranges + ) + + # Get the command object to test + self.cmd = network_segment_range.DeleteNetworkSegmentRange( + self.app, + self.namespace + ) + + def test_delete(self): + arglist = [ + self._network_segment_ranges[0].id, + ] + verifylist = [ + ('network_segment_range', [self._network_segment_ranges[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network.delete_network_segment_range.assert_called_once_with( + self._network_segment_ranges[0] + ) + self.assertIsNone(result) + + def test_delete_multiple(self): + arglist = [] + for _network_segment_range in self._network_segment_ranges: + arglist.append(_network_segment_range.id) + verifylist = [ + ('network_segment_range', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [] + for _network_segment_range in self._network_segment_ranges: + calls.append(call(_network_segment_range)) + self.network.delete_network_segment_range.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_with_exception(self): + arglist = [ + self._network_segment_ranges[0].id, + 'doesnotexist' + ] + verifylist = [ + ('network_segment_range', + [self._network_segment_ranges[0].id, 'doesnotexist']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [self._network_segment_ranges[0], + exceptions.CommandError] + self.network.find_network_segment_range = ( + mock.Mock(side_effect=find_mock_result) + ) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 network segment ranges failed to delete.', + str(e)) + + self.network.find_network_segment_range.assert_any_call( + self._network_segment_ranges[0].id, ignore_missing=False) + self.network.find_network_segment_range.assert_any_call( + 'doesnotexist', ignore_missing=False) + self.network.delete_network_segment_range.assert_called_once_with( + self._network_segment_ranges[0] + ) + + +class TestListNetworkSegmentRange(TestNetworkSegmentRange): + _network_segment_ranges = network_fakes.FakeNetworkSegmentRange.\ + create_network_segment_ranges(count=3) + + columns = ( + 'ID', + 'Name', + 'Default', + 'Shared', + 'Project ID', + 'Network Type', + 'Physical Network', + 'Minimum ID', + 'Maximum ID' + ) + columns_long = columns + ( + 'Used', + 'Available', + ) + + data = [] + for _network_segment_range in _network_segment_ranges: + data.append(( + _network_segment_range.id, + _network_segment_range.name, + _network_segment_range.default, + _network_segment_range.shared, + _network_segment_range.project_id, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.minimum, + _network_segment_range.maximum, + )) + + data_long = [] + for _network_segment_range in _network_segment_ranges: + data_long.append(( + _network_segment_range.id, + _network_segment_range.name, + _network_segment_range.default, + _network_segment_range.shared, + _network_segment_range.project_id, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.minimum, + _network_segment_range.maximum, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ['100-103', '105'], + )) + + def setUp(self): + super(TestListNetworkSegmentRange, self).setUp() + + # Get the command object to test + self.cmd = network_segment_range.ListNetworkSegmentRange( + self.app, self.namespace) + + self.network.find_extension = mock.Mock() + self.network.network_segment_ranges = mock.Mock( + return_value=self._network_segment_ranges) + + def test_list_no_option(self): + arglist = [] + verifylist = [ + ('long', False), + ('available', False), + ('unavailable', False), + ('used', False), + ('unused', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.network_segment_ranges.assert_called_once_with() + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_list_long(self): + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ('available', False), + ('unavailable', False), + ('used', False), + ('unused', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network.network_segment_ranges.assert_called_once_with() + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + +class TestSetNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to set. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + # The network segment range updated. + minimum_updated = _network_segment_range.minimum - 5 + maximum_updated = _network_segment_range.maximum + 5 + available_updated = (list(range(minimum_updated, 104)) + [105] + + list(range(107, maximum_updated + 1))) + _network_segment_range_updated = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range( + attrs={'minimum': minimum_updated, + 'maximum': maximum_updated, + 'used': {104: '3312e4ba67864b2eb53f3f41432f8efc', + 106: '3312e4ba67864b2eb53f3f41432f8efc'}, + 'available': available_updated} + ) + + def setUp(self): + super(TestSetNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.find_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.SetNetworkSegmentRange(self.app, + self.namespace) + + def test_set_no_options(self): + arglist = [ + self._network_segment_range.id, + ] + verifylist = [ + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.network.update_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + result = self.cmd.take_action(parsed_args) + + self.network.update_network_segment_range.assert_called_once_with( + self._network_segment_range, **{} + ) + self.assertIsNone(result) + + def test_set_all_options(self): + arglist = [ + '--name', 'new name', + '--minimum', str(self.minimum_updated), + '--maximum', str(self.maximum_updated), + self._network_segment_range.id, + ] + verifylist = [ + ('name', 'new name'), + ('minimum', self.minimum_updated), + ('maximum', self.maximum_updated), + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.network.update_network_segment_range = mock.Mock( + return_value=self._network_segment_range_updated + ) + result = self.cmd.take_action(parsed_args) + + attrs = { + 'name': 'new name', + 'minimum': self.minimum_updated, + 'maximum': self.maximum_updated, + } + self.network.update_network_segment_range.assert_called_once_with( + self._network_segment_range, **attrs + ) + self.assertIsNone(result) + + +class TestShowNetworkSegmentRange(TestNetworkSegmentRange): + + # The network segment range to show. + _network_segment_range = network_fakes.FakeNetworkSegmentRange.\ + create_one_network_segment_range() + + columns = ( + 'available', + 'default', + 'id', + 'maximum', + 'minimum', + 'name', + 'network_type', + 'physical_network', + 'project_id', + 'shared', + 'used', + ) + + data = ( + ['100-103', '105'], + _network_segment_range.default, + _network_segment_range.id, + _network_segment_range.maximum, + _network_segment_range.minimum, + _network_segment_range.name, + _network_segment_range.network_type, + _network_segment_range.physical_network, + _network_segment_range.project_id, + _network_segment_range.shared, + {'3312e4ba67864b2eb53f3f41432f8efc': ['104', '106']}, + ) + + def setUp(self): + super(TestShowNetworkSegmentRange, self).setUp() + + self.network.find_extension = mock.Mock() + self.network.find_network_segment_range = mock.Mock( + return_value=self._network_segment_range + ) + + # Get the command object to test + self.cmd = network_segment_range.ShowNetworkSegmentRange( + self.app, self.namespace) + + def test_show_no_options(self): + # Missing required args should bail here + self.assertRaises(tests_utils.ParserException, self.check_parser, + self.cmd, [], []) + + def test_show_all_options(self): + arglist = [ + self._network_segment_range.id, + ] + verifylist = [ + ('network_segment_range', self._network_segment_range.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.find_network_segment_range.assert_called_once_with( + self._network_segment_range.id, + ignore_missing=False + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml b/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml new file mode 100644 index 0000000000..4ff4f57536 --- /dev/null +++ b/releasenotes/notes/bp-network-segment-range-management-0abf03fe03eea149.yaml @@ -0,0 +1,6 @@ +--- +features: + - Add ``network segment range create``, ``network segment range delete``, + ``network segment range list``, ``network segment range show`` and + ``network segment range set`` commands. + [Blueprint `network-segment-range-management `_] diff --git a/setup.cfg b/setup.cfg index 48c2247ff7..c73f2ce9f6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -463,6 +463,12 @@ openstack.network.v2 = network_segment_set = openstackclient.network.v2.network_segment:SetNetworkSegment network_segment_show = openstackclient.network.v2.network_segment:ShowNetworkSegment + network_segment_range_create = openstackclient.network.v2.network_segment_range:CreateNetworkSegmentRange + network_segment_range_delete = openstackclient.network.v2.network_segment_range:DeleteNetworkSegmentRange + network_segment_range_list = openstackclient.network.v2.network_segment_range:ListNetworkSegmentRange + network_segment_range_set = openstackclient.network.v2.network_segment_range:SetNetworkSegmentRange + network_segment_range_show = openstackclient.network.v2.network_segment_range:ShowNetworkSegmentRange + network_service_provider_list = openstackclient.network.v2.network_service_provider:ListNetworkServiceProvider port_create = openstackclient.network.v2.port:CreatePort