Add API extension for reporting IP availability usage statistics

Implements an API extension for reporting availibility of IP
addresses on Neutron networks/subnets based on the blueprint
proposed at https://review.openstack.org/#/c/180803/

This provides an easy way for operators to count the number of
used and total IP addresses on any or all networks and/or
subnets.

Co-Authored-By: David Bingham <dbingham@godaddy.com>
Co-Authored-By: Craig Jellick <craig.jellick@gmail.com>

APIImpact
DocImpact: As a new API, will need all new docs. See devref for details.

Implements: blueprint network-ip-usage-api
Closes-Bug: 1457986
Change-Id: I81406054d46b2c0e0ffcd56e898e329f943ba46f
This commit is contained in:
Mike Dorman 2015-08-13 21:24:58 -06:00 committed by David Bingham
parent f8ecd2b1c3
commit 2f741ca5f9
11 changed files with 861 additions and 0 deletions

View File

@ -76,6 +76,7 @@ Neutron Internals
instrumentation
address_scopes
openvswitch_firewall
network_ip_availability
Testing
-------

View File

@ -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

View File

@ -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",

View File

@ -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']

View File

@ -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 {}

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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.

View File

@ -79,6 +79,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 =