diff --git a/doc/source/devref/index.rst b/doc/source/devref/index.rst index a8a08312c7e..92cbc065988 100644 --- a/doc/source/devref/index.rst +++ b/doc/source/devref/index.rst @@ -76,6 +76,7 @@ Neutron Internals instrumentation address_scopes openvswitch_firewall + network_ip_availability Testing ------- diff --git a/doc/source/devref/network_ip_availability.rst b/doc/source/devref/network_ip_availability.rst new file mode 100644 index 00000000000..b951c243ad7 --- /dev/null +++ b/doc/source/devref/network_ip_availability.rst @@ -0,0 +1,180 @@ +.. + 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 IP Availability Extension +================================= + +This extension is an information-only API that allows a user or process +to determine the amount of IPs that are consumed across networks and +their subnets' allocation pools. Each network and embedded subnet returns +with values for **used_ips** and **total_ips** making it easy +to determine how much of your network's IP space is consumed. + +This API provides the ability for network administrators to periodically +list usage (manual or automated) in order to preemptively add new network +capacity when thresholds are exceeded. + +**Important Note:** + +This API tracks a network's "consumable" IPs. What's the distinction? +After a network and its subnets are created, consumable IPs +are: + +* Consumed in the subnet's allocations (derives used IPs) +* Consumed from the subnet's allocation pools (derives total IPs) + +This API tracks consumable IPs so network administrators know when their +subnet's IP pools (and and ultimately a network's) IPs are about to run out. +This API does not account reserved IPs such as a subnet's gateway IP or other +reserved or unused IPs of a subnet's cidr that are consumed as a result of +the subnet creation itself. + +Enabling in Neutron +------------------- + +To enable this plugin within neutron, append this pluging class to the +comma-delimited plugin list to the end of the **service_plugins** configuration +property within your neutron.conf file. + +Example:: + + service_plugins=router, network_ip_availability + + +API Specification +----------------- + +Availability for all networks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +GET /v2.0/network-ip-availabilities :: + + Request to url: v2.0/network-ip-availabilities + headers: {'content-type': 'application/json', 'X-Auth-Token': 'SOME_AUTH_TOKEN'} + +Example response :: + + Response: + HTTP/1.1 200 OK + Content-Type: application/json; charset=UTF-8 + +.. code:: + + { + "network_ip_availabilities": [ + { + "network_id": "f944c153-3f46-417b-a3c2-487cd9a456b9", + "network_name": "net1", + "subnet_ip_availability": [ + { + "cidr": "10.0.0.0/24", + "ip_version": 4, + "subnet_id": "46b1406a-8373-454c-8eb8-500a09eb77fb", + "subnet_name": "", + "total_ips": 253, + "used_ips": 3 + } + ], + "tenant_id": "test-tenant", + "total_ips": 253, + "used_ips": 3 + }, + { + "network_id": "47035bae-4f29-4fef-be2e-2941b72528a8", + "network_name": "net2", + "subnet_ip_availability": [], + "tenant_id": "test-tenant", + "total_ips": 0, + "used_ips": 0 + }, + { + "network_id": "2e3ea0cd-c757-44bf-bb30-42d038687e3f", + "network_name": "net3", + "subnet_ip_availability": [ + { + "cidr": "40.0.0.0/24", + "ip_version": 4, + "subnet_id": "aab6b35c-16b5-489c-a5c7-fec778273495", + "subnet_name": "", + "total_ips": 253, + "used_ips": 2 + } + ], + "tenant_id": "test-tenant", + "total_ips": 253, + "used_ips": 2 + } + ] + } + +Availability by network ID +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +GET /v2.0/network-ip-availabilities/{network\_uuid} :: + + Request to url: /v2.0/network-ip-availabilities/aba3b29b-c119-4b45-afbd-88e500acd970 + headers: {'content-type': 'application/json', 'X-Auth-Token': 'SOME_AUTH_TOKEN'} + +Example response :: + + Response: + HTTP/1.1 200 OK + Content-Type: application/json; charset=UTF-8 + +.. code:: + + { + "network_ip_availability": { + "network_id": "f944c153-3f46-417b-a3c2-487cd9a456b9", + "network_name": "net1", + "subnet_ip_availability": [ + { + "cidr": "10.0.0.0/24", + "ip_version": 4, + "subnet_name": "", + "subnet_id": "46b1406a-8373-454c-8eb8-500a09eb77fb", + "total_ips": 253, + "used_ips": 3 + } + ], + "tenant_id": "test-tenant", + "total_ips": 253, + "used_ips": 3 + } + } + +Supported Query Filters +~~~~~~~~~~~~~~~~~~~~~~~ +This API currently supports the following query parameters: + +* **network_id**: Returns availability for the network matching the network ID. + Note: This query (?network_id={network_id_guid})is roughly equivalent to + *Availability by network ID* section except it returns the plural + response form as a list rather than as an item. +* **network_name**: Returns availability for network matching + the provided name +* **tenant_id**: Returns availability for all networks owned by the provided + tenant ID. +* **ip_version**: Filters network subnets by those supporting the supplied + ip version. Values can be either 4 or 6. + +Query filters can be combined to further narrow results and what is returned +will match all criteria. When a parameter is specified more +than once, it will return results that match both. Examples: :: + + # Fetch IPv4 availability for a specific tenant uuid + GET /v2.0/network-ip-availabilities?ip_version=4&tenant_id=example-tenant-uuid + + # Fetch multiple networks by their ids + GET /v2.0/network-ip-availabilities?network_id=uuid_sample_1&network_id=uuid_sample_2 diff --git a/etc/policy.json b/etc/policy.json index 3817e5999ef..44963d61187 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -43,6 +43,8 @@ "get_network:provider:physical_network": "rule:admin_only", "get_network:provider:segmentation_id": "rule:admin_only", "get_network:queue_id": "rule:admin_only", + "get_network_ip_availabilities": "rule:admin_only", + "get_network_ip_availability": "rule:admin_only", "create_network:shared": "rule:admin_only", "create_network:router:external": "rule:admin_only", "create_network:is_default": "rule:admin_only", diff --git a/neutron/db/network_ip_availability_db.py b/neutron/db/network_ip_availability_db.py new file mode 100644 index 00000000000..f94538a8c04 --- /dev/null +++ b/neutron/db/network_ip_availability_db.py @@ -0,0 +1,182 @@ +# Copyright 2016 GoDaddy. +# +# 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 netaddr +import six +from sqlalchemy import func + +import neutron.db.models_v2 as mod +import oslo_log.log as logging + +LOG = logging.getLogger(__name__) + +NETWORK_ID = 'network_id' +NETWORK_NAME = 'network_name' +SUBNET_ID = 'subnet_id' +SUBNET_NAME = 'subnet_name' + +SUPPORTED_FILTERS = { + NETWORK_ID: mod.Network.id, + NETWORK_NAME: mod.Network.name, + 'tenant_id': mod.Network.tenant_id, + 'ip_version': mod.Subnet.ip_version, +} +SUPPORTED_FILTER_KEYS = six.viewkeys(SUPPORTED_FILTERS) + + +class IpAvailabilityMixin(object): + """Mixin class to query for IP availability.""" + + # Columns common to all queries + common_columns = [ + mod.Network.id.label(NETWORK_ID), + mod.Subnet.id.label(SUBNET_ID), + mod.Subnet.cidr, + mod.Subnet.ip_version + ] + + # Columns for the network/subnet and used_ip counts + network_used_ips_columns = list(common_columns) + network_used_ips_columns.append(mod.Network.name.label(NETWORK_NAME)) + network_used_ips_columns.append(mod.Network.tenant_id) + network_used_ips_columns.append(mod.Subnet.name.label(SUBNET_NAME)) + # Aggregate query computed column + network_used_ips_computed_columns = [ + func.count(mod.IPAllocation.subnet_id).label('used_ips')] + + # Columns for total_ips query + total_ips_columns = list(common_columns) + total_ips_columns.append(mod.IPAllocationPool.first_ip) + total_ips_columns.append(mod.IPAllocationPool.last_ip) + + @classmethod + def get_network_ip_availabilities(cls, context, filters=None): + """Get IP availability stats on a per subnet basis. + + Returns a list of network summaries which internally contains a list + of subnet summaries. The used_ip and total_ip counts are returned at + both levels. + """ + + # Fetch total_ips by subnet + subnet_total_ips_dict = cls._generate_subnet_total_ips_dict(context, + filters) + # Query network/subnet data along with used IP counts + record_and_count_query = cls._build_network_used_ip_query(context, + filters) + # Assemble results + result_dict = {} + for row in record_and_count_query: + cls._add_result(row, result_dict, + subnet_total_ips_dict.get(row.subnet_id, 0)) + + # Convert result back into the list it expects + net_ip_availabilities = list(six.viewvalues(result_dict)) + return net_ip_availabilities + + @classmethod + def _build_network_used_ip_query(cls, context, filters): + # Generate a query to gather network/subnet/used_ips. + # Ensure query is tolerant of missing child table data (outerjoins) + # Process these outerjoin columns assuming their values may be None + query = context.session.query() + query = query.add_columns(*cls.network_used_ips_columns) + query = query.add_columns(*cls.network_used_ips_computed_columns) + query = query.outerjoin(mod.Subnet, + mod.Network.id == mod.Subnet.network_id) + query = query.outerjoin(mod.IPAllocation, + mod.Subnet.id == mod.IPAllocation.subnet_id) + query = query.group_by(*cls.network_used_ips_columns) + + return cls._adjust_query_for_filters(query, filters) + + @classmethod + def _build_total_ips_query(cls, context, filters): + query = context.session.query() + query = query.add_columns(*cls.total_ips_columns) + query = query.outerjoin(mod.Subnet, + mod.Network.id == mod.Subnet.network_id) + query = query.outerjoin( + mod.IPAllocationPool, + mod.Subnet.id == mod.IPAllocationPool.subnet_id) + return cls._adjust_query_for_filters(query, filters) + + @classmethod + def _generate_subnet_total_ips_dict(cls, context, filters): + """Generates a dict whose key=subnet_id, value=total_ips in subnet""" + + # Query to get total_ips counts + total_ips_query = cls._build_total_ips_query(context, filters) + + subnet_totals_dict = {} + for row in total_ips_query: + # Skip networks without subnets + if not row.subnet_id: + continue + + # Add IPAllocationPool data + if row.last_ip: + pool_total = netaddr.IPRange( + netaddr.IPAddress(row.first_ip), + netaddr.IPAddress(row.last_ip)).size + cur_total = subnet_totals_dict.get(row.subnet_id, 0) + subnet_totals_dict[row.subnet_id] = cur_total + pool_total + else: + subnet_totals_dict[row.subnet_id] = netaddr.IPNetwork( + row.cidr, version=row.ip_version).size + + return subnet_totals_dict + + @classmethod + def _adjust_query_for_filters(cls, query, filters): + # The intersect of sets gets us applicable filter keys (others ignored) + common_keys = six.viewkeys(filters) & SUPPORTED_FILTER_KEYS + for key in common_keys: + filter_vals = filters[key] + if filter_vals: + query = query.filter(SUPPORTED_FILTERS[key].in_(filter_vals)) + return query + + @classmethod + def _add_result(cls, db_row, result_dict, subnet_total_ips): + # Find network in results. Create and add if missing + if db_row.network_id in result_dict: + network = result_dict[db_row.network_id] + else: + network = {NETWORK_ID: db_row.network_id, + NETWORK_NAME: db_row.network_name, + 'tenant_id': db_row.tenant_id, + 'subnet_ip_availability': [], + 'used_ips': 0, 'total_ips': 0} + result_dict[db_row.network_id] = network + + # Only add subnet data if outerjoin rows have it + if db_row.subnet_id: + cls._add_subnet_data_to_net(db_row, network, subnet_total_ips) + + @classmethod + def _add_subnet_data_to_net(cls, db_row, network_dict, subnet_total_ips): + subnet = { + SUBNET_ID: db_row.subnet_id, + 'ip_version': db_row.ip_version, + 'cidr': db_row.cidr, + SUBNET_NAME: db_row.subnet_name, + 'used_ips': db_row.used_ips if db_row.used_ips else 0, + 'total_ips': subnet_total_ips + } + # Attach subnet result and rollup subnet sums into the parent + network_dict['subnet_ip_availability'].append(subnet) + network_dict['total_ips'] += subnet['total_ips'] + network_dict['used_ips'] += subnet['used_ips'] diff --git a/neutron/extensions/network_ip_availability.py b/neutron/extensions/network_ip_availability.py new file mode 100644 index 00000000000..31ec560d5b5 --- /dev/null +++ b/neutron/extensions/network_ip_availability.py @@ -0,0 +1,86 @@ +# Copyright 2016 GoDaddy. +# +# 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 oslo_log.log as logging + +import neutron.api.extensions as extensions +import neutron.api.v2.attributes as attr +import neutron.api.v2.base as base +import neutron.services.network_ip_availability.plugin as plugin + +LOG = logging.getLogger(__name__) + +RESOURCE_NAME = "network_ip_availability" +RESOURCE_PLURAL = "network_ip_availabilities" +COLLECTION_NAME = RESOURCE_PLURAL.replace('_', '-') +EXT_ALIAS = RESOURCE_NAME.replace('_', '-') + +RESOURCE_ATTRIBUTE_MAP = { + RESOURCE_PLURAL: { + 'network_id': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'network_name': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'tenant_id': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'total_ips': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'used_ips': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + 'subnet_ip_availability': {'allow_post': False, 'allow_put': False, + 'is_visible': True}, + # TODO(wwriverrat) Make composite attribute for subnet_ip_availability + } +} + + +class Network_ip_availability(extensions.ExtensionDescriptor): + """Extension class supporting network ip availability information.""" + + @classmethod + def get_name(cls): + return "Network IP Availability" + + @classmethod + def get_alias(cls): + return EXT_ALIAS + + @classmethod + def get_description(cls): + return "Provides IP availability data for each network and subnet." + + @classmethod + def get_updated(cls): + return "2015-09-24T00:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Extended Resource for service type management.""" + attr.PLURALS[RESOURCE_PLURAL] = RESOURCE_NAME + resource_attributes = RESOURCE_ATTRIBUTE_MAP[RESOURCE_PLURAL] + controller = base.create_resource( + RESOURCE_PLURAL, + RESOURCE_NAME, + plugin.NetworkIPAvailabilityPlugin.get_instance(), + resource_attributes) + return [extensions.ResourceExtension(COLLECTION_NAME, + controller, + attr_map=resource_attributes)] + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/neutron/services/network_ip_availability/__init__.py b/neutron/services/network_ip_availability/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/neutron/services/network_ip_availability/plugin.py b/neutron/services/network_ip_availability/plugin.py new file mode 100644 index 00000000000..e3a4f8674fa --- /dev/null +++ b/neutron/services/network_ip_availability/plugin.py @@ -0,0 +1,53 @@ +# Copyright 2016 GoDaddy. +# +# 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 neutron.common.exceptions as exceptions +import neutron.db.db_base_plugin_v2 as db_base_plugin_v2 +import neutron.db.network_ip_availability_db as ip_availability_db + + +class NetworkIPAvailabilityPlugin(ip_availability_db.IpAvailabilityMixin, + db_base_plugin_v2.NeutronDbPluginV2): + """This plugin exposes IP availability data for networks and subnets.""" + _instance = None + + supported_extension_aliases = ["network-ip-availability"] + + @classmethod + def get_instance(cls): + if cls._instance is None: + cls._instance = cls() + return cls._instance + + def get_plugin_description(self): + return "Provides IP availability data for each network and subnet." + + def get_plugin_type(self): + return "network-ip-availability" + + def get_network_ip_availabilities(self, context, filters=None, + fields=None): + """Returns ip availability data for a collection of networks.""" + return super(NetworkIPAvailabilityPlugin, + self).get_network_ip_availabilities(context, filters) + + def get_network_ip_availability(self, context, id=None, fields=None): + """Return ip availability data for a specific network id.""" + filters = {'network_id': [id]} + result = self.get_network_ip_availabilities(context, filters) + if result: + return result[0] + else: + raise exceptions.NetworkNotFound(net_id=id) diff --git a/neutron/tests/etc/policy.json b/neutron/tests/etc/policy.json index 3817e5999ef..44963d61187 100644 --- a/neutron/tests/etc/policy.json +++ b/neutron/tests/etc/policy.json @@ -43,6 +43,8 @@ "get_network:provider:physical_network": "rule:admin_only", "get_network:provider:segmentation_id": "rule:admin_only", "get_network:queue_id": "rule:admin_only", + "get_network_ip_availabilities": "rule:admin_only", + "get_network_ip_availability": "rule:admin_only", "create_network:shared": "rule:admin_only", "create_network:router:external": "rule:admin_only", "create_network:is_default": "rule:admin_only", diff --git a/neutron/tests/unit/extensions/test_network_ip_availability.py b/neutron/tests/unit/extensions/test_network_ip_availability.py new file mode 100644 index 00000000000..3a6ae2f8439 --- /dev/null +++ b/neutron/tests/unit/extensions/test_network_ip_availability.py @@ -0,0 +1,345 @@ +# Copyright 2016 GoDaddy. +# +# 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 neutron.api.extensions as api_ext +import neutron.common.config as config +import neutron.common.constants as constants +import neutron.extensions +import neutron.services.network_ip_availability.plugin as plugin_module +import neutron.tests.unit.db.test_db_base_plugin_v2 as test_db_base_plugin_v2 + +API_RESOURCE = 'network-ip-availabilities' +IP_AVAIL_KEY = 'network_ip_availability' +IP_AVAILS_KEY = 'network_ip_availabilities' +EXTENSIONS_PATH = ':'.join(neutron.extensions.__path__) +PLUGIN_NAME = '%s.%s' % (plugin_module.NetworkIPAvailabilityPlugin.__module__, + plugin_module.NetworkIPAvailabilityPlugin.__name__) + + +class TestNetworkIPAvailabilityAPI( + test_db_base_plugin_v2.NeutronDbPluginV2TestCase): + def setUp(self): + svc_plugins = {'plugin_name': PLUGIN_NAME} + super(TestNetworkIPAvailabilityAPI, self).setUp( + service_plugins=svc_plugins) + self.plugin = plugin_module.NetworkIPAvailabilityPlugin() + ext_mgr = api_ext.PluginAwareExtensionManager( + EXTENSIONS_PATH, {"network-ip-availability": self.plugin} + ) + app = config.load_paste_app('extensions_test_app') + self.ext_api = api_ext.ExtensionMiddleware(app, ext_mgr=ext_mgr) + + def _validate_availability(self, network, availability, expected_used_ips, + expected_total_ips=253): + self.assertEqual(network['name'], availability['network_name']) + self.assertEqual(network['id'], availability['network_id']) + self.assertEqual(expected_used_ips, availability['used_ips']) + self.assertEqual(expected_total_ips, availability['total_ips']) + + def _validate_from_availabilities(self, availabilities, wrapped_network, + expected_used_ips, + expected_total_ips=253): + network = wrapped_network['network'] + availability = self._find_availability(availabilities, network['id']) + self.assertIsNotNone(availability) + self._validate_availability(network, availability, + expected_used_ips=expected_used_ips, + expected_total_ips=expected_total_ips) + + @staticmethod + def _find_availability(availabilities, net_id): + for ip_availability in availabilities: + if net_id == ip_availability['network_id']: + return ip_availability + + def test_basic(self): + with self.network() as net: + with self.subnet(network=net): + network = net['network'] + # Get ALL + request = self.new_list_request(API_RESOURCE, self.fmt) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 0) + + # Get single via id + request = self.new_show_request(API_RESOURCE, network['id']) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + self.assertIn(IP_AVAIL_KEY, response) + usage = response[IP_AVAIL_KEY] + self._validate_availability(network, usage, 0) + + def test_usages_multi_nets_subnets(self): + with self.network(name='net1') as n1,\ + self.network(name='net2') as n2,\ + self.network(name='net3') as n3: + # n1 should have 2 subnets, n2 should have none, n3 has 1 + with self.subnet(network=n1) as subnet1_1, \ + self.subnet(cidr='40.0.0.0/24', network=n3) as subnet3_1: + # Consume 3 ports n1, none n2, 2 ports on n3 + with self.port(subnet=subnet1_1),\ + self.port(subnet=subnet1_1),\ + self.port(subnet=subnet1_1),\ + self.port(subnet=subnet3_1),\ + self.port(subnet=subnet3_1): + + # Test get ALL + request = self.new_list_request(API_RESOURCE) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(3, len(response[IP_AVAILS_KEY])) + + data = response[IP_AVAILS_KEY] + self._validate_from_availabilities(data, n1, 3, 253) + self._validate_from_availabilities(data, n2, 0, 0) + self._validate_from_availabilities(data, n3, 2, 253) + + # Test get single via network id + network = n1['network'] + request = self.new_show_request(API_RESOURCE, + network['id']) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + self.assertIn(IP_AVAIL_KEY, response) + self._validate_availability(network, + response[IP_AVAIL_KEY], 3, 253) + + def test_usages_multi_nets_subnets_sums(self): + with self.network(name='net1') as n1: + # n1 has 2 subnets + with self.subnet(network=n1) as subnet1_1, \ + self.subnet(cidr='40.0.0.0/24', network=n1) as subnet1_2: + # Consume 3 ports n1: 1 on subnet 1 and 2 on subnet 2 + with self.port(subnet=subnet1_1),\ + self.port(subnet=subnet1_2),\ + self.port(subnet=subnet1_2): + # Get ALL + request = self.new_list_request(API_RESOURCE) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + n1, 3, 506) + + # Get single via network id + network = n1['network'] + request = self.new_show_request(API_RESOURCE, + network['id']) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + self.assertIn(IP_AVAIL_KEY, response) + self._validate_availability(network, + response[IP_AVAIL_KEY], 3, 506) + + def test_usages_port_consumed_v4(self): + with self.network() as net: + with self.subnet(network=net) as subnet: + request = self.new_list_request(API_RESOURCE) + # Consume 2 ports + with self.port(subnet=subnet), self.port(subnet=subnet): + response = self.deserialize(self.fmt, + request.get_response( + self.ext_api)) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 2) + + def test_usages_query_ip_version_v4(self): + with self.network() as net: + with self.subnet(network=net): + # Get IPv4 + params = 'ip_version=4' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 0) + + # Get IPv6 should return empty array + params = 'ip_version=6' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(0, len(response[IP_AVAILS_KEY])) + + def test_usages_query_ip_version_v6(self): + with self.network() as net: + with self.subnet( + network=net, cidr='2607:f0d0:1002:51::/64', + ip_version=6, + ipv6_address_mode=constants.DHCPV6_STATELESS): + # Get IPv6 + params = 'ip_version=6' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities( + response[IP_AVAILS_KEY], net, 0, 18446744073709551614) + + # Get IPv4 should return empty array + params = 'ip_version=4' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(0, len(response[IP_AVAILS_KEY])) + + def test_usages_ports_consumed_v6(self): + with self.network() as net: + with self.subnet( + network=net, cidr='2607:f0d0:1002:51::/64', + ip_version=6, + ipv6_address_mode=constants.DHCPV6_STATELESS) as subnet: + request = self.new_list_request(API_RESOURCE) + # Consume 3 ports + with self.port(subnet=subnet),\ + self.port(subnet=subnet), \ + self.port(subnet=subnet): + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 3, + 18446744073709551614) + + def test_usages_query_network_id(self): + with self.network() as net: + with self.subnet(network=net): + network = net['network'] + test_id = network['id'] + # Get by query param: network_id + params = 'network_id=%s' % test_id + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 0) + + # Get by NON-matching query param: network_id + params = 'network_id=clearlywontmatch' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(0, len(response[IP_AVAILS_KEY])) + + def test_usages_query_network_name(self): + test_name = 'net_name_1' + with self.network(name=test_name) as net: + with self.subnet(network=net): + # Get by query param: network_name + params = 'network_name=%s' % test_name + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 0) + + # Get by NON-matching query param: network_name + params = 'network_name=clearly-wont-match' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(0, len(response[IP_AVAILS_KEY])) + + def test_usages_query_tenant_id(self): + test_tenant_id = 'a-unique-test-id' + with self.network(tenant_id=test_tenant_id) as net: + with self.subnet(network=net): + # Get by query param: network_name + params = 'tenant_id=%s' % test_tenant_id + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertIn(IP_AVAILS_KEY, response) + self.assertEqual(1, len(response[IP_AVAILS_KEY])) + self._validate_from_availabilities(response[IP_AVAILS_KEY], + net, 0) + for net_avail in response[IP_AVAILS_KEY]: + self.assertEqual(test_tenant_id, net_avail['tenant_id']) + + # Get by NON-matching query param: network_name + params = 'tenant_id=clearly-wont-match' + request = self.new_list_request(API_RESOURCE, params=params) + response = self.deserialize(self.fmt, + request.get_response(self.ext_api)) + self.assertEqual(0, len(response[IP_AVAILS_KEY])) + + def test_usages_multi_net_multi_subnet_46(self): + # Setup mixed v4/v6 networks with IPs consumed on each + with self.network(name='net-v6-1') as net_v6_1, \ + self.network(name='net-v6-2') as net_v6_2, \ + self.network(name='net-v4-1') as net_v4_1, \ + self.network(name='net-v4-2') as net_v4_2: + with self.subnet(network=net_v6_1, cidr='2607:f0d0:1002:51::/64', + ip_version=6) as s61, \ + self.subnet(network=net_v6_2, + cidr='2607:f0d0:1003:52::/64', + ip_version=6) as s62, \ + self.subnet(network=net_v4_1, cidr='10.0.0.0/24') as s41, \ + self.subnet(network=net_v4_2, cidr='10.0.1.0/24') as s42: + with self.port(subnet=s61),\ + self.port(subnet=s62), self.port(subnet=s62), \ + self.port(subnet=s41), \ + self.port(subnet=s42), self.port(subnet=s42): + + # Verify consumption across all + request = self.new_list_request(API_RESOURCE) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + avails_list = response[IP_AVAILS_KEY] + self._validate_from_availabilities( + avails_list, net_v6_1, 1, 18446744073709551614) + self._validate_from_availabilities( + avails_list, net_v6_2, 2, 18446744073709551614) + self._validate_from_availabilities( + avails_list, net_v4_1, 1, 253) + self._validate_from_availabilities( + avails_list, net_v4_2, 2, 253) + + # Query by IP versions. Ensure subnet versions match + for ip_ver in [4, 6]: + params = 'ip_version=%i' % ip_ver + request = self.new_list_request(API_RESOURCE, + params=params) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + for net_avail in response[IP_AVAILS_KEY]: + for sub in net_avail['subnet_ip_availability']: + self.assertEqual(ip_ver, sub['ip_version']) + + # Verify consumption querying 2 network ids (IN clause) + request = self.new_list_request( + API_RESOURCE, + params='network_id=%s&network_id=%s' + % (net_v4_2['network']['id'], + net_v6_2['network']['id'])) + response = self.deserialize( + self.fmt, request.get_response(self.ext_api)) + avails_list = response[IP_AVAILS_KEY] + self._validate_from_availabilities( + avails_list, net_v6_2, 2, 18446744073709551614) + self._validate_from_availabilities( + avails_list, net_v4_2, 2, 253) diff --git a/releasenotes/notes/network_ip_availability-d64bd7032b3c15ee.yaml b/releasenotes/notes/network_ip_availability-d64bd7032b3c15ee.yaml new file mode 100644 index 00000000000..33ab4316adb --- /dev/null +++ b/releasenotes/notes/network_ip_availability-d64bd7032b3c15ee.yaml @@ -0,0 +1,9 @@ +--- +prelude: > + Neutron now provides network IP availability information. +features: + - A new API endpoint /v2.0/network-ip-availabilities that allows an admin + to quickly get counts of used_ips and total_ips for network(s). New + endpoint allows filtering by network_id, network_name, tenant_id, and + ip_version. Response returns network and nested subnet data that includes + used and total IPs. diff --git a/setup.cfg b/setup.cfg index 5547c156ece..e1d262a2003 100644 --- a/setup.cfg +++ b/setup.cfg @@ -80,6 +80,7 @@ neutron.service_plugins = bgp = neutron.services.bgp.bgp_plugin:BgpPlugin flavors = neutron.services.flavors.flavors_plugin:FlavorsPlugin auto_allocate = neutron.services.auto_allocate.plugin:Plugin + network_ip_availability = neutron.services.network_ip_availability.plugin:NetworkIPAvailabilityPlugin neutron.qos.notification_drivers = message_queue = neutron.services.qos.notification_drivers.message_queue:RpcQosServiceNotificationDriver neutron.ml2.type_drivers =