Merge "Add ability for admins to reserve ip addresses"

This commit is contained in:
Jenkins
2016-08-12 19:55:41 +00:00
committed by Gerrit Code Review
4 changed files with 367 additions and 32 deletions

View File

@@ -15,6 +15,10 @@
from neutron_lib import exceptions as n_exc
class ActionNotAuthorized(n_exc.NotAuthorized):
message = _("Action Forbidden: %(msg)")
class NetworkAlreadyExists(n_exc.Conflict):
message = _("Network %(id)s already exists.")

View File

@@ -74,20 +74,40 @@ def get_ip_addresses(context, **filters):
filters = {}
if 'type' in filters:
filters['address_type'] = filters['type']
filters["_deallocated"] = False
if context.is_admin:
if 'deallocated' in filters:
if filters['deallocated'] == 'True':
filters["_deallocated"] = True
elif filters['deallocated'] == 'False':
filters["_deallocated"] = False
elif filters['deallocated'].lower() == 'both':
pass
else:
filters['_deallocated'] = False
else:
filters['_deallocated'] = False
else:
filters["_deallocated"] = False
if 'deallocated' in filters:
# this needs to be removed or it corrupts the filters passed to the db
# model. Only _deallocated needs to be present
del filters['deallocated']
addrs = db_api.ip_address_find(context, scope=db_api.ALL, **filters)
return [v._make_ip_dict(ip) for ip in addrs]
return [v._make_ip_dict(ip, context.is_admin) for ip in addrs]
def get_ip_address(context, id):
LOG.info("get_ip_address %s for tenant %s" %
(id, context.tenant_id))
filters = {}
filters["_deallocated"] = False
if not context.is_admin:
filters["_deallocated"] = False
addr = db_api.ip_address_find(context, id=id, scope=db_api.ONE, **filters)
if not addr:
raise q_exc.IpAddressNotFound(addr_id=id)
return v._make_ip_dict(addr)
return v._make_ip_dict(addr, context.is_admin)
def validate_and_fetch_segment(ports, network_id):
@@ -239,7 +259,7 @@ def create_ip_address(context, body):
with context.session.begin():
address = new_addresses[0]
new_address = db_api.port_associate_ip(context, ports, address)
return v._make_ip_dict(new_address)
return v._make_ip_dict(new_address, context.is_admin)
def _get_deallocated_override():
@@ -264,20 +284,20 @@ def update_ip_address(context, id, ip_address):
raise n_exc.BadRequest(resource="ip_addresses",
msg="Invalid request body.")
with context.session.begin():
address = db_api.ip_address_find(context, id=id, scope=db_api.ONE)
if not address:
db_address = db_api.ip_address_find(context, id=id, scope=db_api.ONE)
if not db_address:
raise q_exc.IpAddressNotFound(addr_id=id)
iptype = address.address_type
iptype = db_address.address_type
if iptype == ip_types.FIXED and not CONF.QUARK.ipaddr_allow_fixed_ip:
raise n_exc.BadRequest(
resource="ip_addresses",
msg="Fixed ips cannot be updated using this interface.")
reset = ip_address['ip_address'].get('reset_allocation_time', False)
if reset and address['deallocated'] == 1:
if reset and db_address['deallocated'] == 1:
if context.is_admin:
LOG.info("IP's deallocated time being manually reset")
address['deallocated_at'] = _get_deallocated_override()
db_address['deallocated_at'] = _get_deallocated_override()
else:
msg = "Modification of reset_allocation_time requires admin"
raise webob.exc.HTTPForbidden(detail=msg)
@@ -290,7 +310,7 @@ def update_ip_address(context, id, ip_address):
msg="Cannot be updated with empty port_id list")
if iptype == ip_types.SHARED:
has_owner = address.has_any_shared_owner()
has_owner = db_address.has_any_shared_owner()
if port_ids:
if iptype == ip_types.FIXED and len(port_ids) > 1:
@@ -298,7 +318,7 @@ def update_ip_address(context, id, ip_address):
resource="ip_addresses",
msg="Fixed ips cannot be updated with more than one port.")
_raise_if_shared_and_enabled(ip_address, address)
_raise_if_shared_and_enabled(ip_address, db_address)
ports = db_api.port_find(context, tenant_id=context.tenant_id,
id=port_ids, scope=db_api.ALL)
# NOTE(name): could be considered inefficient because we're
@@ -306,27 +326,45 @@ def update_ip_address(context, id, ip_address):
if len(ports) != len(port_ids):
raise n_exc.PortNotFound(port_id=port_ids)
validate_and_fetch_segment(ports, address["network_id"])
validate_port_ip_quotas(context, address.network_id, ports)
validate_and_fetch_segment(ports, db_address["network_id"])
validate_port_ip_quotas(context, db_address.network_id, ports)
if iptype == ip_types.SHARED and has_owner:
for assoc in address.associations:
for assoc in db_address.associations:
pid = assoc.port_id
if pid not in port_ids and 'none' != assoc.service:
raise q_exc.PortRequiresDisassociation()
LOG.info("Updating IP address, %s, to only be used by the"
"following ports: %s" % (address.address_readable,
"following ports: %s" % (db_address.address_readable,
[p.id for p in ports]))
new_address = db_api.update_port_associations_for_ip(context,
ports,
address)
db_address)
elif iptype == ip_types.SHARED and has_owner:
raise q_exc.PortRequiresDisassociation()
elif 'deallocated' in ip_address['ip_address']\
and context.is_admin:
# Verify no port associations
if len(db_address.associations) != 0:
exc_msg = ("IP %s cannot be deallocated or allocated while"
" still associated with ports: %s"
% (db_address['address_readable'],
db_address.associations))
raise q_exc.ActionNotAuthorized(msg=exc_msg)
# NOTE: If an admin, allow a user to set deallocated to false
# in order to reserve a deallocated IP. Alternatively, allow them
# reverse that choice if a mistake was made.
if ip_address['ip_address']['deallocated'] == 'False':
db_address['deallocated'] = False
else:
db_address['deallocated'] = True
return v._make_ip_dict(db_address, context.is_admin)
else:
ipam_driver.deallocate_ip_address(context, address)
return v._make_ip_dict(address)
return v._make_ip_dict(new_address)
ipam_driver.deallocate_ip_address(context, db_address)
return v._make_ip_dict(db_address, context.is_admin)
return v._make_ip_dict(new_address, context.is_admin)
def delete_ip_address(context, id):

View File

@@ -302,17 +302,20 @@ def _make_route_dict(route):
"subnet_id": route["subnet_id"]}
def _make_ip_dict(address):
return {"id": address["id"],
"network_id": address["network_id"],
"ip_address": address.formatted(),
"address": address.formatted(),
"port_ids": [assoc.port_id
for assoc in address["associations"]],
"subnet_id": address["subnet_id"],
"tenant_id": address["used_by_tenant_id"],
"version": address["version"],
"type": address['address_type']}
def _make_ip_dict(address, is_admin=False):
ip_view = {"id": address["id"],
"network_id": address["network_id"],
"ip_address": address.formatted(),
"address": address.formatted(),
"port_ids": [assoc.port_id
for assoc in address["associations"]],
"subnet_id": address["subnet_id"],
"tenant_id": address["used_by_tenant_id"],
"version": address["version"],
"type": address['address_type']}
if is_admin:
ip_view["_deallocated"] = address['_deallocated']
return ip_view
def _make_ip_policy_dict(ipp):

View File

@@ -14,10 +14,18 @@
# under the License.
import contextlib
import mock
import netaddr
from neutron_lib import exceptions as n_exc
from oslo_config import cfg
from quark.db import api as db_api
from quark import exceptions as q_exc
from quark.plugin_modules import ip_addresses as ip_addr
import quark.plugin_modules.mac_address_ranges as macrng_api
import quark.plugin_modules.networks as network_api
import quark.plugin_modules.ports as port_api
from quark.tests.functional.base import BaseFunctionalTest
@@ -71,3 +79,285 @@ class QuarkGetIPAddresses(BaseFunctionalTest):
ip_address=[netaddr.IPAddress("192.168.10.2")],
scope=db_api.ALL)
self.assertEqual(len(ip_addresses), 0)
class QuarkTestReserveIPAdmin(BaseFunctionalTest):
def setUp(self):
super(QuarkTestReserveIPAdmin, self).setUp()
self.addr = netaddr.IPAddress("192.168.10.1")
self.ip_address_dealloc = {"ip_address": {"blah": "False"}}
self.ip_address_reserve = {"ip_address": {"deallocated": "False"}}
@contextlib.contextmanager
def _stubs(self):
with self.context.session.begin():
subnet = db_api.subnet_create(self.context,
cidr="192.168.0.0/24")
ip = db_api.ip_address_create(self.context,
address=self.addr,
subnet_id=subnet["id"])
yield ip
def test_reserve_ip_non_admin(self):
with self._stubs() as ip:
deallocated_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=deallocated_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
deallocated_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_reserve)
ip_address = db_api.ip_address_find(
self.context,
id=deallocated_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
def test_reserve_ip_admin(self):
self.context.is_admin = True
with self._stubs() as ip:
deallocated_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=deallocated_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
deallocated_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_reserve)
ip_address = db_api.ip_address_find(
self.context,
id=deallocated_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], False)
class QuarkTestReserveIPAdminWithPorts(BaseFunctionalTest):
def setUp(self):
super(QuarkTestReserveIPAdminWithPorts, self).setUp()
self.addr = netaddr.IPAddress("192.168.10.1")
self.ip_address_dealloc = {"ip_address": {"deallocated": "True"}}
@contextlib.contextmanager
def _stubs(self):
with contextlib.nested(
mock.patch("neutron.common.rpc.get_notifier"),
mock.patch("neutron.quota.QUOTAS.limit_check")):
mac = {'mac_address_range': dict(cidr="AA:BB:CC")}
self.context.is_admin = True
macrng_api.create_mac_address_range(self.context, mac)
self.context.is_admin = False
network_info = dict(name="public", tenant_id="fake",
network_plugin="BASE",
ipam_strategy="ANY")
network_info = {"network": network_info}
network = network_api.create_network(self.context, network_info)
subnet = db_api.subnet_create(self.context, tenant_id="fake",
cidr="192.168.10.0/24",
network_id=network['id'])
fixed_ips = [dict(subnet_id=subnet['id'], enabled=True,
ip_address=self.addr)]
port = dict(port=dict(network_id=network['id'],
tenant_id=self.context.tenant_id,
device_id=2,
fixed_ips=fixed_ips))
port_api.create_port(self.context, port)
self.context.is_admin = True
filters = {"deallocated": "both"}
ip = ip_addr.get_ip_addresses(self.context, **filters)
self.context.is_admin = False
yield ip[0]
def test_reserve_ip_admin_port_assoc_raises_not_authorized(self):
# This value prevents the testing of the exception thrown if an admin
# attempts to deallocate/allocate an IP associated with a port since
# an exception is thrown beforehand.
old_override = cfg.CONF.QUARK.ipaddr_allow_fixed_ip
cfg.CONF.set_override('ipaddr_allow_fixed_ip', True, 'QUARK')
with self._stubs() as ip:
with self.assertRaises(q_exc.ActionNotAuthorized):
self.context.is_admin = True
ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
cfg.CONF.set_override('ipaddr_allow_fixed_ip', old_override, 'QUARK')
def test_reserve_ip_admin_port_assoc_raises_bad_request(self):
with self._stubs() as ip:
with self.assertRaises(n_exc.BadRequest):
self.context.is_admin = True
ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
def test_reserve_ip_non_admin_port_assoc_raises_bad_request(self):
with self._stubs() as ip:
with self.assertRaises(n_exc.BadRequest):
ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
class QuarkTestGetDeallocatedIP(BaseFunctionalTest):
def setUp(self):
super(QuarkTestGetDeallocatedIP, self).setUp()
self.addr = netaddr.IPAddress("192.168.10.1")
self.ip_address_dealloc = {"ip_address": {"this": "deallocates"}}
@contextlib.contextmanager
def _stubs(self):
with self.context.session.begin():
subnet = db_api.subnet_create(self.context,
cidr="192.168.0.0/24")
ip = db_api.ip_address_create(self.context,
address=self.addr,
subnet_id=subnet["id"])
yield ip
def test_get_single_deallocated_ip_non_admin_raises(self):
with self._stubs() as ip:
reserved_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=reserved_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
with self.assertRaises(q_exc.IpAddressNotFound):
ip_addr.get_ip_address(self.context,
ip_address['id'])
def test_get_deallocated_ips_non_admin_empty(self):
with self._stubs() as ip:
reserved_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=reserved_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
deallocated_ips = ip_addr.get_ip_addresses(self.context)
self.assertEqual(len(deallocated_ips), 0)
def test_get_single_deallocated_ip_admin(self):
self.context.is_admin = True
with self._stubs() as ip:
reserved_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=reserved_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
deallocated_ip = ip_addr.get_ip_address(self.context,
ip_address['id'])
self.assertEqual(reserved_ip['id'], deallocated_ip['id'])
self.assertEqual(deallocated_ip['_deallocated'], True)
def test_get_deallocated_ips_admin(self):
self.context.is_admin = True
with self._stubs() as ip:
reserved_ip = ip_addr.update_ip_address(self.context, ip["id"],
self.ip_address_dealloc)
ip_address = db_api.ip_address_find(
self.context,
id=reserved_ip["id"],
scope=db_api.ONE)
self.assertEqual(ip_address["deallocated"], True)
filters = {'deallocated': 'True'}
deallocated_ips = ip_addr.get_ip_addresses(self.context, **filters)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(reserved_ip['id'], deallocated_ips[0]['id'])
self.assertEqual(deallocated_ips[0]['_deallocated'], True)
class QuarkTestGetMultipleDeallocatedIPs(BaseFunctionalTest):
def setUp(self):
super(QuarkTestGetMultipleDeallocatedIPs, self).setUp()
self.addr1 = netaddr.IPAddress("192.168.10.1")
self.addr2 = netaddr.IPAddress("192.168.10.2")
self.ip_address_dealloc = {"ip_address": {"this": "deallocates"}}
@contextlib.contextmanager
def _stubs(self):
with self.context.session.begin():
subnet = db_api.subnet_create(self.context,
cidr="192.168.0.0/24")
ip1 = db_api.ip_address_create(self.context,
address=self.addr1,
subnet_id=subnet["id"])
ip2 = db_api.ip_address_create(self.context,
address=self.addr2,
subnet_id=subnet["id"])
yield ip1, ip2
def test_get_deallocated_ips_admin_both(self):
self.context.is_admin = True
with self._stubs() as (ip1, ip2):
reserved_ip = ip_addr.update_ip_address(self.context, ip2["id"],
self.ip_address_dealloc)
self.assertEqual(reserved_ip["_deallocated"], True)
deallocated_ips = ip_addr.get_ip_addresses(self.context)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(ip1['id'], deallocated_ips[0]['id'])
self.assertEqual(deallocated_ips[0]['_deallocated'], False)
filters = {'deallocated': 'True'}
deallocated_ips = ip_addr.get_ip_addresses(self.context, **filters)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(reserved_ip['id'], deallocated_ips[0]['id'])
self.assertEqual(deallocated_ips[0]['_deallocated'], True)
filters = {'deallocated': 'False'}
deallocated_ips = ip_addr.get_ip_addresses(self.context, **filters)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(ip1['id'], deallocated_ips[0]['id'])
self.assertEqual(deallocated_ips[0]['_deallocated'], False)
filters = {'deallocated': 'both'}
deallocated_ips = ip_addr.get_ip_addresses(self.context, **filters)
self.assertEqual(len(deallocated_ips), 2)
for ip in deallocated_ips:
if ip["id"] == ip1["id"]:
self.assertEqual(ip["_deallocated"], False)
elif ip["id"] == ip2["id"]:
self.assertEqual(ip["_deallocated"], True)
def test_get_deallocated_ips_non_admin_both(self):
with self._stubs() as (ip1, ip2):
reserved_ip = ip_addr.update_ip_address(self.context, ip2["id"],
self.ip_address_dealloc)
self.assertNotIn('_deallocated', reserved_ip)
ip_addresses = db_api.ip_address_find(
self.context,
scope=db_api.ALL)
self.assertEqual(len(ip_addresses), 2)
for ip in ip_addresses:
if ip["id"] == ip1["id"]:
self.assertEqual(ip["_deallocated"], False)
elif ip["id"] == ip2["id"]:
self.assertEqual(ip["_deallocated"], True)
deallocated_ips = ip_addr.get_ip_addresses(self.context)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(ip1['id'], deallocated_ips[0]['id'])
self.assertNotIn('_deallocated', deallocated_ips[0])
filters = {'deallocated': 'True'}
deallocated_ips1 = ip_addr.get_ip_addresses(self.context,
**filters)
self.assertEqual(len(deallocated_ips1), 1)
filters = {'deallocated': 'False'}
deallocated_ips1 = ip_addr.get_ip_addresses(self.context,
**filters)
self.assertEqual(len(deallocated_ips1), 1)
filters = {'deallocated': 'both'}
deallocated_ips = ip_addr.get_ip_addresses(self.context, **filters)
self.assertEqual(len(deallocated_ips), 1)
self.assertEqual(ip1['id'], deallocated_ips[0]['id'])
self.assertNotIn('_deallocated', deallocated_ips[0])