diff --git a/doc/source/command-objects/subnet.rst b/doc/source/command-objects/subnet.rst index 97d5c68b8..56a28d737 100644 --- a/doc/source/command-objects/subnet.rst +++ b/doc/source/command-objects/subnet.rst @@ -20,6 +20,114 @@ Delete a subnet Subnet to delete (name or ID) +subnet create +-------------- + +Create new subnet + +.. program:: subnet create +.. code:: bash + + os subnet create + [--project [--project-domain ]] + [--subnet-pool | --use-default-subnet-pool [--prefix-length ]] + [--subnet-range ] + [--allocation-pool start=,end=] + [--dhcp | --no-dhcp] + [--dns-nameserver ] + [--gateway ] + [--host-route destination=,gateway=] + [--ip-version {4,6}] + [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] + [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] + --network + + +.. option:: --project + + Owner's project (name or ID) + +.. option:: --project-domain + + Domain the project belongs to (name or ID). + This can be used in case collisions between project names exist. + +.. option:: --subnet-pool + + Subnet pool from which this subnet will obtain a CIDR (name or ID) + +.. option:: --use-default-subnet-pool + + Use default subnet pool for --ip-version + +.. option:: --prefix-length + + Prefix length for subnet allocation from subnet pool + +.. option:: --subnet-range + + Subnet range in CIDR notation + (required if --subnet-pool is not specified, optional otherwise) + +.. option:: --allocation-pool start=,end= + + Allocation pool IP addresses for this subnet e.g.: + start=192.168.199.2,end=192.168.199.254 (This option can be repeated) + +.. option:: --dhcp + + Enable DHCP (default) + +.. option:: --no-dhcp + + Disable DHCP + +.. option:: --dns-nameserver + + DNS name server for this subnet (This option can be repeated) + +.. option:: --gateway + + Specify a gateway for the subnet. The three options are: + : Specific IP address to use as the gateway + 'auto': Gateway address should automatically be chosen from + within the subnet itself + 'none': This subnet will not use a gateway + e.g.: --gateway 192.168.9.1, --gateway auto, --gateway none + (default is 'auto') + +.. option:: --host-route destination=,gateway= + + Additional route for this subnet e.g.: + destination=10.10.0.0/16,gateway=192.168.71.254 + destination: destination subnet (in CIDR notation) + gateway: nexthop IP address + (This option can be repeated) + +.. option:: --ip-version {4,6} + + IP version (default is 4). Note that when subnet pool is specified, + IP version is determined from the subnet pool and this option + is ignored. + +.. option:: --ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} + + IPv6 RA (Router Advertisement) mode, + valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] + +.. option:: --ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac} + + IPv6 address mode, valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac] + +.. option:: --network + + Network this subnet belongs to (name or ID) + +.. _subnet_create-name: +.. describe:: + + Name of subnet to create + subnet list ----------- diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index b514a88f7..11646b4a7 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -12,9 +12,14 @@ # """Subnet action implementations""" +import copy + +from json.encoder import JSONEncoder from openstackclient.common import command +from openstackclient.common import parseractions from openstackclient.common import utils +from openstackclient.identity import common as identity_common def _format_allocation_pools(data): @@ -23,10 +28,17 @@ def _format_allocation_pools(data): return ','.join(pool_formatted) +def _format_host_routes(data): + try: + return '\n'.join([JSONEncoder().encode(route) for route in data]) + except (TypeError, KeyError): + return '' + + _formatters = { 'allocation_pools': _format_allocation_pools, 'dns_nameservers': utils.format_list, - 'host_routes': utils.format_list, + 'host_routes': _format_host_routes, } @@ -38,6 +50,214 @@ def _get_columns(item): return tuple(sorted(columns)) +def convert_entries_to_nexthop(entries): + # Change 'gateway' entry to 'nexthop' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + entry['nexthop'] = entry['gateway'] + del entry['gateway'] + + return changed_entries + + +def convert_entries_to_gateway(entries): + # Change 'nexhop' entry to 'gateway' + changed_entries = copy.deepcopy(entries) + for entry in changed_entries: + entry['gateway'] = entry['nexthop'] + del entry['nexthop'] + + return changed_entries + + +def _get_attrs(client_manager, parsed_args): + attrs = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = client_manager.identity + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['tenant_id'] = project_id + + client = client_manager.network + attrs['network_id'] = client.find_network(parsed_args.network, + ignore_missing=False).id + + if parsed_args.subnet_pool is not None: + subnet_pool = client.find_subnet_pool(parsed_args.subnet_pool, + ignore_missing=False) + attrs['subnetpool_id'] = subnet_pool.id + + if parsed_args.use_default_subnet_pool: + attrs['use_default_subnetpool'] = True + if parsed_args.gateway.lower() != 'auto': + if parsed_args.gateway.lower() == 'none': + attrs['gateway_ip'] = None + else: + attrs['gateway_ip'] = parsed_args.gateway + if parsed_args.prefix_length is not None: + attrs['prefixlen'] = parsed_args.prefix_length + if parsed_args.subnet_range is not None: + attrs['cidr'] = parsed_args.subnet_range + if parsed_args.ip_version is not None: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.ipv6_ra_mode is not None: + attrs['ipv6_ra_mode'] = parsed_args.ipv6_ra_mode + if parsed_args.ipv6_address_mode is not None: + attrs['ipv6_address_mode'] = parsed_args.ipv6_address_mode + if parsed_args.allocation_pools is not None: + attrs['allocation_pools'] = parsed_args.allocation_pools + if parsed_args.enable_dhcp is not None: + attrs['enable_dhcp'] = parsed_args.enable_dhcp + if parsed_args.dns_nameservers is not None: + attrs['dns_nameservers'] = parsed_args.dns_nameservers + if parsed_args.host_routes is not None: + # Change 'gateway' entry to 'nexthop' to match the API + attrs['host_routes'] = convert_entries_to_nexthop( + parsed_args.host_routes) + + return attrs + + +class CreateSubnet(command.ShowOne): + """Create a subnet""" + + def get_parser(self, prog_name): + parser = super(CreateSubnet, self).get_parser(prog_name) + parser.add_argument( + 'name', + help='New subnet name', + ) + parser.add_argument( + '--project', + metavar='', + help="Owner's project (name or ID)", + ) + identity_common.add_project_domain_option_to_parser(parser) + subnet_pool_group = parser.add_mutually_exclusive_group() + subnet_pool_group.add_argument( + '--subnet-pool', + metavar='', + help='Subnet pool from which this subnet will obtain a CIDR ' + '(Name or ID)', + ) + subnet_pool_group.add_argument( + '--use-default-subnet-pool', + action='store_true', + help='Use default subnet pool for --ip-version', + ) + parser.add_argument( + '--prefix-length', + metavar='', + help='Prefix length for subnet allocation from subnetpool', + ) + parser.add_argument( + '--subnet-range', + metavar='', + help='Subnet range in CIDR notation ' + '(required if --subnet-pool is not specified, ' + 'optional otherwise)', + ) + parser.add_argument( + '--allocation-pool', + metavar='start=,end=', + dest='allocation_pools', + action=parseractions.MultiKeyValueAction, + required_keys=['start', 'end'], + help='Allocation pool IP addresses for this subnet ' + 'e.g.: start=192.168.199.2,end=192.168.199.254 ' + '(This option can be repeated)', + ) + dhcp_enable_group = parser.add_mutually_exclusive_group() + dhcp_enable_group.add_argument( + '--dhcp', + dest='enable_dhcp', + action='store_true', + default=True, + help='Enable DHCP (default)', + ) + dhcp_enable_group.add_argument( + '--no-dhcp', + dest='enable_dhcp', + action='store_false', + help='Disable DHCP', + ) + parser.add_argument( + '--dns-nameserver', + metavar='', + action='append', + dest='dns_nameservers', + help='DNS name server for this subnet ' + '(This option can be repeated)', + ) + parser.add_argument( + '--gateway', + metavar='', + default='auto', + help="Specify a gateway for the subnet. The three options are: " + " : Specific IP address to use as the gateway " + " 'auto': Gateway address should automatically be " + " chosen from within the subnet itself " + " 'none': This subnet will not use a gateway " + "e.g.: --gateway 192.168.9.1, --gateway auto, --gateway none" + "(default is 'auto')", + ) + parser.add_argument( + '--host-route', + metavar='destination=,gateway=', + dest='host_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['destination', 'gateway'], + help='Additional route for this subnet ' + 'e.g.: destination=10.10.0.0/16,gateway=192.168.71.254 ' + 'destination: destination subnet (in CIDR notation) ' + 'gateway: nexthop IP address ' + '(This option can be repeated)', + ) + parser.add_argument( + '--ip-version', + type=int, + default=4, + choices=[4, 6], + help='IP version (default is 4). Note that when subnet pool is ' + 'specified, IP version is determined from the subnet pool ' + 'and this option is ignored.', + ) + parser.add_argument( + '--ipv6-ra-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help='IPv6 RA (Router Advertisement) mode, ' + 'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]', + ) + parser.add_argument( + '--ipv6-address-mode', + choices=['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac'], + help='IPv6 address mode, ' + 'valid modes: [dhcpv6-stateful, dhcpv6-stateless, slaac]', + ) + parser.add_argument( + '--network', + required=True, + metavar='', + help='Network this subnet belongs to (name or ID)', + ) + + return parser + + def take_action(self, parsed_args): + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) + obj = client.create_subnet(**attrs) + columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns, formatters=_formatters) + return (columns, data) + + class DeleteSubnet(command.Command): """Delete subnet""" @@ -46,7 +266,7 @@ class DeleteSubnet(command.Command): parser.add_argument( 'subnet', metavar="", - help="Subnet to delete (name or ID)" + help="Subnet to delete (name or ID)", ) return parser @@ -97,7 +317,7 @@ class ShowSubnet(command.ShowOne): parser.add_argument( 'subnet', metavar="", - help="Subnet to show (name or ID)" + help="Subnet to show (name or ID)", ) return parser diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index 26213b1f1..59886d204 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -573,7 +573,7 @@ class FakeSubnet(object): 'dns_nameservers': [], 'allocation_pools': [], 'host_routes': [], - 'ip_version': '4', + 'ip_version': 4, 'gateway_ip': '10.10.10.1', 'ipv6_address_mode': 'None', 'ipv6_ra_mode': 'None', diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py index a95635ffa..b718d262a 100644 --- a/openstackclient/tests/network/v2/test_subnet.py +++ b/openstackclient/tests/network/v2/test_subnet.py @@ -11,10 +11,13 @@ # under the License. # +import copy import mock from openstackclient.common import utils from openstackclient.network.v2 import subnet as subnet_v2 +from openstackclient.tests import fakes +from openstackclient.tests.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.network.v2 import fakes as network_fakes from openstackclient.tests import utils as tests_utils @@ -28,6 +31,333 @@ class TestSubnet(network_fakes.TestNetworkV2): self.network = self.app.client_manager.network +class TestCreateSubnet(TestSubnet): + + # An IPv4 subnet to be created with mostly default values + _subnet = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + } + ) + + # Subnet pool to be used to create a subnet from a pool + _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() + + # An IPv4 subnet to be created using a specific subnet pool + _subnet_from_pool = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + 'subnetpool_id': _subnet_pool.id, + 'dns_nameservers': ['8.8.8.8', + '8.8.4.4'], + 'host_routes': [{'destination': '10.20.20.0/24', + 'nexthop': '10.20.20.1'}, + {'destination': '10.30.30.0/24', + 'nexthop': '10.30.30.1'}], + } + ) + + # An IPv6 subnet to be created with most options specified + _subnet_ipv6 = network_fakes.FakeSubnet.create_one_subnet( + attrs={ + 'tenant_id': identity_fakes_v3.project_id, + 'cidr': 'fe80:0:0:a00a::/64', + 'enable_dhcp': True, + 'dns_nameservers': ['fe80:27ff:a00a:f00f::ffff', + 'fe80:37ff:a00a:f00f::ffff'], + 'allocation_pools': [{'start': 'fe80::a00a:0:c0de:0:100', + 'end': 'fe80::a00a:0:c0de:0:f000'}, + {'start': 'fe80::a00a:0:c0de:1:100', + 'end': 'fe80::a00a:0:c0de:1:f000'}], + 'host_routes': [{'destination': 'fe80:27ff:a00a:f00f::/64', + 'nexthop': 'fe80:27ff:a00a:f00f::1'}, + {'destination': 'fe80:37ff:a00a:f00f::/64', + 'nexthop': 'fe80:37ff:a00a:f00f::1'}], + 'ip_version': 6, + 'gateway_ip': 'fe80::a00a:0:c0de:0:1', + 'ipv6_address_mode': 'slaac', + 'ipv6_ra_mode': 'slaac', + 'subnetpool_id': 'None', + } + ) + + # The network to be returned from find_network + _network = network_fakes.FakeNetwork.create_one_network( + attrs={ + 'id': _subnet.network_id, + } + ) + + columns = ( + 'allocation_pools', + 'cidr', + 'dns_nameservers', + 'enable_dhcp', + 'gateway_ip', + 'host_routes', + 'id', + 'ip_version', + 'ipv6_address_mode', + 'ipv6_ra_mode', + 'name', + 'network_id', + 'project_id', + 'subnetpool_id', + ) + + data = ( + subnet_v2._format_allocation_pools(_subnet.allocation_pools), + _subnet.cidr, + utils.format_list(_subnet.dns_nameservers), + _subnet.enable_dhcp, + _subnet.gateway_ip, + subnet_v2._format_host_routes(_subnet.host_routes), + _subnet.id, + _subnet.ip_version, + _subnet.ipv6_address_mode, + _subnet.ipv6_ra_mode, + _subnet.name, + _subnet.network_id, + _subnet.project_id, + _subnet.subnetpool_id, + ) + + data_subnet_pool = ( + subnet_v2._format_allocation_pools(_subnet_from_pool.allocation_pools), + _subnet_from_pool.cidr, + utils.format_list(_subnet_from_pool.dns_nameservers), + _subnet_from_pool.enable_dhcp, + _subnet_from_pool.gateway_ip, + subnet_v2._format_host_routes(_subnet_from_pool.host_routes), + _subnet_from_pool.id, + _subnet_from_pool.ip_version, + _subnet_from_pool.ipv6_address_mode, + _subnet_from_pool.ipv6_ra_mode, + _subnet_from_pool.name, + _subnet_from_pool.network_id, + _subnet_from_pool.project_id, + _subnet_from_pool.subnetpool_id, + ) + + data_ipv6 = ( + subnet_v2._format_allocation_pools(_subnet_ipv6.allocation_pools), + _subnet_ipv6.cidr, + utils.format_list(_subnet_ipv6.dns_nameservers), + _subnet_ipv6.enable_dhcp, + _subnet_ipv6.gateway_ip, + subnet_v2._format_host_routes(_subnet_ipv6.host_routes), + _subnet_ipv6.id, + _subnet_ipv6.ip_version, + _subnet_ipv6.ipv6_address_mode, + _subnet_ipv6.ipv6_ra_mode, + _subnet_ipv6.name, + _subnet_ipv6.network_id, + _subnet_ipv6.project_id, + _subnet_ipv6.subnetpool_id, + ) + + def setUp(self): + super(TestCreateSubnet, self).setUp() + + # Get the command object to test + self.cmd = subnet_v2.CreateSubnet(self.app, self.namespace) + + # Set identity client v3. And get a shortcut to Identity client. + identity_client = identity_fakes_v3.FakeIdentityv3Client( + endpoint=fakes.AUTH_URL, + token=fakes.AUTH_TOKEN, + ) + self.app.client_manager.identity = identity_client + self.identity = self.app.client_manager.identity + + # Get a shortcut to the ProjectManager Mock + self.projects_mock = self.identity.projects + self.projects_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.PROJECT), + loaded=True, + ) + + # Get a shortcut to the DomainManager Mock + self.domains_mock = self.identity.domains + self.domains_mock.get.return_value = fakes.FakeResource( + None, + copy.deepcopy(identity_fakes_v3.DOMAIN), + loaded=True, + ) + + def test_create_no_options(self): + arglist = [] + verifylist = [] + + # Testing that a call without the required argument will fail and + # throw a "ParserExecption" + self.assertRaises(tests_utils.ParserException, + self.check_parser, self.cmd, arglist, verifylist) + + def test_create_default_options(self): + # Mock create_subnet and find_network sdk calls to return the + # values we want for this test + self.network.create_subnet = mock.Mock(return_value=self._subnet) + self._network.id = self._subnet.network_id + self.network.find_network = mock.Mock(return_value=self._network) + + arglist = [ + self._subnet.name, + "--subnet-range", self._subnet.cidr, + "--network", self._subnet.network_id, + ] + verifylist = [ + ('name', self._subnet.name), + ('subnet_range', self._subnet.cidr), + ('network', self._subnet.network_id), + ('ip_version', self._subnet.ip_version), + ('gateway', 'auto'), + + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_with(**{ + 'cidr': self._subnet.cidr, + 'enable_dhcp': self._subnet.enable_dhcp, + 'ip_version': self._subnet.ip_version, + 'name': self._subnet.name, + 'network_id': self._subnet.network_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_create_from_subnet_pool_options(self): + # Mock create_subnet, find_subnet_pool, and find_network sdk calls + # to return the values we want for this test + self.network.create_subnet = \ + mock.Mock(return_value=self._subnet_from_pool) + self._network.id = self._subnet_from_pool.network_id + self.network.find_network = mock.Mock(return_value=self._network) + self.network.find_subnet_pool = \ + mock.Mock(return_value=self._subnet_pool) + + arglist = [ + self._subnet_from_pool.name, + "--subnet-pool", self._subnet_from_pool.subnetpool_id, + "--prefix-length", '24', + "--network", self._subnet_from_pool.network_id, + "--ip-version", str(self._subnet_from_pool.ip_version), + "--gateway", self._subnet_from_pool.gateway_ip, + "--dhcp", + ] + + for dns_addr in self._subnet_from_pool.dns_nameservers: + arglist.append('--dns-nameserver') + arglist.append(dns_addr) + + for host_route in self._subnet_from_pool.host_routes: + arglist.append('--host-route') + value = 'gateway=' + host_route.get('nexthop', '') + \ + ',destination=' + host_route.get('destination', '') + arglist.append(value) + + verifylist = [ + ('name', self._subnet_from_pool.name), + ('prefix_length', '24'), + ('network', self._subnet_from_pool.network_id), + ('ip_version', self._subnet_from_pool.ip_version), + ('gateway', self._subnet_from_pool.gateway_ip), + ('dns_nameservers', self._subnet_from_pool.dns_nameservers), + ('enable_dhcp', self._subnet_from_pool.enable_dhcp), + ('host_routes', subnet_v2.convert_entries_to_gateway( + self._subnet_from_pool.host_routes)), + ('subnet_pool', self._subnet_from_pool.subnetpool_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_with(**{ + 'dns_nameservers': self._subnet_from_pool.dns_nameservers, + 'enable_dhcp': self._subnet_from_pool.enable_dhcp, + 'gateway_ip': self._subnet_from_pool.gateway_ip, + 'host_routes': self._subnet_from_pool.host_routes, + 'ip_version': self._subnet_from_pool.ip_version, + 'name': self._subnet_from_pool.name, + 'network_id': self._subnet_from_pool.network_id, + 'prefixlen': '24', + 'subnetpool_id': self._subnet_from_pool.subnetpool_id, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data_subnet_pool, data) + + def test_create_options_subnet_range_ipv6(self): + # Mock create_subnet and find_network sdk calls to return the + # values we want for this test + self.network.create_subnet = mock.Mock(return_value=self._subnet_ipv6) + self._network.id = self._subnet_ipv6.network_id + self.network.find_network = mock.Mock(return_value=self._network) + + arglist = [ + self._subnet_ipv6.name, + "--subnet-range", self._subnet_ipv6.cidr, + "--network", self._subnet_ipv6.network_id, + "--ip-version", str(self._subnet_ipv6.ip_version), + "--ipv6-ra-mode", self._subnet_ipv6.ipv6_ra_mode, + "--ipv6-address-mode", self._subnet_ipv6.ipv6_address_mode, + "--gateway", self._subnet_ipv6.gateway_ip, + "--dhcp", + ] + + for dns_addr in self._subnet_ipv6.dns_nameservers: + arglist.append('--dns-nameserver') + arglist.append(dns_addr) + + for host_route in self._subnet_ipv6.host_routes: + arglist.append('--host-route') + value = 'gateway=' + host_route.get('nexthop', '') + \ + ',destination=' + host_route.get('destination', '') + arglist.append(value) + + for pool in self._subnet_ipv6.allocation_pools: + arglist.append('--allocation-pool') + value = 'start=' + pool.get('start', '') + \ + ',end=' + pool.get('end', '') + arglist.append(value) + + verifylist = [ + ('name', self._subnet_ipv6.name), + ('subnet_range', self._subnet_ipv6.cidr), + ('network', self._subnet_ipv6.network_id), + ('ip_version', self._subnet_ipv6.ip_version), + ('ipv6_ra_mode', self._subnet_ipv6.ipv6_ra_mode), + ('ipv6_address_mode', self._subnet_ipv6.ipv6_address_mode), + ('gateway', self._subnet_ipv6.gateway_ip), + ('dns_nameservers', self._subnet_ipv6.dns_nameservers), + ('enable_dhcp', self._subnet_ipv6.enable_dhcp), + ('host_routes', subnet_v2.convert_entries_to_gateway( + self._subnet_ipv6.host_routes)), + ('allocation_pools', self._subnet_ipv6.allocation_pools), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network.create_subnet.assert_called_with(**{ + 'cidr': self._subnet_ipv6.cidr, + 'dns_nameservers': self._subnet_ipv6.dns_nameservers, + 'enable_dhcp': self._subnet_ipv6.enable_dhcp, + 'gateway_ip': self._subnet_ipv6.gateway_ip, + 'host_routes': self._subnet_ipv6.host_routes, + 'ip_version': self._subnet_ipv6.ip_version, + 'ipv6_address_mode': self._subnet_ipv6.ipv6_address_mode, + 'ipv6_ra_mode': self._subnet_ipv6.ipv6_ra_mode, + 'name': self._subnet_ipv6.name, + 'network_id': self._subnet_ipv6.network_id, + 'allocation_pools': self._subnet_ipv6.allocation_pools, + }) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data_ipv6, data) + + class TestDeleteSubnet(TestSubnet): # The subnet to delete. @@ -65,7 +395,7 @@ class TestListSubnet(TestSubnet): 'ID', 'Name', 'Network', - 'Subnet' + 'Subnet', ) columns_long = columns + ( 'Project', @@ -74,7 +404,7 @@ class TestListSubnet(TestSubnet): 'Allocation Pools', 'Host Routes', 'IP Version', - 'Gateway' + 'Gateway', ) data = [] @@ -99,7 +429,7 @@ class TestListSubnet(TestSubnet): subnet_v2._format_allocation_pools(subnet.allocation_pools), utils.format_list(subnet.host_routes), subnet.ip_version, - subnet.gateway_ip + subnet.gateway_ip, )) def setUp(self): diff --git a/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml b/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml new file mode 100644 index 000000000..0d61ba36c --- /dev/null +++ b/releasenotes/notes/bug-1542364-5d1e93cfd24f0b65.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``subnet create`` command. + [Bug `1542364 `_] diff --git a/setup.cfg b/setup.cfg index 5a1e32c85..1d2580a7b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -351,6 +351,7 @@ openstack.network.v2 = security_group_rule_delete = openstackclient.network.v2.security_group_rule:DeleteSecurityGroupRule security_group_rule_show = openstackclient.network.v2.security_group_rule:ShowSecurityGroupRule + subnet_create = openstackclient.network.v2.subnet:CreateSubnet subnet_delete = openstackclient.network.v2.subnet:DeleteSubnet subnet_list = openstackclient.network.v2.subnet:ListSubnet subnet_show = openstackclient.network.v2.subnet:ShowSubnet