implement the database models for the v2 db plugin base to store dns + additional routes
also handles the dns/route remote/update
Change-Id: I06c585b47668ee963324a5064e40a24471da28c4
'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},
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
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
"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.")
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()
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
'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):
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))
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'],
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)
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):
device_id = 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.
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):
# 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():
'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]
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,
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:
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
cidr,
allocation_pools,
ip_version,
- enable_dhcp)
+ enable_dhcp,
+ dns_nameservers,
+ host_routes)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
else:
cidr,
allocation_pools,
ip_version,
- enable_dhcp)
+ enable_dhcp,
+ dns_nameservers,
+ host_routes)
yield subnet
self._delete('subnets', subnet['subnet']['id'])
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)