prevent invalid deletion of ports using by L3 devices
bug 1039947 The bug noticed that an admin user could delete a port that stored the underlying IP allocation for a floating IP. This patch prevents the direction deletion of ports via the API for ports that are used as router interfaces, router gateways, of for floating IPs. Add a unit test to check such an invalid delete, and also updates unit tests to avoid them tripping over the new checks. Change-Id: Ief28e3181583428d55259275a7c21151a4a4fa9b
This commit is contained in:
parent
97e7df67ec
commit
54bc60850f
@ -153,7 +153,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
router.update({'gw_port_id': None})
|
router.update({'gw_port_id': None})
|
||||||
context.session.add(router)
|
context.session.add(router)
|
||||||
self.delete_port(context, gw_port['id'])
|
self.delete_port(context, gw_port['id'], l3_port_check=False)
|
||||||
|
|
||||||
if network_id is not None and (gw_port is None or
|
if network_id is not None and (gw_port is None or
|
||||||
gw_port['network_id'] != network_id):
|
gw_port['network_id'] != network_id):
|
||||||
@ -169,7 +169,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
'name': ''}})
|
'name': ''}})
|
||||||
|
|
||||||
if not len(gw_port['fixed_ips']):
|
if not len(gw_port['fixed_ips']):
|
||||||
self.delete_port(context, gw_port['id'])
|
self.delete_port(context, gw_port['id'], l3_port_check=False)
|
||||||
msg = ('No IPs available for external network %s' %
|
msg = ('No IPs available for external network %s' %
|
||||||
network_id)
|
network_id)
|
||||||
raise q_exc.BadRequest(resource='router', msg=msg)
|
raise q_exc.BadRequest(resource='router', msg=msg)
|
||||||
@ -244,6 +244,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
self._check_for_dup_router_subnet(context, router_id,
|
self._check_for_dup_router_subnet(context, router_id,
|
||||||
port['network_id'],
|
port['network_id'],
|
||||||
fixed_ips[0]['subnet_id'])
|
fixed_ips[0]['subnet_id'])
|
||||||
|
with context.session.begin(subtransactions=True):
|
||||||
port.update({'device_id': router_id,
|
port.update({'device_id': router_id,
|
||||||
'device_owner': DEVICE_OWNER_ROUTER_INTF})
|
'device_owner': DEVICE_OWNER_ROUTER_INTF})
|
||||||
elif 'subnet_id' in interface_info:
|
elif 'subnet_id' in interface_info:
|
||||||
@ -277,7 +278,13 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
msg = "Either subnet_id or port_id must be specified"
|
msg = "Either subnet_id or port_id must be specified"
|
||||||
raise q_exc.BadRequest(resource='router', msg=msg)
|
raise q_exc.BadRequest(resource='router', msg=msg)
|
||||||
if 'port_id' in interface_info:
|
if 'port_id' in interface_info:
|
||||||
port_db = self._get_port(context, interface_info['port_id'])
|
port_id = interface_info['port_id']
|
||||||
|
port_db = self._get_port(context, port_id)
|
||||||
|
if not (port_db['device_owner'] == DEVICE_OWNER_ROUTER_INTF and
|
||||||
|
port_db['device_id'] == router_id):
|
||||||
|
raise w_exc.HTTPNotFound("Router %(router_id)s does not have "
|
||||||
|
" an interface with id %(port_id)s"
|
||||||
|
% locals())
|
||||||
if 'subnet_id' in interface_info:
|
if 'subnet_id' in interface_info:
|
||||||
port_subnet_id = port_db['fixed_ips'][0]['subnet_id']
|
port_subnet_id = port_db['fixed_ips'][0]['subnet_id']
|
||||||
if port_subnet_id != interface_info['subnet_id']:
|
if port_subnet_id != interface_info['subnet_id']:
|
||||||
@ -288,7 +295,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
if port_db['device_id'] != router_id:
|
if port_db['device_id'] != router_id:
|
||||||
raise w_exc.HTTPConflict("port_id %s not used by router" %
|
raise w_exc.HTTPConflict("port_id %s not used by router" %
|
||||||
port_db['id'])
|
port_db['id'])
|
||||||
self.delete_port(context, port_db['id'])
|
self.delete_port(context, port_db['id'], l3_port_check=False)
|
||||||
elif 'subnet_id' in interface_info:
|
elif 'subnet_id' in interface_info:
|
||||||
subnet_id = interface_info['subnet_id']
|
subnet_id = interface_info['subnet_id']
|
||||||
subnet = self._get_subnet(context, subnet_id)
|
subnet = self._get_subnet(context, subnet_id)
|
||||||
@ -303,7 +310,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
|
|
||||||
for p in ports:
|
for p in ports:
|
||||||
if p['fixed_ips'][0]['subnet_id'] == subnet_id:
|
if p['fixed_ips'][0]['subnet_id'] == subnet_id:
|
||||||
self.delete_port(context, p['id'])
|
self.delete_port(context, p['id'], l3_port_check=False)
|
||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
except exc.NoResultFound:
|
except exc.NoResultFound:
|
||||||
@ -461,7 +468,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
if not external_port['fixed_ips']:
|
if not external_port['fixed_ips']:
|
||||||
msg = "Unable to find any IP address on external network"
|
msg = "Unable to find any IP address on external network"
|
||||||
# remove the external port
|
# remove the external port
|
||||||
self.delete_port(context, external_port['id'])
|
self.delete_port(context, external_port['id'], l3_port_check=False)
|
||||||
raise q_exc.BadRequest(resource='floatingip', msg=msg)
|
raise q_exc.BadRequest(resource='floatingip', msg=msg)
|
||||||
|
|
||||||
floating_ip_address = external_port['fixed_ips'][0]['ip_address']
|
floating_ip_address = external_port['fixed_ips'][0]['ip_address']
|
||||||
@ -484,7 +491,7 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Floating IP association failed")
|
LOG.exception("Floating IP association failed")
|
||||||
# Remove the port created for internal purposes
|
# Remove the port created for internal purposes
|
||||||
self.delete_port(context, external_port['id'])
|
self.delete_port(context, external_port['id'], l3_port_check=False)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return self._make_floatingip_dict(floatingip_db)
|
return self._make_floatingip_dict(floatingip_db)
|
||||||
@ -504,7 +511,8 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
floatingip = self._get_floatingip(context, id)
|
floatingip = self._get_floatingip(context, id)
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
context.session.delete(floatingip)
|
context.session.delete(floatingip)
|
||||||
self.delete_port(context, floatingip['floating_port_id'])
|
self.delete_port(context, floatingip['floating_port_id'],
|
||||||
|
l3_port_check=False)
|
||||||
|
|
||||||
def get_floatingip(self, context, id, fields=None):
|
def get_floatingip(self, context, id, fields=None):
|
||||||
floatingip = self._get_floatingip(context, id)
|
floatingip = self._get_floatingip(context, id)
|
||||||
@ -515,6 +523,21 @@ class L3_NAT_db_mixin(l3.RouterPluginBase):
|
|||||||
self._make_floatingip_dict,
|
self._make_floatingip_dict,
|
||||||
filters=filters, fields=fields)
|
filters=filters, fields=fields)
|
||||||
|
|
||||||
|
def prevent_l3_port_deletion(self, context, port_id):
|
||||||
|
""" Checks to make sure a port is allowed to be deleted, raising
|
||||||
|
an exception if this is not the case. This should be called by
|
||||||
|
any plugin when the API requests the deletion of a port, since
|
||||||
|
some ports for L3 are not intended to be deleted directly via a
|
||||||
|
DELETE to /ports, but rather via other API calls that perform the
|
||||||
|
proper deletion checks.
|
||||||
|
"""
|
||||||
|
port_db = self._get_port(context, port_id)
|
||||||
|
if port_db['device_owner'] in [DEVICE_OWNER_ROUTER_INTF,
|
||||||
|
DEVICE_OWNER_ROUTER_GW,
|
||||||
|
DEVICE_OWNER_FLOATINGIP]:
|
||||||
|
raise l3.L3PortInUse(port_id=port_id,
|
||||||
|
device_owner=port_db['device_owner'])
|
||||||
|
|
||||||
def disassociate_floatingips(self, context, port_id):
|
def disassociate_floatingips(self, context, port_id):
|
||||||
with context.session.begin(subtransactions=True):
|
with context.session.begin(subtransactions=True):
|
||||||
try:
|
try:
|
||||||
|
@ -50,7 +50,13 @@ class ExternalGatewayForFloatingIPNotFound(qexception.NotFound):
|
|||||||
|
|
||||||
|
|
||||||
class FloatingIPPortAlreadyAssociated(qexception.InUse):
|
class FloatingIPPortAlreadyAssociated(qexception.InUse):
|
||||||
message = _("Port %(port_id) already has a floating IP associated with it")
|
message = _("Port %(port_id)s already has a floating IP"
|
||||||
|
" associated with it")
|
||||||
|
|
||||||
|
|
||||||
|
class L3PortInUse(qexception.InUse):
|
||||||
|
message = _("Port %(port_id)s has owner %(device_owner)s and therefore"
|
||||||
|
" cannot be deleted directly via the port API.")
|
||||||
|
|
||||||
|
|
||||||
def _validate_uuid_or_none(data, valid_values=None):
|
def _validate_uuid_or_none(data, valid_values=None):
|
||||||
|
@ -357,6 +357,11 @@ class LinuxBridgePluginV2(db_base_plugin_v2.QuantumDbPluginV2,
|
|||||||
binding.vlan_id)
|
binding.vlan_id)
|
||||||
return port
|
return port
|
||||||
|
|
||||||
def delete_port(self, context, id):
|
def delete_port(self, context, id, l3_port_check=True):
|
||||||
|
|
||||||
|
# if needed, check to see if this is a port owned by
|
||||||
|
# and l3-router. If so, we should prevent deletion.
|
||||||
|
if l3_port_check:
|
||||||
|
self.prevent_l3_port_deletion(context, id)
|
||||||
self.disassociate_floatingips(context, id)
|
self.disassociate_floatingips(context, id)
|
||||||
return super(LinuxBridgePluginV2, self).delete_port(context, id)
|
return super(LinuxBridgePluginV2, self).delete_port(context, id)
|
||||||
|
@ -423,6 +423,11 @@ class OVSQuantumPluginV2(db_base_plugin_v2.QuantumDbPluginV2,
|
|||||||
binding.physical_id)
|
binding.physical_id)
|
||||||
return port
|
return port
|
||||||
|
|
||||||
def delete_port(self, context, id):
|
def delete_port(self, context, id, l3_port_check=True):
|
||||||
|
|
||||||
|
# if needed, check to see if this is a port owned by
|
||||||
|
# and l3-router. If so, we should prevent deletion.
|
||||||
|
if l3_port_check:
|
||||||
|
self.prevent_l3_port_deletion(context, id)
|
||||||
self.disassociate_floatingips(context, id)
|
self.disassociate_floatingips(context, id)
|
||||||
return super(OVSQuantumPluginV2, self).delete_port(context, id)
|
return super(OVSQuantumPluginV2, self).delete_port(context, id)
|
||||||
|
@ -432,19 +432,22 @@ class QuantumDbPluginV2TestCase(unittest2.TestCase):
|
|||||||
self._delete('subnets', subnet['subnet']['id'])
|
self._delete('subnets', subnet['subnet']['id'])
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def port(self, subnet=None, fixed_ips=None, fmt='json', **kwargs):
|
def port(self, subnet=None, fixed_ips=None, fmt='json', no_delete=False,
|
||||||
|
**kwargs):
|
||||||
if not subnet:
|
if not subnet:
|
||||||
with self.subnet() as subnet:
|
with self.subnet() as subnet:
|
||||||
net_id = subnet['subnet']['network_id']
|
net_id = subnet['subnet']['network_id']
|
||||||
port = self._make_port(fmt, net_id, fixed_ips=fixed_ips,
|
port = self._make_port(fmt, net_id, fixed_ips=fixed_ips,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
yield port
|
yield port
|
||||||
|
if not no_delete:
|
||||||
self._delete('ports', port['port']['id'])
|
self._delete('ports', port['port']['id'])
|
||||||
else:
|
else:
|
||||||
net_id = subnet['subnet']['network_id']
|
net_id = subnet['subnet']['network_id']
|
||||||
port = self._make_port(fmt, net_id, fixed_ips=fixed_ips,
|
port = self._make_port(fmt, net_id, fixed_ips=fixed_ips,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
yield port
|
yield port
|
||||||
|
if not no_delete:
|
||||||
self._delete('ports', port['port']['id'])
|
self._delete('ports', port['port']['id'])
|
||||||
|
|
||||||
|
|
||||||
|
@ -220,7 +220,9 @@ class TestL3NatPlugin(db_base_plugin_v2.QuantumDbPluginV2,
|
|||||||
l3_db.L3_NAT_db_mixin):
|
l3_db.L3_NAT_db_mixin):
|
||||||
supported_extension_aliases = ["os-quantum-router"]
|
supported_extension_aliases = ["os-quantum-router"]
|
||||||
|
|
||||||
def delete_port(self, context, id):
|
def delete_port(self, context, id, l3_port_check=True):
|
||||||
|
if l3_port_check:
|
||||||
|
self.prevent_l3_port_deletion(context, id)
|
||||||
self.disassociate_floatingips(context, id)
|
self.disassociate_floatingips(context, id)
|
||||||
return super(TestL3NatPlugin, self).delete_port(context, id)
|
return super(TestL3NatPlugin, self).delete_port(context, id)
|
||||||
|
|
||||||
@ -332,7 +334,7 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
|
|
||||||
def test_router_add_interface_port(self):
|
def test_router_add_interface_port(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
with self.port() as p:
|
with self.port(no_delete=True) as p:
|
||||||
body = self._router_interface_action('add',
|
body = self._router_interface_action('add',
|
||||||
r['router']['id'],
|
r['router']['id'],
|
||||||
None,
|
None,
|
||||||
@ -344,6 +346,12 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
body = self._show('ports', p['port']['id'])
|
body = self._show('ports', p['port']['id'])
|
||||||
self.assertEquals(body['port']['device_id'], r['router']['id'])
|
self.assertEquals(body['port']['device_id'], r['router']['id'])
|
||||||
|
|
||||||
|
# clean-up
|
||||||
|
self._router_interface_action('remove',
|
||||||
|
r['router']['id'],
|
||||||
|
None,
|
||||||
|
p['port']['id'])
|
||||||
|
|
||||||
def test_router_add_interface_dup_subnet1(self):
|
def test_router_add_interface_dup_subnet1(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
with self.subnet() as s:
|
with self.subnet() as s:
|
||||||
@ -365,7 +373,7 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
def test_router_add_interface_dup_subnet2(self):
|
def test_router_add_interface_dup_subnet2(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
with self.subnet() as s:
|
with self.subnet() as s:
|
||||||
with self.port(subnet=s) as p1:
|
with self.port(subnet=s, no_delete=True) as p1:
|
||||||
with self.port(subnet=s) as p2:
|
with self.port(subnet=s) as p2:
|
||||||
self._router_interface_action('add',
|
self._router_interface_action('add',
|
||||||
r['router']['id'],
|
r['router']['id'],
|
||||||
@ -377,6 +385,11 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
p2['port']['id'],
|
p2['port']['id'],
|
||||||
expected_code=
|
expected_code=
|
||||||
exc.HTTPBadRequest.code)
|
exc.HTTPBadRequest.code)
|
||||||
|
# clean-up
|
||||||
|
self._router_interface_action('remove',
|
||||||
|
r['router']['id'],
|
||||||
|
None,
|
||||||
|
p1['port']['id'])
|
||||||
|
|
||||||
def test_router_add_interface_no_data(self):
|
def test_router_add_interface_no_data(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
@ -435,7 +448,7 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
def test_router_remove_router_interface_wrong_subnet_returns_409(self):
|
def test_router_remove_router_interface_wrong_subnet_returns_409(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
with self.subnet() as s:
|
with self.subnet() as s:
|
||||||
with self.port() as p:
|
with self.port(no_delete=True) as p:
|
||||||
self._router_interface_action('add',
|
self._router_interface_action('add',
|
||||||
r['router']['id'],
|
r['router']['id'],
|
||||||
None,
|
None,
|
||||||
@ -445,11 +458,16 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
s['subnet']['id'],
|
s['subnet']['id'],
|
||||||
p['port']['id'],
|
p['port']['id'],
|
||||||
exc.HTTPConflict.code)
|
exc.HTTPConflict.code)
|
||||||
|
#remove properly to clean-up
|
||||||
|
self._router_interface_action('remove',
|
||||||
|
r['router']['id'],
|
||||||
|
None,
|
||||||
|
p['port']['id'])
|
||||||
|
|
||||||
def test_router_remove_router_interface_wrong_port_returns_409(self):
|
def test_router_remove_router_interface_wrong_port_returns_404(self):
|
||||||
with self.router() as r:
|
with self.router() as r:
|
||||||
with self.subnet() as s:
|
with self.subnet() as s:
|
||||||
with self.port() as p:
|
with self.port(no_delete=True) as p:
|
||||||
self._router_interface_action('add',
|
self._router_interface_action('add',
|
||||||
r['router']['id'],
|
r['router']['id'],
|
||||||
None,
|
None,
|
||||||
@ -461,7 +479,12 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
r['router']['id'],
|
r['router']['id'],
|
||||||
None,
|
None,
|
||||||
p2['port']['id'],
|
p2['port']['id'],
|
||||||
exc.HTTPConflict.code)
|
exc.HTTPNotFound.code)
|
||||||
|
# remove correct interface to cleanup
|
||||||
|
self._router_interface_action('remove',
|
||||||
|
r['router']['id'],
|
||||||
|
None,
|
||||||
|
p['port']['id'])
|
||||||
# remove extra port created
|
# remove extra port created
|
||||||
self._delete('ports', p2['port']['id'])
|
self._delete('ports', p2['port']['id'])
|
||||||
|
|
||||||
@ -608,6 +631,16 @@ class L3NatDBTestCase(test_db_plugin.QuantumDbPluginV2TestCase):
|
|||||||
{'port_id': port_id}},
|
{'port_id': port_id}},
|
||||||
expected_code=exc.HTTPConflict.code)
|
expected_code=exc.HTTPConflict.code)
|
||||||
|
|
||||||
|
def test_floating_ip_direct_port_delete_returns_409(self):
|
||||||
|
found = False
|
||||||
|
with self.floatingip_with_assoc() as fip:
|
||||||
|
for p in self._list('ports')['ports']:
|
||||||
|
if p['device_owner'] == 'network:floatingip':
|
||||||
|
self._delete('ports', p['id'],
|
||||||
|
expected_code=exc.HTTPConflict.code)
|
||||||
|
found = True
|
||||||
|
self.assertTrue(found)
|
||||||
|
|
||||||
def test_create_floatingip_no_ext_gateway_return_404(self):
|
def test_create_floatingip_no_ext_gateway_return_404(self):
|
||||||
with self.subnet() as public_sub:
|
with self.subnet() as public_sub:
|
||||||
with self.port() as private_port:
|
with self.port() as private_port:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user