From: xchenum Date: Sun, 12 Aug 2012 05:11:05 +0000 (-0400) Subject: implementation for bug 1008180 X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=1a9717a8cbd2fe405ccde25fa21214553deee635;p=openstack-build%2Fneutron-build.git implementation for bug 1008180 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 --- diff --git a/quantum/api/v2/attributes.py b/quantum/api/v2/attributes.py index c55caab12..80844f1e4 100644 --- a/quantum/api/v2/attributes.py +++ b/quantum/api/v2/attributes.py @@ -238,12 +238,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}, diff --git a/quantum/api/v2/base.py b/quantum/api/v2/base.py index e499fa906..3ff1ea8de 100644 --- a/quantum/api/v2/base.py +++ b/quantum/api/v2/base.py @@ -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 diff --git a/quantum/common/config.py b/quantum/common/config.py index 6b463f3de..7fe6e02d9 100644 --- a/quantum/common/config.py +++ b/quantum/common/config.py @@ -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 diff --git a/quantum/common/exceptions.py b/quantum/common/exceptions.py index b7f2a137b..aeb994b7e 100644 --- a/quantum/common/exceptions.py +++ b/quantum/common/exceptions.py @@ -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.") diff --git a/quantum/db/db_base_plugin_v2.py b/quantum/db/db_base_plugin_v2.py index 2f27bf2f5..229d9aae4 100644 --- a/quantum/db/db_base_plugin_v2.py +++ b/quantum/db/db_base_plugin_v2.py @@ -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): @@ -755,8 +788,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)) @@ -771,10 +833,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'], @@ -785,11 +862,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) @@ -802,6 +928,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): diff --git a/quantum/db/models_v2.py b/quantum/db/models_v2.py index b727e4881..e93f99d14 100644 --- a/quantum/db/models_v2.py +++ b/quantum/db/models_v2.py @@ -98,6 +98,25 @@ class Port(model_base.BASEV2, HasId, HasTenant): 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. @@ -113,10 +132,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): diff --git a/quantum/tests/unit/test_db_plugin.py b/quantum/tests/unit/test_db_plugin.py index 24c189527..021e52158 100644 --- a/quantum/tests/unit/test_db_plugin.py +++ b/quantum/tests/unit/test_db_plugin.py @@ -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] @@ -258,7 +261,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, @@ -266,7 +270,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: @@ -330,7 +336,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 @@ -342,7 +350,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: @@ -352,7 +362,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']) @@ -1717,3 +1729,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)