Merge "implementation for bug 1008180"

This commit is contained in:
Jenkins 2012-08-13 23:58:14 +00:00 committed by Gerrit Code Review
commit 02cff04fb4
7 changed files with 370 additions and 20 deletions

View File

@ -241,12 +241,12 @@ RESOURCE_ATTRIBUTE_MAP = {
'allocation_pools': {'allow_post': True, 'allow_put': False,
'default': ATTR_NOT_SPECIFIED,
'is_visible': True},
'dns_namesevers': {'allow_post': True, 'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'is_visible': False},
'additional_host_routes': {'allow_post': True, 'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'is_visible': False},
'dns_nameservers': {'allow_post': True, 'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'is_visible': True},
'host_routes': {'allow_post': True, 'allow_put': True,
'default': ATTR_NOT_SPECIFIED,
'is_visible': True},
'tenant_id': {'allow_post': True, 'allow_put': False,
'required_by_policy': True,
'is_visible': True},

View File

@ -41,6 +41,8 @@ FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
exceptions.OutOfBoundsAllocationPool: webob.exc.HTTPBadRequest,
exceptions.InvalidAllocationPool: webob.exc.HTTPBadRequest,
exceptions.InvalidSharedSetting: webob.exc.HTTPConflict,
exceptions.HostRoutesExhausted: webob.exc.HTTPBadRequest,
exceptions.DNSNameServersExhausted: webob.exc.HTTPBadRequest,
}
QUOTAS = quota.QUOTAS

View File

@ -45,6 +45,8 @@ core_opts = [
cfg.StrOpt('base_mac', default="fa:16:3e:00:00:00"),
cfg.IntOpt('mac_generation_retries', default=16),
cfg.BoolOpt('allow_bulk', default=True),
cfg.IntOpt('max_dns_nameservers', default=5),
cfg.IntOpt('max_subnet_host_routes', default=20),
]
# Register the configuration options

View File

@ -100,6 +100,18 @@ class MacAddressInUse(InUse):
"The mac address %(mac)s is in use.")
class HostRoutesExhausted(QuantumException):
# NOTE(xchenum): probably make sense to use quota exceeded exception?
message = _("Unable to complete operation for %(subnet_id)s. "
"The number of host routes exceeds the limit %(quota).")
class DNSNameServersExhausted(QuantumException):
# NOTE(xchenum): probably make sense to use quota exceeded exception?
message = _("Unable to complete operation for %(subnet_id)s. "
"The number of DNS nameservers exceeds the limit %(quota).")
class IpAddressInUse(InUse):
message = _("Unable to complete operation for network %(net_id)s. "
"The IP address %(ip_address)s is in use.")

View File

@ -125,6 +125,20 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
raise q_exc.PortNotFound(port_id=id)
return port
def _get_dns_by_subnet(self, context, subnet_id):
try:
dns_qry = context.session.query(models_v2.DNSNameServer)
return dns_qry.filter_by(subnet_id=subnet_id).all()
except exc.NoResultFound:
return []
def _get_route_by_subnet(self, context, subnet_id):
try:
route_qry = context.session.query(models_v2.Route)
return route_qry.filter_by(subnet_id=subnet_id).all()
except exc.NoResultFound:
return []
def _fields(self, resource, fields):
if fields:
return dict(((key, item) for key, item in resource.iteritems()
@ -588,6 +602,18 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
pool_2=r_range,
subnet_cidr=subnet_cidr)
def _validate_host_route(self, route):
try:
netaddr.IPNetwork(route['destination'])
netaddr.IPAddress(route['nexthop'])
except netaddr.core.AddrFormatError:
err_msg = ("invalid route: %s" % (str(route)))
raise q_exc.InvalidInput(error_message=err_msg)
except ValueError:
# netaddr.IPAddress would raise this
err_msg = ("invalid route: %s" % (str(route)))
raise q_exc.InvalidInput(error_message=err_msg)
def _allocate_pools_for_subnet(self, context, subnet):
"""Create IP allocation pools for a given subnet
@ -663,9 +689,16 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
'end': pool['last_ip']}
for pool in subnet['allocation_pools']],
'gateway_ip': subnet['gateway_ip'],
'enable_dhcp': subnet['enable_dhcp']}
'enable_dhcp': subnet['enable_dhcp'],
'dns_nameservers': [dns['address']
for dns in subnet['dns_nameservers']],
'host_routes': [{'destination': route['destination'],
'nexthop': route['nexthop']}
for route in subnet['routes']],
}
if subnet['gateway_ip']:
res['gateway_ip'] = subnet['gateway_ip']
return self._fields(res, fields)
def _make_port_dict(self, port, fields=None):
@ -756,8 +789,37 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
def create_subnet_bulk(self, context, subnets):
return self._create_bulk('subnet', context, subnets)
def _validate_subnet(self, s):
"""a subroutine to validate a subnet spec"""
# check if the number of DNS nameserver exceeds the quota
if 'dns_nameservers' in s and \
s['dns_nameservers'] != attributes.ATTR_NOT_SPECIFIED:
if len(s['dns_nameservers']) > cfg.CONF.max_dns_nameservers:
raise q_exc.DNSNameServersExhausted(
subnet_id=id,
quota=cfg.CONF.max_dns_nameservers)
for dns in s['dns_nameservers']:
try:
netaddr.IPAddress(dns)
except Exception:
raise q_exc.InvalidInput(
error_message=("error parsing dns address %s" % dns))
# check if the number of host routes exceeds the quota
if 'host_routes' in s and \
s['host_routes'] != attributes.ATTR_NOT_SPECIFIED:
if len(s['host_routes']) > cfg.CONF.max_subnet_host_routes:
raise q_exc.HostRoutesExhausted(
subnet_id=id,
quota=cfg.CONF.max_subnet_host_routes)
# check if the routes are all valid
for rt in s['host_routes']:
self._validate_host_route(rt)
def create_subnet(self, context, subnet):
s = subnet['subnet']
self._validate_subnet(s)
net = netaddr.IPNetwork(s['cidr'])
if s['gateway_ip'] == attributes.ATTR_NOT_SPECIFIED:
s['gateway_ip'] = str(netaddr.IPAddress(net.first + 1))
@ -772,10 +834,25 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
network_id=s['network_id'],
ip_version=s['ip_version'],
cidr=s['cidr'],
gateway_ip=s['gateway_ip'],
enable_dhcp=s['enable_dhcp'])
context.session.add(subnet)
enable_dhcp=s['enable_dhcp'],
gateway_ip=s['gateway_ip'])
# perform allocate pools first, since it might raise an error
pools = self._allocate_pools_for_subnet(context, s)
context.session.add(subnet)
if s['dns_nameservers'] != attributes.ATTR_NOT_SPECIFIED:
for addr in s['dns_nameservers']:
ns = models_v2.DNSNameServer(address=addr,
subnet_id=subnet.id)
context.session.add(ns)
if s['host_routes'] != attributes.ATTR_NOT_SPECIFIED:
for rt in s['host_routes']:
route = models_v2.Route(subnet_id=subnet.id,
destination=rt['destination'],
nexthop=rt['nexthop'])
context.session.add(route)
for pool in pools:
ip_pool = models_v2.IPAllocationPool(subnet=subnet,
first_ip=pool['start'],
@ -786,11 +863,60 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
first_ip=pool['start'],
last_ip=pool['end'])
context.session.add(ip_range)
return self._make_subnet_dict(subnet)
def update_subnet(self, context, id, subnet):
"""Update the subnet with new info. The change however will not be
realized until the client renew the dns lease or we support
gratuitous DHCP offers"""
s = subnet['subnet']
self._validate_subnet(s)
with context.session.begin():
if "dns_nameservers" in s:
old_dns_list = self._get_dns_by_subnet(context, id)
new_dns_addr_set = set(s["dns_nameservers"])
old_dns_addr_set = set([dns['address']
for dns in old_dns_list])
for dns_addr in old_dns_addr_set - new_dns_addr_set:
for dns in old_dns_list:
if dns['address'] == dns_addr:
context.session.delete(dns)
for dns_addr in new_dns_addr_set - old_dns_addr_set:
dns = models_v2.DNSNameServer(
address=dns_addr,
subnet_id=id)
context.session.add(dns)
del s["dns_nameservers"]
def _combine(ht):
return ht['destination'] + "_" + ht['nexthop']
if "host_routes" in s:
old_route_list = self._get_route_by_subnet(context, id)
new_route_set = set([_combine(route)
for route in s['host_routes']])
old_route_set = set([_combine(route)
for route in old_route_list])
for route_str in old_route_set - new_route_set:
for route in old_route_list:
if _combine(route) == route_str:
context.session.delete(route)
for route_str in new_route_set - old_route_set:
route = models_v2.Route(
destination=route_str.partition("_")[0],
nexthop=route_str.partition("_")[2],
subnet_id=id)
context.session.add(route)
del s["host_routes"]
subnet = self._get_subnet(context, id)
subnet.update(s)
return self._make_subnet_dict(subnet)
@ -803,6 +929,7 @@ class QuantumDbPluginV2(quantum_plugin_base_v2.QuantumPluginBaseV2):
allocated = allocated_qry.filter_by(subnet_id=id).all()
if allocated:
raise q_exc.SubnetInUse(subnet_id=id)
context.session.delete(subnet)
def get_subnet(self, context, id, fields=None, verbose=None):

View File

@ -99,6 +99,25 @@ class Port(model_base.BASEV2, HasId, HasTenant):
device_owner = sa.Column(sa.String(255), nullable=False)
class DNSNameServer(model_base.BASEV2):
"""Internal representation of a DNS nameserver."""
address = sa.Column(sa.String(128), nullable=False, primary_key=True)
subnet_id = sa.Column(sa.String(36),
sa.ForeignKey('subnets.id',
ondelete="CASCADE"),
primary_key=True)
class Route(model_base.BASEV2):
"""Represents a route for a subnet or port."""
destination = sa.Column(sa.String(64), nullable=False, primary_key=True)
nexthop = sa.Column(sa.String(64), nullable=False, primary_key=True)
subnet_id = sa.Column(sa.String(36),
sa.ForeignKey('subnets.id',
ondelete="CASCADE"),
primary_key=True)
class Subnet(model_base.BASEV2, HasId, HasTenant):
"""Represents a quantum subnet.
@ -114,10 +133,12 @@ class Subnet(model_base.BASEV2, HasId, HasTenant):
backref='subnet',
lazy="dynamic")
enable_dhcp = sa.Column(sa.Boolean())
#TODO(danwent):
# - dns_namservers
# - additional_routes
dns_nameservers = orm.relationship(DNSNameServer,
backref='subnet',
cascade='delete')
routes = orm.relationship(Route,
backref='subnet',
cascade='delete')
class Network(model_base.BASEV2, HasId, HasTenant):

View File

@ -73,6 +73,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
# Update the plugin
cfg.CONF.set_override('core_plugin', plugin)
cfg.CONF.set_override('base_mac', "12:34:56:78:90:ab")
cfg.CONF.max_dns_nameservers = 2
cfg.CONF.max_subnet_host_routes = 2
self.api = APIRouter()
def _is_native_bulk_supported():
@ -179,7 +181,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
'tenant_id': self._tenant_id}}
for arg in ('allocation_pools',
'ip_version', 'tenant_id',
'enable_dhcp'):
'enable_dhcp', 'allocation_pools',
'dns_nameservers', 'host_routes'):
# Arg must be present and not null (but can be false)
if arg in kwargs and kwargs[arg] is not None:
data['subnet'][arg] = kwargs[arg]
@ -259,7 +262,8 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
return self._create_bulk(fmt, number, 'port', base_data, **kwargs)
def _make_subnet(self, fmt, network, gateway, cidr,
allocation_pools=None, ip_version=4, enable_dhcp=True):
allocation_pools=None, ip_version=4, enable_dhcp=True,
dns_nameservers=None, host_routes=None):
res = self._create_subnet(fmt,
net_id=network['network']['id'],
cidr=cidr,
@ -267,7 +271,9 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
tenant_id=network['network']['tenant_id'],
allocation_pools=allocation_pools,
ip_version=ip_version,
enable_dhcp=enable_dhcp)
enable_dhcp=enable_dhcp,
dns_nameservers=dns_nameservers,
host_routes=host_routes)
# Things can go wrong - raise HTTP exc with res code only
# so it can be caught by unit tests
if res.status_int >= 400:
@ -331,7 +337,9 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
fmt='json',
ip_version=4,
allocation_pools=None,
enable_dhcp=True):
enable_dhcp=True,
dns_nameservers=None,
host_routes=None):
# TODO(anyone) DRY this
# NOTE(salvatore-orlando): we can pass the network object
# to gen function anyway, and then avoid the repetition
@ -343,7 +351,9 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
cidr,
allocation_pools,
ip_version,
enable_dhcp)
enable_dhcp,
dns_nameservers,
host_routes)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
else:
@ -353,7 +363,9 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
cidr,
allocation_pools,
ip_version,
enable_dhcp)
enable_dhcp,
dns_nameservers,
host_routes)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
@ -1763,3 +1775,177 @@ class TestSubnetsV2(QuantumDbPluginV2TestCase):
subnet_req = self.new_create_request('subnets', data)
res = subnet_req.get_response(self.api)
self.assertEquals(res.status_int, 422)
def test_create_subnet_with_one_dns(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
allocation_pools = [{'start': '10.0.0.2',
'end': '10.0.0.100'}]
dns_nameservers = ['1.2.3.4']
self._test_create_subnet(gateway_ip=gateway_ip,
cidr=cidr,
allocation_pools=allocation_pools,
dns_nameservers=dns_nameservers)
def test_create_subnet_with_two_dns(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
allocation_pools = [{'start': '10.0.0.2',
'end': '10.0.0.100'}]
dns_nameservers = ['1.2.3.4', '4.3.2.1']
self._test_create_subnet(gateway_ip=gateway_ip,
cidr=cidr,
allocation_pools=allocation_pools,
dns_nameservers=dns_nameservers)
def test_create_subnet_with_too_many_dns(self):
with self.network() as network:
dns_list = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
data = {'subnet': {'network_id': network['network']['id'],
'cidr': '10.0.2.0/24',
'ip_version': 4,
'tenant_id': network['network']['tenant_id'],
'gateway_ip': '10.0.0.1',
'dns_nameservers': dns_list}}
subnet_req = self.new_create_request('subnets', data)
res = subnet_req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_create_subnet_with_one_host_route(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
allocation_pools = [{'start': '10.0.0.2',
'end': '10.0.0.100'}]
host_routes = [{'destination': '135.207.0.0/16',
'nexthop': '1.2.3.4'}]
self._test_create_subnet(gateway_ip=gateway_ip,
cidr=cidr,
allocation_pools=allocation_pools,
host_routes=host_routes)
def test_create_subnet_with_two_host_routes(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
allocation_pools = [{'start': '10.0.0.2',
'end': '10.0.0.100'}]
host_routes = [{'destination': '135.207.0.0/16',
'nexthop': '1.2.3.4'},
{'destination': '12.0.0.0/8',
'nexthop': '4.3.2.1'}]
self._test_create_subnet(gateway_ip=gateway_ip,
cidr=cidr,
allocation_pools=allocation_pools,
host_routes=host_routes)
def test_create_subnet_with_too_many_routes(self):
with self.network() as network:
host_routes = [{'destination': '135.207.0.0/16',
'nexthop': '1.2.3.4'},
{'destination': '12.0.0.0/8',
'nexthop': '4.3.2.1'},
{'destination': '141.212.0.0/16',
'nexthop': '2.2.2.2'}]
data = {'subnet': {'network_id': network['network']['id'],
'cidr': '10.0.2.0/24',
'ip_version': 4,
'tenant_id': network['network']['tenant_id'],
'gateway_ip': '10.0.0.1',
'host_routes': host_routes}}
subnet_req = self.new_create_request('subnets', data)
res = subnet_req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_update_subnet_dns(self):
with self.subnet() as subnet:
data = {'subnet': {'dns_nameservers': ['11.0.0.1']}}
req = self.new_update_request('subnets', data,
subnet['subnet']['id'])
res = self.deserialize('json', req.get_response(self.api))
self.assertEqual(res['subnet']['dns_nameservers'],
data['subnet']['dns_nameservers'])
def test_update_subnet_dns_with_too_many_entries(self):
with self.subnet() as subnet:
dns_list = ['1.1.1.1', '2.2.2.2', '3.3.3.3']
data = {'subnet': {'dns_nameservers': dns_list}}
req = self.new_update_request('subnets', data,
subnet['subnet']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_update_subnet_route(self):
with self.subnet() as subnet:
data = {'subnet': {'host_routes':
[{'destination': '12.0.0.0/8', 'nexthop': '1.2.3.4'}]}}
req = self.new_update_request('subnets', data,
subnet['subnet']['id'])
res = self.deserialize('json', req.get_response(self.api))
self.assertEqual(res['subnet']['host_routes'],
data['subnet']['host_routes'])
def test_update_subnet_route_with_too_many_entries(self):
with self.subnet() as subnet:
data = {'subnet': {'host_routes': [
{'destination': '12.0.0.0/8', 'nexthop': '1.2.3.4'},
{'destination': '13.0.0.0/8', 'nexthop': '1.2.3.5'},
{'destination': '14.0.0.0/8', 'nexthop': '1.2.3.6'}]}}
req = self.new_update_request('subnets', data,
subnet['subnet']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 400)
def test_delete_subnet_with_dns(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
fmt = 'json'
dns_nameservers = ['1.2.3.4']
# Create new network
res = self._create_network(fmt=fmt, name='net',
admin_status_up=True)
network = self.deserialize(fmt, res)
subnet = self._make_subnet(fmt, network, gateway_ip,
cidr, ip_version=4,
dns_nameservers=dns_nameservers)
req = self.new_delete_request('subnets', subnet['subnet']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 204)
def test_delete_subnet_with_route(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
fmt = 'json'
host_routes = [{'destination': '135.207.0.0/16',
'nexthop': '1.2.3.4'}]
# Create new network
res = self._create_network(fmt=fmt, name='net',
admin_status_up=True)
network = self.deserialize(fmt, res)
subnet = self._make_subnet(fmt, network, gateway_ip,
cidr, ip_version=4,
host_routes=host_routes)
req = self.new_delete_request('subnets', subnet['subnet']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 204)
def test_delete_subnet_with_dns_and_route(self):
gateway_ip = '10.0.0.1'
cidr = '10.0.0.0/24'
fmt = 'json'
dns_nameservers = ['1.2.3.4']
host_routes = [{'destination': '135.207.0.0/16',
'nexthop': '1.2.3.4'}]
# Create new network
res = self._create_network(fmt=fmt, name='net',
admin_status_up=True)
network = self.deserialize(fmt, res)
subnet = self._make_subnet(fmt, network, gateway_ip,
cidr, ip_version=4,
dns_nameservers=dns_nameservers,
host_routes=host_routes)
req = self.new_delete_request('subnets', subnet['subnet']['id'])
res = req.get_response(self.api)
self.assertEquals(res.status_int, 204)