487 lines
20 KiB
Python
487 lines
20 KiB
Python
# Copyright 2013 Rackspace Hosting Inc.
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import netaddr
|
|
from neutron._i18n import _
|
|
from neutron.common import config as neutron_cfg
|
|
from neutron import quota
|
|
from neutron_lib import exceptions as n_exc
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import importutils
|
|
|
|
from quark import allocation_pool
|
|
from quark.db import api as db_api
|
|
from quark import exceptions as q_exc
|
|
from quark import network_strategy
|
|
from quark.plugin_modules import ip_policies
|
|
from quark.plugin_modules import routes
|
|
from quark import plugin_views as v
|
|
from quark import quota_driver as qdv
|
|
from quark import utils
|
|
|
|
CONF = cfg.CONF
|
|
LOG = logging.getLogger(__name__)
|
|
STRATEGY = network_strategy.STRATEGY
|
|
|
|
quark_subnet_opts = [
|
|
cfg.BoolOpt('allow_allocation_pool_update',
|
|
default=False,
|
|
help=_('Controls whether or not to allow allocation_pool '
|
|
'updates')),
|
|
cfg.BoolOpt('allow_allocation_pool_growth',
|
|
default=False,
|
|
help=_('Controls whether or not to allow allocation_pool '
|
|
'growing. Otherwise shrinking is only allowed'))
|
|
]
|
|
|
|
CONF.register_opts(quark_subnet_opts, "QUARK")
|
|
|
|
ipam_driver = (importutils.import_class(CONF.QUARK.ipam_driver))()
|
|
|
|
|
|
def _validate_subnet_cidr(context, network_id, new_subnet_cidr):
|
|
"""Validate the CIDR for a subnet.
|
|
|
|
Verifies the specified CIDR does not overlap with the ones defined
|
|
for the other subnets specified for this network, or with any other
|
|
CIDR if overlapping IPs are disabled.
|
|
|
|
"""
|
|
if neutron_cfg.cfg.CONF.allow_overlapping_ips:
|
|
return
|
|
|
|
try:
|
|
new_subnet_ipset = netaddr.IPSet([new_subnet_cidr])
|
|
except TypeError:
|
|
LOG.exception("Invalid or missing cidr: %s" % new_subnet_cidr)
|
|
raise n_exc.BadRequest(resource="subnet",
|
|
msg="Invalid or missing cidr")
|
|
|
|
filters = {
|
|
'network_id': network_id,
|
|
'shared': [False]
|
|
}
|
|
# Using admin context here, in case we actually share networks later
|
|
subnet_list = db_api.subnet_find(context=context.elevated(), **filters)
|
|
|
|
for subnet in subnet_list:
|
|
if (netaddr.IPSet([subnet.cidr]) & new_subnet_ipset):
|
|
# don't give out details of the overlapping subnet
|
|
err_msg = (_("Requested subnet with cidr: %(cidr)s for "
|
|
"network: %(network_id)s overlaps with another "
|
|
"subnet") %
|
|
{'cidr': new_subnet_cidr,
|
|
'network_id': network_id})
|
|
LOG.error(_("Validation for CIDR: %(new_cidr)s failed - "
|
|
"overlaps with subnet %(subnet_id)s "
|
|
"(CIDR: %(cidr)s)"),
|
|
{'new_cidr': new_subnet_cidr,
|
|
'subnet_id': subnet.id,
|
|
'cidr': subnet.cidr})
|
|
raise n_exc.InvalidInput(error_message=err_msg)
|
|
|
|
|
|
def create_subnet(context, subnet):
|
|
"""Create a subnet.
|
|
|
|
Create a subnet which represents a range of IP addresses
|
|
that can be allocated to devices
|
|
|
|
: param context: neutron api request context
|
|
: param subnet: dictionary describing the subnet, with keys
|
|
as listed in the RESOURCE_ATTRIBUTE_MAP object in
|
|
neutron/api/v2/attributes.py. All keys will be populated.
|
|
"""
|
|
LOG.info("create_subnet for tenant %s" % context.tenant_id)
|
|
net_id = subnet["subnet"]["network_id"]
|
|
|
|
with context.session.begin():
|
|
net = db_api.network_find(context=context, limit=None, sorts=['id'],
|
|
marker=None, page_reverse=False, fields=None,
|
|
id=net_id, scope=db_api.ONE)
|
|
if not net:
|
|
raise n_exc.NetworkNotFound(net_id=net_id)
|
|
|
|
sub_attrs = subnet["subnet"]
|
|
|
|
always_pop = ["enable_dhcp", "ip_version", "first_ip", "last_ip",
|
|
"_cidr"]
|
|
admin_only = ["segment_id", "do_not_use", "created_at",
|
|
"next_auto_assign_ip"]
|
|
utils.filter_body(context, sub_attrs, admin_only, always_pop)
|
|
|
|
_validate_subnet_cidr(context, net_id, sub_attrs["cidr"])
|
|
|
|
cidr = netaddr.IPNetwork(sub_attrs["cidr"])
|
|
|
|
err_vals = {'cidr': sub_attrs["cidr"], 'network_id': net_id}
|
|
err = _("Requested subnet with cidr: %(cidr)s for "
|
|
"network: %(network_id)s. Prefix is too small, must be a "
|
|
"larger subnet. A prefix less than /%(prefix)s is required.")
|
|
|
|
if cidr.version == 6 and cidr.prefixlen > 64:
|
|
err_vals["prefix"] = 65
|
|
err_msg = err % err_vals
|
|
raise n_exc.InvalidInput(error_message=err_msg)
|
|
elif cidr.version == 4 and cidr.prefixlen > 30:
|
|
err_vals["prefix"] = 31
|
|
err_msg = err % err_vals
|
|
raise n_exc.InvalidInput(error_message=err_msg)
|
|
# Enforce subnet quotas
|
|
net_subnets = get_subnets(context,
|
|
filters=dict(network_id=net_id))
|
|
if not context.is_admin:
|
|
v4_count, v6_count = 0, 0
|
|
for subnet in net_subnets:
|
|
if netaddr.IPNetwork(subnet['cidr']).version == 6:
|
|
v6_count += 1
|
|
else:
|
|
v4_count += 1
|
|
|
|
if cidr.version == 6:
|
|
tenant_quota_v6 = context.session.query(qdv.Quota).filter_by(
|
|
tenant_id=context.tenant_id,
|
|
resource='v6_subnets_per_network').first()
|
|
if tenant_quota_v6 != -1:
|
|
quota.QUOTAS.limit_check(
|
|
context, context.tenant_id,
|
|
v6_subnets_per_network=v6_count + 1)
|
|
else:
|
|
tenant_quota_v4 = context.session.query(qdv.Quota).filter_by(
|
|
tenant_id=context.tenant_id,
|
|
resource='v4_subnets_per_network').first()
|
|
if tenant_quota_v4 != -1:
|
|
quota.QUOTAS.limit_check(
|
|
context, context.tenant_id,
|
|
v4_subnets_per_network=v4_count + 1)
|
|
|
|
# See RM981. The default behavior of setting a gateway unless
|
|
# explicitly asked to not is no longer desirable.
|
|
gateway_ip = utils.pop_param(sub_attrs, "gateway_ip")
|
|
dns_ips = utils.pop_param(sub_attrs, "dns_nameservers", [])
|
|
host_routes = utils.pop_param(sub_attrs, "host_routes", [])
|
|
allocation_pools = utils.pop_param(sub_attrs, "allocation_pools", None)
|
|
|
|
sub_attrs["network"] = net
|
|
new_subnet = db_api.subnet_create(context, **sub_attrs)
|
|
|
|
cidrs = []
|
|
alloc_pools = allocation_pool.AllocationPools(sub_attrs["cidr"],
|
|
allocation_pools)
|
|
if isinstance(allocation_pools, list):
|
|
cidrs = alloc_pools.get_policy_cidrs()
|
|
|
|
quota.QUOTAS.limit_check(
|
|
context,
|
|
context.tenant_id,
|
|
alloc_pools_per_subnet=len(alloc_pools))
|
|
|
|
ip_policies.ensure_default_policy(cidrs, [new_subnet])
|
|
new_subnet["ip_policy"] = db_api.ip_policy_create(context,
|
|
exclude=cidrs)
|
|
|
|
quota.QUOTAS.limit_check(context, context.tenant_id,
|
|
routes_per_subnet=len(host_routes))
|
|
|
|
default_route = None
|
|
for route in host_routes:
|
|
netaddr_route = netaddr.IPNetwork(route["destination"])
|
|
if netaddr_route.value == routes.DEFAULT_ROUTE.value:
|
|
if default_route:
|
|
raise q_exc.DuplicateRouteConflict(
|
|
subnet_id=new_subnet["id"])
|
|
|
|
default_route = route
|
|
gateway_ip = default_route["nexthop"]
|
|
alloc_pools.validate_gateway_excluded(gateway_ip)
|
|
|
|
new_subnet["routes"].append(db_api.route_create(
|
|
context, cidr=route["destination"], gateway=route["nexthop"]))
|
|
|
|
quota.QUOTAS.limit_check(context, context.tenant_id,
|
|
dns_nameservers_per_subnet=len(dns_ips))
|
|
|
|
for dns_ip in dns_ips:
|
|
new_subnet["dns_nameservers"].append(db_api.dns_create(
|
|
context, ip=netaddr.IPAddress(dns_ip)))
|
|
|
|
# if the gateway_ip is IN the cidr for the subnet and NOT excluded by
|
|
# policies, we should raise a 409 conflict
|
|
if gateway_ip and default_route is None:
|
|
alloc_pools.validate_gateway_excluded(gateway_ip)
|
|
new_subnet["routes"].append(db_api.route_create(
|
|
context, cidr=str(routes.DEFAULT_ROUTE), gateway=gateway_ip))
|
|
|
|
subnet_dict = v._make_subnet_dict(new_subnet)
|
|
subnet_dict["gateway_ip"] = gateway_ip
|
|
|
|
return subnet_dict
|
|
|
|
|
|
def _pool_is_growing(original_pool, new_pool):
|
|
# create IPSet for original pool
|
|
ori_set = netaddr.IPSet()
|
|
for rng in original_pool._alloc_pools:
|
|
ori_set.add(netaddr.IPRange(rng['start'], rng['end']))
|
|
|
|
# create IPSet for net pool
|
|
new_set = netaddr.IPSet()
|
|
for rng in new_pool._alloc_pools:
|
|
new_set.add(netaddr.IPRange(rng['start'], rng['end']))
|
|
|
|
# we are growing the original set is not a superset of the new set
|
|
return not ori_set.issuperset(new_set)
|
|
|
|
|
|
def update_subnet(context, id, subnet):
|
|
"""Update values of a subnet.
|
|
|
|
: param context: neutron api request context
|
|
: param id: UUID representing the subnet to update.
|
|
: param subnet: dictionary with keys indicating fields to update.
|
|
valid keys are those that have a value of True for 'allow_put'
|
|
as listed in the RESOURCE_ATTRIBUTE_MAP object in
|
|
neutron/api/v2/attributes.py.
|
|
"""
|
|
LOG.info("update_subnet %s for tenant %s" %
|
|
(id, context.tenant_id))
|
|
|
|
with context.session.begin():
|
|
subnet_db = db_api.subnet_find(context=context, limit=None,
|
|
page_reverse=False, sorts=['id'],
|
|
marker_obj=None, fields=None,
|
|
id=id, scope=db_api.ONE)
|
|
if not subnet_db:
|
|
raise n_exc.SubnetNotFound(subnet_id=id)
|
|
|
|
s = subnet["subnet"]
|
|
always_pop = ["_cidr", "cidr", "first_ip", "last_ip", "ip_version",
|
|
"segment_id", "network_id"]
|
|
admin_only = ["do_not_use", "created_at", "tenant_id",
|
|
"next_auto_assign_ip", "enable_dhcp"]
|
|
utils.filter_body(context, s, admin_only, always_pop)
|
|
|
|
dns_ips = utils.pop_param(s, "dns_nameservers", [])
|
|
host_routes = utils.pop_param(s, "host_routes", [])
|
|
gateway_ip = utils.pop_param(s, "gateway_ip", None)
|
|
allocation_pools = utils.pop_param(s, "allocation_pools", None)
|
|
if not CONF.QUARK.allow_allocation_pool_update:
|
|
if allocation_pools:
|
|
raise n_exc.BadRequest(
|
|
resource="subnets",
|
|
msg="Allocation pools cannot be updated.")
|
|
|
|
if subnet_db["ip_policy"] is not None:
|
|
ip_policy_cidrs = subnet_db["ip_policy"].get_cidrs_ip_set()
|
|
else:
|
|
ip_policy_cidrs = netaddr.IPSet([])
|
|
|
|
alloc_pools = allocation_pool.AllocationPools(
|
|
subnet_db["cidr"],
|
|
policies=ip_policy_cidrs)
|
|
else:
|
|
alloc_pools = allocation_pool.AllocationPools(subnet_db["cidr"],
|
|
allocation_pools)
|
|
original_pools = subnet_db.allocation_pools
|
|
ori_pools = allocation_pool.AllocationPools(subnet_db["cidr"],
|
|
original_pools)
|
|
# Check if the pools are growing or shrinking
|
|
is_growing = _pool_is_growing(ori_pools, alloc_pools)
|
|
if not CONF.QUARK.allow_allocation_pool_growth and is_growing:
|
|
raise n_exc.BadRequest(
|
|
resource="subnets",
|
|
msg="Allocation pools may not be updated to be larger "
|
|
"do to configuration settings")
|
|
|
|
quota.QUOTAS.limit_check(
|
|
context,
|
|
context.tenant_id,
|
|
alloc_pools_per_subnet=len(alloc_pools))
|
|
if gateway_ip:
|
|
alloc_pools.validate_gateway_excluded(gateway_ip)
|
|
default_route = None
|
|
for route in host_routes:
|
|
netaddr_route = netaddr.IPNetwork(route["destination"])
|
|
if netaddr_route.value == routes.DEFAULT_ROUTE.value:
|
|
default_route = route
|
|
break
|
|
|
|
if default_route is None:
|
|
route_model = db_api.route_find(
|
|
context, cidr=str(routes.DEFAULT_ROUTE), subnet_id=id,
|
|
scope=db_api.ONE)
|
|
if route_model:
|
|
db_api.route_update(context, route_model,
|
|
gateway=gateway_ip)
|
|
else:
|
|
db_api.route_create(context,
|
|
cidr=str(routes.DEFAULT_ROUTE),
|
|
gateway=gateway_ip, subnet_id=id)
|
|
|
|
if dns_ips:
|
|
subnet_db["dns_nameservers"] = []
|
|
quota.QUOTAS.limit_check(context, context.tenant_id,
|
|
dns_nameservers_per_subnet=len(dns_ips))
|
|
|
|
for dns_ip in dns_ips:
|
|
subnet_db["dns_nameservers"].append(db_api.dns_create(
|
|
context,
|
|
ip=netaddr.IPAddress(dns_ip)))
|
|
|
|
if host_routes:
|
|
subnet_db["routes"] = []
|
|
quota.QUOTAS.limit_check(context, context.tenant_id,
|
|
routes_per_subnet=len(host_routes))
|
|
|
|
for route in host_routes:
|
|
subnet_db["routes"].append(db_api.route_create(
|
|
context, cidr=route["destination"], gateway=route["nexthop"]))
|
|
if CONF.QUARK.allow_allocation_pool_update:
|
|
if isinstance(allocation_pools, list):
|
|
cidrs = alloc_pools.get_policy_cidrs()
|
|
ip_policies.ensure_default_policy(cidrs, [subnet_db])
|
|
subnet_db["ip_policy"] = db_api.ip_policy_update(
|
|
context, subnet_db["ip_policy"], exclude=cidrs)
|
|
# invalidate the cache
|
|
db_api.subnet_update_set_alloc_pool_cache(context, subnet_db)
|
|
subnet = db_api.subnet_update(context, subnet_db, **s)
|
|
return v._make_subnet_dict(subnet)
|
|
|
|
|
|
def get_subnet(context, id, fields=None):
|
|
"""Retrieve a subnet.
|
|
|
|
: param context: neutron api request context
|
|
: param id: UUID representing the subnet to fetch.
|
|
: param fields: a list of strings that are valid keys in a
|
|
subnet dictionary as listed in the RESOURCE_ATTRIBUTE_MAP
|
|
object in neutron/api/v2/attributes.py. Only these fields
|
|
will be returned.
|
|
"""
|
|
LOG.info("get_subnet %s for tenant %s with fields %s" %
|
|
(id, context.tenant_id, fields))
|
|
subnet = db_api.subnet_find(context=context, limit=None,
|
|
page_reverse=False, sorts=['id'],
|
|
marker_obj=None, fields=None, id=id,
|
|
join_dns=True, join_routes=True,
|
|
scope=db_api.ONE)
|
|
if not subnet:
|
|
raise n_exc.SubnetNotFound(subnet_id=id)
|
|
|
|
cache = subnet.get("_allocation_pool_cache")
|
|
if not cache:
|
|
new_cache = subnet.allocation_pools
|
|
db_api.subnet_update_set_alloc_pool_cache(context, subnet, new_cache)
|
|
return v._make_subnet_dict(subnet)
|
|
|
|
|
|
def get_subnets(context, limit=None, page_reverse=False, sorts=['id'],
|
|
marker=None, filters=None, fields=None):
|
|
"""Retrieve a list of subnets.
|
|
|
|
The contents of the list depends on the identity of the user
|
|
making the request (as indicated by the context) as well as any
|
|
filters.
|
|
: param context: neutron api request context
|
|
: param filters: a dictionary with keys that are valid keys for
|
|
a subnet as listed in the RESOURCE_ATTRIBUTE_MAP object
|
|
in neutron/api/v2/attributes.py. Values in this dictiontary
|
|
are an iterable containing values that will be used for an exact
|
|
match comparison for that value. Each result returned by this
|
|
function will have matched one of the values for each key in
|
|
filters.
|
|
: param fields: a list of strings that are valid keys in a
|
|
subnet dictionary as listed in the RESOURCE_ATTRIBUTE_MAP
|
|
object in neutron/api/v2/attributes.py. Only these fields
|
|
will be returned.
|
|
"""
|
|
LOG.info("get_subnets for tenant %s with filters %s fields %s" %
|
|
(context.tenant_id, filters, fields))
|
|
filters = filters or {}
|
|
subnets = db_api.subnet_find(context, limit=limit,
|
|
page_reverse=page_reverse, sorts=sorts,
|
|
marker_obj=marker, join_dns=True,
|
|
join_routes=True, join_pool=True, **filters)
|
|
for subnet in subnets:
|
|
cache = subnet.get("_allocation_pool_cache")
|
|
if not cache:
|
|
db_api.subnet_update_set_alloc_pool_cache(
|
|
context, subnet, subnet.allocation_pools)
|
|
return v._make_subnets_list(subnets, fields=fields)
|
|
|
|
|
|
def get_subnets_count(context, filters=None):
|
|
"""Return the number of subnets.
|
|
|
|
The result depends on the identity of the user making the request
|
|
(as indicated by the context) as well as any filters.
|
|
: param context: neutron api request context
|
|
: param filters: a dictionary with keys that are valid keys for
|
|
a network as listed in the RESOURCE_ATTRIBUTE_MAP object
|
|
in neutron/api/v2/attributes.py. Values in this dictiontary
|
|
are an iterable containing values that will be used for an exact
|
|
match comparison for that value. Each result returned by this
|
|
function will have matched one of the values for each key in
|
|
filters.
|
|
|
|
NOTE: this method is optional, as it was not part of the originally
|
|
defined plugin API.
|
|
"""
|
|
LOG.info("get_subnets_count for tenant %s with filters %s" %
|
|
(context.tenant_id, filters))
|
|
return db_api.subnet_count_all(context, **filters)
|
|
|
|
|
|
def _delete_subnet(context, subnet):
|
|
if subnet.allocated_ips:
|
|
raise n_exc.SubnetInUse(subnet_id=subnet["id"])
|
|
db_api.subnet_delete(context, subnet)
|
|
|
|
|
|
def delete_subnet(context, id):
|
|
"""Delete a subnet.
|
|
|
|
: param context: neutron api request context
|
|
: param id: UUID representing the subnet to delete.
|
|
"""
|
|
LOG.info("delete_subnet %s for tenant %s" % (id, context.tenant_id))
|
|
with context.session.begin():
|
|
subnet = db_api.subnet_find(context, id=id, scope=db_api.ONE)
|
|
if not subnet:
|
|
raise n_exc.SubnetNotFound(subnet_id=id)
|
|
|
|
if not context.is_admin:
|
|
if STRATEGY.is_provider_network(subnet.network_id):
|
|
if subnet.tenant_id == context.tenant_id:
|
|
# A tenant can't delete subnets on provider network
|
|
raise n_exc.NotAuthorized(subnet_id=id)
|
|
else:
|
|
# Raise a NotFound here because the foreign tenant
|
|
# does not have to know about other tenant's subnet
|
|
# existence.
|
|
raise n_exc.SubnetNotFound(subnet_id=id)
|
|
|
|
_delete_subnet(context, subnet)
|
|
|
|
|
|
def diagnose_subnet(context, id, fields):
|
|
if not context.is_admin:
|
|
raise n_exc.NotAuthorized()
|
|
|
|
if id == "*":
|
|
return {'subnets': get_subnets(context, filters={})}
|
|
return {'subnets': get_subnet(context, id)}
|