Merge "Add ability for admins to reserve ip addresses"
This commit is contained in:
@@ -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.")
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user