diff --git a/doc/source/command-objects/subnet.rst b/doc/source/command-objects/subnet.rst index e8740e534..16092bcd2 100644 --- a/doc/source/command-objects/subnet.rst +++ b/doc/source/command-objects/subnet.rst @@ -29,6 +29,7 @@ Create new subnet [--ipv6-ra-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] [--ipv6-address-mode {dhcpv6-stateful,dhcpv6-stateless,slaac}] [--network-segment ] + [--service-type ] --network @@ -116,6 +117,13 @@ Create new subnet to change. Use global option ``--os-beta-command`` to enable this command option. +.. option:: --service-type + + Service type for this subnet e.g.: + ``network:floatingip_agent_gateway``. + Must be a valid device owner value for a network port + (repeat option to set multiple service types) + .. option:: --network Network this subnet belongs to (name or ID) @@ -171,6 +179,13 @@ List subnets List subnets which have DHCP disabled +.. option:: --service-type + + List only subnets of a given service type in output + e.g.: ``network:floatingip_agent_gateway``. + Must be a valid device owner value for a network port + (repeat option to list multiple service types) + subnet set ---------- @@ -185,6 +200,7 @@ Set subnet properties [--dns-nameserver ] [--gateway ] [--host-route destination=,gateway=] + [--service-type ] [--name ] @@ -221,6 +237,13 @@ Set subnet properties gateway: nexthop IP address (repeat option to add multiple routes) +.. option:: --service-type + + Service type for this subnet e.g.: + ``network:floatingip_agent_gateway``. + Must be a valid device owner value for a network port + (repeat option to set multiple service types) + .. option:: --name Updated name of the subnet @@ -259,6 +282,7 @@ Unset subnet properties [--allocation-pool start=,end= [...]] [--dns-nameserver [...]] [--host-route destination=,gateway= [...]] + [--service-type ] .. option:: --dns-nameserver @@ -280,6 +304,13 @@ Unset subnet properties gateway: nexthop IP address (repeat option to unset multiple host routes) +.. option:: --service-type + + Service type to be removed from this subnet e.g.: + ``network:floatingip_agent_gateway``. + Must be a valid device owner value for a network port + (repeat option to unset multiple service types) + .. _subnet_unset-subnet: .. describe:: diff --git a/functional/tests/network/v2/test_subnet.py b/functional/tests/network/v2/test_subnet.py index 11ae6da19..7fb48437d 100644 --- a/functional/tests/network/v2/test_subnet.py +++ b/functional/tests/network/v2/test_subnet.py @@ -53,6 +53,13 @@ class SubnetTests(test.TestCase): raw_output = self.openstack('subnet show ' + self.NAME + opts) self.assertEqual("False\n" + self.NAME + "\n", raw_output) + def test_subnet_set_service_type(self): + TYPE = 'network:floatingip_agent_gateway' + self.openstack('subnet set --service-type ' + TYPE + ' ' + self.NAME) + opts = self.get_opts(['name', 'service_types']) + raw_output = self.openstack('subnet show ' + self.NAME + opts) + self.assertEqual(self.NAME + "\n" + TYPE + "\n", raw_output) + def test_subnet_show(self): opts = self.get_opts(self.FIELDS) raw_output = self.openstack('subnet show ' + self.NAME + opts) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 22452809c..6feb8aa00 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -48,6 +48,7 @@ _formatters = { 'allocation_pools': _format_allocation_pools, 'dns_nameservers': utils.format_list, 'host_routes': _format_host_routes, + 'service_types': utils.format_list, } @@ -82,6 +83,16 @@ def _get_common_parse_arguments(parser): "gateway: nexthop IP address " "(repeat option to add multiple routes)") ) + parser.add_argument( + '--service-type', + metavar='', + action='append', + dest='service_types', + help=_("Service type for this subnet " + "e.g.: network:floatingip_agent_gateway. " + "Must be a valid device owner value for a network port " + "(repeat option to set multiple service types)") + ) def _get_columns(item): @@ -177,6 +188,9 @@ def _get_attrs(client_manager, parsed_args, is_create=True): # Change 'gateway' entry to 'nexthop' to match the API attrs['host_routes'] = convert_entries_to_nexthop( parsed_args.host_routes) + if ('service_types' in parsed_args and + parsed_args.service_types is not None): + attrs['service_types'] = parsed_args.service_types return attrs @@ -352,6 +366,16 @@ class ListSubnet(command.Lister): action='store_true', help=_("List subnets which have DHCP disabled") ) + parser.add_argument( + '--service-type', + metavar='', + action='append', + dest='service_types', + help=_("List only subnets of a given service type in output " + "e.g.: network:floatingip_agent_gateway. " + "Must be a valid device owner value for a network port " + "(repeat option to list multiple service types)") + ) return parser def take_action(self, parsed_args): @@ -362,6 +386,8 @@ class ListSubnet(command.Lister): filters['enable_dhcp'] = True elif parsed_args.no_dhcp: filters['enable_dhcp'] = False + if parsed_args.service_types: + filters['service_types'] = parsed_args.service_types data = self.app.client_manager.network.subnets(**filters) headers = ('ID', 'Name', 'Network', 'Subnet') @@ -369,10 +395,10 @@ class ListSubnet(command.Lister): if parsed_args.long: headers += ('Project', 'DHCP', 'Name Servers', 'Allocation Pools', 'Host Routes', 'IP Version', - 'Gateway') + 'Gateway', 'Service Types') columns += ('tenant_id', 'enable_dhcp', 'dns_nameservers', 'allocation_pools', 'host_routes', 'ip_version', - 'gateway_ip') + 'gateway_ip', 'service_types') return (headers, (utils.get_item_properties( @@ -430,6 +456,8 @@ class SetSubnet(command.Command): attrs['host_routes'] += obj.host_routes if 'allocation_pools' in attrs: attrs['allocation_pools'] += obj.allocation_pools + if 'service_types' in attrs: + attrs['service_types'] += obj.service_types client.update_subnet(obj, **attrs) return @@ -489,6 +517,16 @@ class UnsetSubnet(command.Command): 'gateway: nexthop IP address ' '(repeat option to unset multiple host routes)') ) + parser.add_argument( + '--service-type', + metavar='', + action='append', + dest='service_types', + help=_('Service type to be removed from this subnet ' + 'e.g.: network:floatingip_agent_gateway. ' + 'Must be a valid device owner value for a network port ' + '(repeat option to unset multiple service types)') + ) parser.add_argument( 'subnet', metavar="", @@ -528,5 +566,13 @@ class UnsetSubnet(command.Command): str(error)) raise exceptions.CommandError(msg) attrs['allocation_pools'] = tmp_obj.allocation_pools + if parsed_args.service_types: + try: + _update_arguments(tmp_obj.service_types, + parsed_args.service_types) + except ValueError as error: + msg = (_("%s not in service-types") % str(error)) + raise exceptions.CommandError(msg) + attrs['service_types'] = tmp_obj.service_types if attrs: client.update_subnet(obj, **attrs) diff --git a/openstackclient/tests/network/v2/fakes.py b/openstackclient/tests/network/v2/fakes.py index a6de75e24..33bc40179 100644 --- a/openstackclient/tests/network/v2/fakes.py +++ b/openstackclient/tests/network/v2/fakes.py @@ -889,6 +889,7 @@ class FakeSubnet(object): 'ipv6_address_mode': None, 'ipv6_ra_mode': None, 'segment_id': None, + 'service_types': [], 'subnetpool_id': None, } diff --git a/openstackclient/tests/network/v2/test_subnet.py b/openstackclient/tests/network/v2/test_subnet.py index ba757c983..c117c6fdc 100644 --- a/openstackclient/tests/network/v2/test_subnet.py +++ b/openstackclient/tests/network/v2/test_subnet.py @@ -61,6 +61,8 @@ class TestCreateSubnet(TestSubnet): 'nexthop': '10.20.20.1'}, {'destination': '10.30.30.0/24', 'nexthop': '10.30.30.1'}], + 'service_types': ['network:router_gateway', + 'network:floatingip_agent_gateway'], } ) @@ -85,6 +87,8 @@ class TestCreateSubnet(TestSubnet): 'ipv6_address_mode': 'slaac', 'ipv6_ra_mode': 'slaac', 'subnetpool_id': 'None', + 'service_types': ['network:router_gateway', + 'network:floatingip_agent_gateway'], } ) @@ -118,6 +122,7 @@ class TestCreateSubnet(TestSubnet): 'network_id', 'project_id', 'segment_id', + 'service_types', 'subnetpool_id', ) @@ -136,6 +141,7 @@ class TestCreateSubnet(TestSubnet): _subnet.network_id, _subnet.project_id, _subnet.segment_id, + utils.format_list(_subnet.service_types), _subnet.subnetpool_id, ) @@ -154,6 +160,7 @@ class TestCreateSubnet(TestSubnet): _subnet_from_pool.network_id, _subnet_from_pool.project_id, _subnet_from_pool.segment_id, + utils.format_list(_subnet_from_pool.service_types), _subnet_from_pool.subnetpool_id, ) @@ -172,6 +179,7 @@ class TestCreateSubnet(TestSubnet): _subnet_ipv6.network_id, _subnet_ipv6.project_id, _subnet_ipv6.segment_id, + utils.format_list(_subnet_ipv6.service_types), _subnet_ipv6.subnetpool_id, ) @@ -259,6 +267,10 @@ class TestCreateSubnet(TestSubnet): ',destination=' + host_route.get('destination', '') arglist.append(value) + for service_type in self._subnet_from_pool.service_types: + arglist.append('--service-type') + arglist.append(service_type) + verifylist = [ ('name', self._subnet_from_pool.name), ('prefix_length', '24'), @@ -270,6 +282,7 @@ class TestCreateSubnet(TestSubnet): ('host_routes', subnet_v2.convert_entries_to_gateway( self._subnet_from_pool.host_routes)), ('subnet_pool', self._subnet_from_pool.subnetpool_id), + ('service_types', self._subnet_from_pool.service_types), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -285,6 +298,7 @@ class TestCreateSubnet(TestSubnet): 'network_id': self._subnet_from_pool.network_id, 'prefixlen': '24', 'subnetpool_id': self._subnet_from_pool.subnetpool_id, + 'service_types': self._subnet_from_pool.service_types, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data_subnet_pool, data) @@ -321,6 +335,10 @@ class TestCreateSubnet(TestSubnet): ',end=' + pool.get('end', '') arglist.append(value) + for service_type in self._subnet_ipv6.service_types: + arglist.append('--service-type') + arglist.append(service_type) + verifylist = [ ('name', self._subnet_ipv6.name), ('subnet_range', self._subnet_ipv6.cidr), @@ -334,6 +352,7 @@ class TestCreateSubnet(TestSubnet): ('host_routes', subnet_v2.convert_entries_to_gateway( self._subnet_ipv6.host_routes)), ('allocation_pools', self._subnet_ipv6.allocation_pools), + ('service_types', self._subnet_ipv6.service_types), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -351,6 +370,7 @@ class TestCreateSubnet(TestSubnet): 'name': self._subnet_ipv6.name, 'network_id': self._subnet_ipv6.network_id, 'allocation_pools': self._subnet_ipv6.allocation_pools, + 'service_types': self._subnet_ipv6.service_types, }) self.assertEqual(self.columns, columns) self.assertEqual(self.data_ipv6, data) @@ -505,6 +525,7 @@ class TestListSubnet(TestSubnet): 'Host Routes', 'IP Version', 'Gateway', + 'Service Types', ) data = [] @@ -530,6 +551,7 @@ class TestListSubnet(TestSubnet): utils.format_list(subnet.host_routes), subnet.ip_version, subnet.gateway_ip, + utils.format_list(subnet.service_types), )) def setUp(self): @@ -616,6 +638,41 @@ class TestListSubnet(TestSubnet): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_subnet_list_service_type(self): + arglist = [ + '--service-type', 'network:router_gateway', + ] + verifylist = [ + ('service_types', ['network:router_gateway']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'service_types': ['network:router_gateway']} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_subnet_list_service_type_multiple(self): + arglist = [ + '--service-type', 'network:router_gateway', + '--service-type', 'network:floatingip_agent_gateway', + ] + verifylist = [ + ('service_types', ['network:router_gateway', + 'network:floatingip_agent_gateway']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'service_types': ['network:router_gateway', + 'network:floatingip_agent_gateway']} + + self.network.subnets.assert_called_once_with(**filters) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetSubnet(TestSubnet): @@ -688,19 +745,24 @@ class TestSetSubnet(TestSubnet): def test_append_options(self): _testsubnet = network_fakes.FakeSubnet.create_one_subnet( - {'dns_nameservers': ["10.0.0.1"]}) + {'dns_nameservers': ["10.0.0.1"], + 'service_types': ["network:router_gateway"]}) self.network.find_subnet = mock.Mock(return_value=_testsubnet) arglist = [ '--dns-nameserver', '10.0.0.2', + '--service-type', 'network:floatingip_agent_gateway', _testsubnet.name, ] verifylist = [ ('dns_nameservers', ['10.0.0.2']), + ('service_types', ['network:floatingip_agent_gateway']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) attrs = { 'dns_nameservers': ['10.0.0.2', '10.0.0.1'], + 'service_types': ['network:floatingip_agent_gateway', + 'network:router_gateway'], } self.network.update_subnet.assert_called_once_with( _testsubnet, **attrs) @@ -726,6 +788,7 @@ class TestShowSubnet(TestSubnet): 'network_id', 'project_id', 'segment_id', + 'service_types', 'subnetpool_id', ) @@ -744,6 +807,7 @@ class TestShowSubnet(TestSubnet): _subnet.network_id, _subnet.tenant_id, _subnet.segment_id, + utils.format_list(_subnet.service_types), _subnet.subnetpool_id, ) @@ -796,7 +860,9 @@ class TestUnsetSubnet(TestSubnet): 'allocation_pools': [{'start': '8.8.8.100', 'end': '8.8.8.150'}, {'start': '8.8.8.160', - 'end': '8.8.8.170'}], }) + 'end': '8.8.8.170'}], + 'service_types': ['network:router_gateway', + 'network:floatingip_agent_gateway'], }) self.network.find_subnet = mock.Mock(return_value=self._testsubnet) self.network.update_subnet = mock.Mock(return_value=None) # Get the command object to test @@ -807,6 +873,7 @@ class TestUnsetSubnet(TestSubnet): '--dns-nameserver', '8.8.8.8', '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', + '--service-type', 'network:router_gateway', self._testsubnet.name, ] verifylist = [ @@ -815,6 +882,7 @@ class TestUnsetSubnet(TestSubnet): "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), ('allocation_pools', [{ 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ('service_types', ['network:router_gateway']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -825,6 +893,7 @@ class TestUnsetSubnet(TestSubnet): 'host_routes': [{ "destination": "10.20.20.0/24", "nexthop": "10.20.20.1"}], 'allocation_pools': [{'start': '8.8.8.160', 'end': '8.8.8.170'}], + 'service_types': ['network:floatingip_agent_gateway'], } self.network.update_subnet.assert_called_once_with( self._testsubnet, **attrs) @@ -886,3 +955,24 @@ class TestUnsetSubnet(TestSubnet): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) + + def test_unset_subnet_wrong_service_type(self): + arglist = [ + '--dns-nameserver', '8.8.8.8', + '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', + '--allocation-pool', 'start=8.8.8.100,end=8.8.8.150', + '--service-type', 'network:dhcp', + self._testsubnet.name, + ] + verifylist = [ + ('dns_nameservers', ['8.8.8.8']), + ('host_routes', [{ + "destination": "10.30.30.30/24", "gateway": "10.30.30.1"}]), + ('allocation_pools', [{ + 'start': '8.8.8.100', 'end': '8.8.8.150'}]), + ('service_types', ['network:dhcp']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(exceptions.CommandError, + self.cmd.take_action, parsed_args) diff --git a/releasenotes/notes/subnet-service-type-8d9c414732e474a4.yaml b/releasenotes/notes/subnet-service-type-8d9c414732e474a4.yaml new file mode 100644 index 000000000..743c72576 --- /dev/null +++ b/releasenotes/notes/subnet-service-type-8d9c414732e474a4.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add ``--service-type`` option to the ``subnet create``, + ``subnet set``, ``subnet unset``, and ``subnet list`` commands. + [ Blueprint `service-subnets `_]