diff --git a/quark/db/api.py b/quark/db/api.py index 8ed4373..85b7e67 100644 --- a/quark/db/api.py +++ b/quark/db/api.py @@ -319,7 +319,7 @@ def ip_address_find(context, lock_mode=False, **filters): return query.filter(*model_filters) -def ip_address_count_all(context, **filters): +def ip_address_count_all(context, filters): query = context.session.query(sql_func.count(models.IPAddress.id)) model_filters = _model_query(context, models.IPAddress, filters) return query.filter(*model_filters).scalar() @@ -892,3 +892,14 @@ def floating_ip_find(context, lock_mode=False, limit=None, sorts=None, return paginate_query(query.filter(*model_filters), models.IPAddress, limit, sorts, marker) + + +def floating_ip_associate_fixed_ip(context, floating_ip, fixed_ip, + enable=True): + assoc = models.FloatingToFixedIPAssociation() + assoc.floating_ip_address_id = floating_ip.id + assoc.fixed_ip_address_id = fixed_ip.id + assoc.enabled = enable + context.session.add(assoc) + floating_ip.fixed_ip = fixed_ip + return floating_ip diff --git a/quark/db/migration/alembic/versions/33e9e23ba761_add_floating_to_fixed_ip_mapping_table.py b/quark/db/migration/alembic/versions/33e9e23ba761_add_floating_to_fixed_ip_mapping_table.py new file mode 100644 index 0000000..ca787f9 --- /dev/null +++ b/quark/db/migration/alembic/versions/33e9e23ba761_add_floating_to_fixed_ip_mapping_table.py @@ -0,0 +1,35 @@ +"""Added Floating IP to Fixed IP mapping table + +Revision ID: 33e9e23ba761 +Revises: 356d6c0623c8 +Create Date: 2015-05-11 14:14:23.619952 + +""" + +# revision identifiers, used by Alembic. +revision = '33e9e23ba761' +down_revision = '356d6c0623c8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table('quark_floating_to_fixed_ip_address_associations', + sa.Column('floating_ip_address_id', sa.String(length=36), + nullable=False), + sa.Column('fixed_ip_address_id', sa.String(length=36), + nullable=False), + sa.Column('enabled', sa.Boolean(), server_default='1', + nullable=False), + sa.ForeignKeyConstraint(['fixed_ip_address_id'], + ['quark_ip_addresses.id'], ), + sa.ForeignKeyConstraint(['floating_ip_address_id'], + ['quark_ip_addresses.id'], ), + sa.PrimaryKeyConstraint('floating_ip_address_id', + 'fixed_ip_address_id'), + mysql_engine='InnoDB') + + +def downgrade(): + op.drop_table('quark_floating_to_fixed_ip_address_associations') diff --git a/quark/db/migration/alembic/versions/3b467be51e43_composite_primary_key_port_ip_.py b/quark/db/migration/alembic/versions/3b467be51e43_composite_primary_key_port_ip_.py index af2a72c..9298ccc 100644 --- a/quark/db/migration/alembic/versions/3b467be51e43_composite_primary_key_port_ip_.py +++ b/quark/db/migration/alembic/versions/3b467be51e43_composite_primary_key_port_ip_.py @@ -50,13 +50,17 @@ def _foreign_keys_dropped(op, table): def upgrade(): - op.alter_column('quark_port_ip_address_associations', 'ip_address_id', - existing_type=sa.String(36), nullable=False) - op.alter_column('quark_port_ip_address_associations', 'port_id', - existing_type=sa.String(36), nullable=False) - op.create_primary_key("pk_quark_port_ip_address_associations", - "quark_port_ip_address_associations", - ['port_id', 'ip_address_id']) + metadata = sa.MetaData(bind=op.get_bind()) + table = sa.Table('quark_port_ip_address_associations', metadata, + autoload=True) + with _foreign_keys_dropped(op, table): + op.alter_column('quark_port_ip_address_associations', 'ip_address_id', + existing_type=sa.String(36), nullable=False) + op.alter_column('quark_port_ip_address_associations', 'port_id', + existing_type=sa.String(36), nullable=False) + op.create_primary_key("pk_quark_port_ip_address_associations", + "quark_port_ip_address_associations", + ['port_id', 'ip_address_id']) def downgrade(): diff --git a/quark/db/migration/alembic/versions/HEAD b/quark/db/migration/alembic/versions/HEAD index 7b01d0a..2e184fe 100644 --- a/quark/db/migration/alembic/versions/HEAD +++ b/quark/db/migration/alembic/versions/HEAD @@ -1 +1 @@ -356d6c0623c8 \ No newline at end of file +33e9e23ba761 \ No newline at end of file diff --git a/quark/db/models.py b/quark/db/models.py index 6b330d8..80fdd54 100644 --- a/quark/db/models.py +++ b/quark/db/models.py @@ -197,6 +197,40 @@ class IPAddress(BASEV2, models.HasId): return str(ip.ipv6()) deallocated_at = sa.Column(sa.DateTime(), index=True) + fixed_ip = None + + +class FloatingToFixedIPAssociation(object): + pass + +flip_to_fixed_ip_assoc_tbl = sa.Table( + "quark_floating_to_fixed_ip_address_associations", + BASEV2.metadata, + sa.Column("floating_ip_address_id", sa.String(36), + sa.ForeignKey("quark_ip_addresses.id"), nullable=False, + primary_key=True), + sa.Column("fixed_ip_address_id", sa.String(36), + sa.ForeignKey("quark_ip_addresses.id"), nullable=False, + primary_key=True), + sa.Column("enabled", sa.Boolean(), default=True, nullable=False, + server_default='1'), + **TABLE_KWARGS) + +orm.mapper(FloatingToFixedIPAssociation, flip_to_fixed_ip_assoc_tbl) + +IPAddress.fixed_ip = orm.relationship("IPAddress", + secondary=flip_to_fixed_ip_assoc_tbl, + primaryjoin=(IPAddress.id == + flip_to_fixed_ip_assoc_tbl + .c.floating_ip_address_id + and + flip_to_fixed_ip_assoc_tbl + .c.floating_ip_address_id == + 1), + secondaryjoin=(IPAddress.id == + flip_to_fixed_ip_assoc_tbl + .c.fixed_ip_address_id), + uselist=False) class Route(BASEV2, models.HasTenant, models.HasId, IsHazTags): @@ -391,6 +425,7 @@ class Port(BASEV2, models.HasTenant, models.HasId): secondary=port_group_association_table, backref="ports") + # Indices tailored specifically to get_instance_nw_info calls from nova sa.Index("idx_ports_1", Port.__table__.c.device_id, Port.__table__.c.tenant_id) sa.Index("idx_ports_2", Port.__table__.c.device_owner, diff --git a/quark/drivers/unicorn_driver.py b/quark/drivers/unicorn_driver.py index 6be68f5..5af0ef7 100644 --- a/quark/drivers/unicorn_driver.py +++ b/quark/drivers/unicorn_driver.py @@ -17,6 +17,8 @@ Unicorn driver for Quark """ +import json +import netaddr import requests from oslo.config import cfg @@ -44,21 +46,41 @@ class UnicornDriver(object): def get_name(cls): return "Unicorn" - def register_floating_ip(self, floating_ip): - pass + def register_floating_ip(self, floating_ip, port, fixed_ip): + url = CONF.QUARK.floating_ip_base_url + req = self._build_request_body(floating_ip, port, fixed_ip) + + LOG.info("Calling unicorn to register floating ip: %s %s" % (url, req)) + r = requests.post(url, data=json.dumps(req)) + + if r.status_code != 200: + msg = "Unexpected status from unicorn API: Status Code %s, " \ + "Message: %s" % (r.status_code, r.json()) + LOG.error("register_floating_ip: %s" % msg) + raise ex.RegisterFloatingIpFailure(id=floating_ip.id) def update_floating_ip(self, floating_ip): pass def remove_floating_ip(self, floating_ip): url = "%s/%s" % (CONF.QUARK.floating_ip_base_url, - floating_ip.formatted()) + floating_ip.address_readable) LOG.info("Calling unicorn to remove floating ip: %s" % url) r = requests.delete(url) if r.status_code != 204: msg = "Unexpected status from unicorn API: Status Code %s, " \ - "Message: %s" % (r.status_code,) + "Message: %s" % (r.status_code, r.json()) LOG.error("remove_floating_ip: %s" % msg) - raise ex.RemoveFloatingIpFailure(id=floating_ip["id"], msg=msg) + raise ex.RemoveFloatingIpFailure(id=floating_ip.id) + + @staticmethod + def _build_request_body(floating_ip, port, fixed_ip): + mac_addr = netaddr.EUI(port.mac_address) + content = {"public_ip": floating_ip["address_readable"], + "network_uuid": port.id, + "destinations": [ + {"private_ip": fixed_ip.address_readable, + "private_mac": str(mac_addr)}]} + return {"floating_ip": content} diff --git a/quark/exceptions.py b/quark/exceptions.py index 0f8e136..231c0ce 100644 --- a/quark/exceptions.py +++ b/quark/exceptions.py @@ -140,5 +140,26 @@ class FloatingIpNotFound(exceptions.NeutronException): class RemoveFloatingIpFailure(exceptions.NeutronException): - message = _("An error occured when trying to remove the floating IP %(id)." - " %(msg)") + message = _("An error occurred when trying to remove the " + "floating IP %(id).") + + +class RegisterFloatingIpFailure(exceptions.NeutronException): + message = _("An error occurred when trying to register the floating IP " + "%(id).") + + +class PortAlreadyContainsFloatingIp(exceptions.Conflict): + message = _("Port %(port_id) already has an associated floating IP.") + + +class FixedIpDoesNotExistsForPort(exceptions.BadRequest): + message = _("Fixed IP %(fixed_ip) does not exist on Port %(port_id)") + + +class NoAvailableFixedIPsForPort(exceptions.Conflict): + message = _("There are no available fixed IPs for port %(port_id)") + + +class PortDoesNotHaveAGateway(exceptions.Conflict): + message = _("Port %(port_id) does not have a gateway") diff --git a/quark/plugin.py b/quark/plugin.py index 4f6af0a..fda3b94 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -386,7 +386,9 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, @sessioned def create_floatingip(self, context, floatingip): - return floating_ips.create_floatingip(context, floatingip) + self._fix_missing_tenant_id(context, floatingip["floatingip"]) + return floating_ips.create_floatingip(context, + floatingip["floatingip"]) @sessioned def update_floatingip(self, context, id, floatingip): @@ -412,9 +414,8 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, def get_routers_count(self, context, filters=None): raise NotImplementedError() - @sessioned def get_floatingips_count(self, context, filters=None): - raise floating_ips.get_floatingips_count(context, filters) + return floating_ips.get_floatingips_count(context, filters) def get_ip_availability(self, **kwargs): return ip_availability.get_ip_availability(**kwargs) diff --git a/quark/plugin_modules/floating_ips.py b/quark/plugin_modules/floating_ips.py index 996c4c5..6428c5b 100644 --- a/quark/plugin_modules/floating_ips.py +++ b/quark/plugin_modules/floating_ips.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from neutron.common import exceptions from oslo.config import cfg from oslo_log import log as logging @@ -30,34 +31,95 @@ LOG = logging.getLogger(__name__) quark_router_opts = [ cfg.StrOpt('default_floating_ip_driver', default='Unicorn', - help=_('Driver for floating IP')) + help=_('Driver for floating IP')), + cfg.StrOpt('floating_ip_segment_name', default='floating_ip', + help=_('Segment name for floating IP subnets')) ] CONF.register_opts(quark_router_opts, "QUARK") -def create_floatingip(context, body): +def create_floatingip(context, content): LOG.info("create_floatingip %s for tenant %s and body %s" % - (id, context.tenant_id, body)) + (id, context.tenant_id, content)) + tenant_id = content.get("tenant_id") + network_id = content.get("floating_network_id") + fixed_ip_address = content.get("fixed_ip_address") + ip_address = content.get("floating_ip_address") + port_id = content.get("port_id") - # floating_ip_dict = body.get("ip_address") - # tenant_id = floating_ip_dict.get("tenant_id") - # network_id = floating_ip_dict.get("floating_network_id") - # # fixed_ip_address = floating_ip_dict.get("fixed_ip_address") - # # ip_address = floating_ip_dict.get("floating_ip_address") - # port_id = floating_ip_dict.get("port_id") - # - # if not tenant_id: - # raise exceptions.BadRequest(resource="floating_ip", - # msg="tenant_id is required.") - # if not network_id: - # raise exceptions.BadRequest(resource="floating_ip", - # msg="floating_network_id is required.") - # if not port_id: - # raise exceptions.BadRequest(resource="floating_ip", - # msg="port_id is required.") + if not tenant_id: + tenant_id = context.tenant_id - raise NotImplementedError() + if not network_id: + raise exceptions.BadRequest(resource="floating_ip", + msg="floating_network_id is required.") + + network = db_api.network_find(context, id=network_id, scope=db_api.ONE) + + if not network: + raise exceptions.NetworkNotFound(net_id=network_id) + + fixed_ip = None + port = None + if port_id: + port = db_api.port_find(context, id=port_id, scope=db_api.ONE) + + if not port: + raise exceptions.PortNotFound(port_id=port_id) + + if not port.ip_addresses or len(port.ip_addresses) == 0: + raise quark_exceptions.NoAvailableFixedIPsForPort(port_id=port_id) + + if not fixed_ip_address: + fixed_ip = _get_next_available_fixed_ip(port) + if not fixed_ip: + raise quark_exceptions.NoAvailableFixedIPsForPort( + port_id=port_id) + else: + fixed_ip = next((ip for ip in port.ip_addresses + if (ip["address_readable"] == fixed_ip_address and + ip.get("address_type") == ip_types.FIXED)), + None) + + if not fixed_ip: + raise quark_exceptions.FixedIpDoesNotExistsForPort( + fixed_ip=fixed_ip_address, port_id=port_id) + + if any(ip for ip in port.ip_addresses + if (ip.get("address_type") == ip_types.FLOATING and + ip.fixed_ip["address_readable"] == fixed_ip_address)): + raise quark_exceptions.PortAlreadyContainsFloatingIp( + port_id=port_id) + + new_addresses = [] + ip_addresses = [] + if ip_address: + ip_addresses.append(ip_address) + + seg_name = CONF.QUARK.floating_ip_segment_name + strategy_name = network.get("ipam_strategy") + ipam_driver = ipam.IPAM_REGISTRY.get_strategy(strategy_name) + ipam_driver.allocate_ip_address(context, new_addresses, network_id, + port_id, CONF.QUARK.ipam_reuse_after, + seg_name, version=4, + ip_addresses=ip_addresses, + address_type=ip_types.FLOATING) + + floating_ip = new_addresses[0] + + if fixed_ip and port: + with context.session.begin(): + floating_ip = db_api.floating_ip_associate_fixed_ip(context, + floating_ip, + fixed_ip) + + flip_driver_type = CONF.QUARK.default_floating_ip_driver + flip_driver = registry.DRIVER_REGISTRY.get_driver(flip_driver_type) + + flip_driver.register_floating_ip(floating_ip, port, fixed_ip) + + return v._make_floating_ip_dict(floating_ip) def update_floatingip(context, id, body): @@ -80,18 +142,19 @@ def delete_floatingip(context, id): filters = {"address_type": ip_types.FLOATING, "_deallocated": False} - addr = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, **filters) - if not addr: + floating_ip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, + **filters) + if not floating_ip: raise quark_exceptions.FloatingIpNotFound(id=id) driver_type = CONF.QUARK.default_floating_ip_driver driver = registry.DRIVER_REGISTRY.get_driver(driver_type) - driver.remove_floating_ip(addr) + driver.remove_floating_ip(floating_ip) - strategy_name = addr.network["ipam_strategy"] + strategy_name = floating_ip.network.get("ipam_strategy") ipam_driver = ipam.IPAM_REGISTRY.get_strategy(strategy_name) - ipam_driver.deallocate_ip_address(context, addr) + ipam_driver.deallocate_ip_address(context, floating_ip) def get_floatingip(context, id, fields=None): @@ -112,12 +175,13 @@ def get_floatingip(context, id, fields=None): filters = {"address_type": ip_types.FLOATING, "_deallocated": False} - addr = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, **filters) + floating_ip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, + **filters) - if not addr: + if not floating_ip: raise quark_exceptions.FloatingIpNotFound(id=id) - return v._make_floating_ip_dict(addr) + return v._make_floating_ip_dict(floating_ip) def get_floatingips(context, filters=None, fields=None, sorts=None, limit=None, @@ -150,9 +214,10 @@ def get_floatingips(context, filters=None, fields=None, sorts=None, limit=None, filters["_deallocated"] = False filters["address_type"] = ip_types.FLOATING - addrs = db_api.floating_ip_find(context, scope=db_api.ALL, **filters) + floating_ips = db_api.floating_ip_find(context, scope=db_api.ALL, + **filters) - return [v._make_floating_ip_dict(ip) for ip in addrs] + return [v._make_floating_ip_dict(flip) for flip in floating_ips] def get_floatingips_count(context, filters=None): @@ -174,7 +239,7 @@ def get_floatingips_count(context, filters=None): NOTE: this method is optional, as it was not part of the originally defined plugin API. """ - LOG.info("get_floatingips_count for tenant %s filters" % + LOG.info("get_floatingips_count for tenant %s filters %s" % (context.tenant_id, filters)) if filters is None: @@ -184,3 +249,20 @@ def get_floatingips_count(context, filters=None): filters["address_type"] = ip_types.FLOATING return db_api.ip_address_count_all(context, filters) + + +def _get_next_available_fixed_ip(port): + floating_ips = [ip for ip in port.ip_addresses + if ip.get("address_type") == ip_types.FLOATING] + fixed_ips = [ip for ip in port.ip_addresses + if ip.get("address_type") == ip_types.FIXED] + + if not fixed_ips or len(fixed_ips) == 0: + return None + + used = [ip.fixed_ip.address for ip in floating_ips + if ip and ip.fixed_ip] + + return next((ip for ip in sorted(fixed_ips, + key=lambda ip: ip.get("allocated_at")) + if ip.address not in used), None) diff --git a/quark/plugin_views.py b/quark/plugin_views.py index 33b7e36..31a9be3 100644 --- a/quark/plugin_views.py +++ b/quark/plugin_views.py @@ -188,7 +188,9 @@ def _make_port_address_dict(ip, port, fields=None): def _make_port_dict(port, fields=None): res = _port_dict(port) res["fixed_ips"] = [_make_port_address_dict(ip, port, fields) - for ip in port.ip_addresses] + for ip in port.ip_addresses + if (not ip.get("address_type") or + ip.get("address_type") == ip_types.FIXED)] return res @@ -196,8 +198,11 @@ def _make_ports_list(query, fields=None): ports = [] for port in query: port_dict = _port_dict(port, fields) - port_dict["fixed_ips"] = [_make_port_address_dict(addr, port, fields) - for addr in port.ip_addresses] + port_dict["fixed_ips"] = [_make_port_address_dict(ip, port, fields) + for ip in port.ip_addresses + if (not ip.get("address_type") or + ip.get("address_type") == ip_types.FIXED) + ] ports.append(port_dict) return ports @@ -245,27 +250,16 @@ def _make_ip_policy_dict(ipp): def _make_floating_ip_dict(flip): - # Note(Alan Quillin) A floating IP should only be associated with a single - # port so we need to get the first port from the list if any exist. - + ports = flip.ports port_id = None - fixed_ip = None - if flip.ports and len(flip.ports) > 0: - port = flip.ports[0] - port_id = flip.ports[0].id - if port.ip_addresses and len(port.ip_addresses) > 0: - # Note(Alan Quillin) If the associated port has multiple fixed IP - # addresses, the first one found is used (based on allocated_at) - fixed_ip = next(ip.formatted() if ip else None - for ip in sorted(port.ip_addresses, - key=lambda ip: - ip.get("allocated_at")) - if ip.get("address_type") == ip_types.FIXED) + if ports and len(ports) > 0: + port_id = None if not ports[0] else ports[0].id + fixed_ip = flip.fixed_ip return {"id": flip.get("id"), "floating_network_id": flip.get("network_id"), "router_id": CONF.QUARK.floating_ip_router_id, - "fixed_ip_address": fixed_ip, + "fixed_ip_address": None if not fixed_ip else fixed_ip.formatted(), "floating_ip_address": flip.formatted(), "tenant_id": flip.get("tenant_id"), "status": flip.get("status"), diff --git a/quark/tests/plugin_modules/test_floating_ips.py b/quark/tests/plugin_modules/test_floating_ips.py index d87caea..2ea9070 100644 --- a/quark/tests/plugin_modules/test_floating_ips.py +++ b/quark/tests/plugin_modules/test_floating_ips.py @@ -15,20 +15,24 @@ import contextlib +import datetime import mock +import netaddr +from neutron.common import exceptions from quark.db import models from quark import exceptions as quark_exceptions +from quark.plugin_modules import floating_ips from quark.tests import test_quark_plugin class TestRemoveFloatingIPs(test_quark_plugin.TestQuarkPlugin): @contextlib.contextmanager - def _stubs(self, addr): - addr_model = None - if addr: - addr_model = models.IPAddress() - addr_model.update(addr) + def _stubs(self, flip=None): + flip_model = None + if flip: + flip_model = models.IPAddress() + flip_model.update(flip) with contextlib.nested( mock.patch("quark.db.api.floating_ip_find"), @@ -36,17 +40,412 @@ class TestRemoveFloatingIPs(test_quark_plugin.TestQuarkPlugin): mock.patch("quark.drivers.unicorn_driver.UnicornDriver" ".remove_floating_ip") ) as (flip_find, mock_dealloc, mock_remove_flip): - flip_find.return_value = addr_model + flip_find.return_value = flip_model yield def test_delete_floating_by_ip_address_id(self): - addr = dict(id=1, address=3232235876, address_readable="192.168.1.100", + flip = dict(id=1, address=3232235876, address_readable="192.168.1.100", subnet_id=1, network_id=2, version=4, used_by_tenant_id=1, network=dict(ipam_strategy="ANY")) - with self._stubs(addr=addr): + with self._stubs(flip=flip): self.plugin.delete_floatingip(self.context, 1) def test_delete_floating_by_when_ip_address_does_not_exists_fails(self): - with self._stubs(addr=None): + with self._stubs(): with self.assertRaises(quark_exceptions.FloatingIpNotFound): self.plugin.delete_floatingip(self.context, 1) + + +class TestFloatingIPUtilityMethods(test_quark_plugin.TestQuarkPlugin): + def test_get_next_available_fixed_ip_with_single_fixed_ip(self): + port = models.Port() + port.update(dict(id=1)) + + fixed_ip_addr = netaddr.IPAddress('192.168.0.1') + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", address=int(fixed_ip_addr), + version=4, address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + port.ip_addresses.append(fixed_ip) + + next_fixed_ip = floating_ips._get_next_available_fixed_ip(port) + + self.assertEqual(next_fixed_ip["address_readable"], '192.168.0.1') + + def test_get_next_available_fixed_ip_with_mult_fixed_ips(self): + port = models.Port() + port.update(dict(id=1)) + + for ip_addr in ["192.168.0.1", "192.168.0.2", "192.168.0.3"]: + fixed_ip_addr = netaddr.IPAddress(ip_addr) + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", + address=int(fixed_ip_addr), + version=4, + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + port.ip_addresses.append(fixed_ip) + + next_fixed_ip = floating_ips._get_next_available_fixed_ip(port) + + self.assertEqual(next_fixed_ip["address_readable"], '192.168.0.1') + + def test_get_next_available_fixed_ip_with_no_avail_fixed_ips(self): + port = models.Port() + port.update(dict(id=1)) + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", + address=int(fixed_ip_addr), + version=4, + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + flip_addr = netaddr.IPAddress("10.0.0.1") + flip = models.IPAddress() + flip.update(dict(address_type="floating", + address=int(flip_addr), + version=4, + address_readable=str(flip_addr), + allocated_at=datetime.datetime.now())) + flip.fixed_ip = fixed_ip + + port.ip_addresses.append(fixed_ip) + port.ip_addresses.append(flip) + + fixed_ip_addr = netaddr.IPAddress("192.168.0.2") + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", + address=int(fixed_ip_addr), + version=4, + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + flip_addr = netaddr.IPAddress("10.0.0.2") + flip = models.IPAddress() + flip.update(dict(address_type="floating", + address=int(flip_addr), + version=4, + address_readable=str(flip_addr), + allocated_at=datetime.datetime.now())) + flip.fixed_ip = fixed_ip + + port.ip_addresses.append(fixed_ip) + port.ip_addresses.append(flip) + + next_fixed_ip = floating_ips._get_next_available_fixed_ip(port) + + self.assertEqual(next_fixed_ip, None) + + def test_get_next_available_fixed_ip_with_avail_fixed_ips(self): + port = models.Port() + port.update(dict(id=1)) + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", + address=int(fixed_ip_addr), + version=4, + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + flip_addr = netaddr.IPAddress("10.0.0.1") + flip = models.IPAddress() + flip.update(dict(address_type="floating", + address=int(flip_addr), + version=4, + address_readable=str(flip_addr), + allocated_at=datetime.datetime.now())) + flip.fixed_ip = fixed_ip + + port.ip_addresses.append(fixed_ip) + port.ip_addresses.append(flip) + + fixed_ip_addr = netaddr.IPAddress("192.168.0.2") + fixed_ip = models.IPAddress() + fixed_ip.update(dict(address_type="fixed", + address=int(fixed_ip_addr), + version=4, + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + port.ip_addresses.append(fixed_ip) + port.ip_addresses.append(flip) + + next_fixed_ip = floating_ips._get_next_available_fixed_ip(port) + + self.assertEqual(next_fixed_ip["address_readable"], "192.168.0.2") + + +class TestCreateFloatingIPs(test_quark_plugin.TestQuarkPlugin): + @contextlib.contextmanager + def _stubs(self, flip=None, port=None, ips=None, network=None): + port_model = None + if port: + port_model = models.Port() + port_model.update(dict(port=port)) + if ips: + for ip in ips: + ip_model = models.IPAddress() + ip_model.update(ip) + if (ip["address_type"] == "floating" + and "fixed_ip_addr" in ip): + fixed_ip = models.IPAddress() + fixed_ip.update(next(ip_addr for ip_addr in ips + if (ip_addr["address_readable"] == + ip["fixed_ip_addr"]))) + ip_model.fixed_ip = fixed_ip + port_model.ip_addresses.append(ip_model) + + flip_model = None + if flip: + flip_model = models.IPAddress() + flip_model.update(flip) + + net_model = None + if network: + net_model = models.Network() + net_model.update(network) + + def _alloc_ip(context, new_addr, net_id, port_m, *args, **kwargs): + new_addr.append(flip_model) + + def _flip_fixed_ip_assoc(context, addr, fixed_ip): + addr.fixed_ip = fixed_ip + return addr + + with contextlib.nested( + mock.patch("quark.db.api.floating_ip_find"), + mock.patch("quark.db.api.network_find"), + mock.patch("quark.db.api.port_find"), + mock.patch("quark.ipam.QuarkIpam.allocate_ip_address"), + mock.patch("quark.drivers.unicorn_driver.UnicornDriver" + ".register_floating_ip"), + mock.patch("quark.db.api.floating_ip_associate_fixed_ip") + ) as (flip_find, net_find, port_find, alloc_ip, mock_reg_flip, assoc): + flip_find.return_value = flip_model + net_find.return_value = net_model + port_find.return_value = port_model + alloc_ip.side_effect = _alloc_ip + assoc.side_effect = _flip_fixed_ip_assoc + yield + + def test_create_with_a_port(self): + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(id=1, address=int(floating_ip_addr), version=4, + address_readable=str(floating_ip_addr), subnet_id=1, + network_id=2, used_by_tenant_id=1) + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ips = [dict(address_type="fixed", address=int(fixed_ip_addr), + version=4, address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())] + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(flip=floating_ip, port=port, + ips=fixed_ips, network=network): + request = dict(floating_network_id=network["id"], + port_id=port["id"]) + flip = self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + self.assertEqual(flip["floating_ip_address"], "10.0.0.1") + self.assertEqual(flip["fixed_ip_address"], "192.168.0.1") + + def test_create_without_a_port(self): + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(id=1, address=int(floating_ip_addr), version=4, + address_readable=str(floating_ip_addr), subnet_id=1, + network_id=2, used_by_tenant_id=1) + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ips = [dict(address_type="fixed", address=int(fixed_ip_addr), + version=4, address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())] + + with self._stubs(flip=floating_ip, port=None, + ips=fixed_ips, network=network): + request = dict(floating_network_id=network["id"], port_id=None) + flip = self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + self.assertEqual(flip["floating_ip_address"], "10.0.0.1") + self.assertEqual(flip.get("fixed_ip_address"), None) + + def test_create_with_fixed_ip_specified(self): + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(id=1, address=int(floating_ip_addr), version=4, + address_readable=str(floating_ip_addr), subnet_id=1, + network_id=2, used_by_tenant_id=1) + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ips = [] + for ip_addr in ["192.168.0.1", "192.168.0.2"]: + fixed_ip_addr = netaddr.IPAddress(ip_addr) + fixed_ips.append(dict(address_type="fixed", version=4, + address=int(fixed_ip_addr), + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())) + + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(flip=floating_ip, port=port, + ips=fixed_ips, network=network): + request = dict(floating_network_id=network["id"], + port_id=port["id"], fixed_ip_address="192.168.0.2") + flip = self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + self.assertEqual(flip["floating_ip_address"], "10.0.0.1") + self.assertEqual(flip["fixed_ip_address"], "192.168.0.2") + + def test_create_with_floating_ip_specified(self): + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(id=1, address=int(floating_ip_addr), version=4, + address_readable=str(floating_ip_addr), subnet_id=1, + network_id=2, used_by_tenant_id=1) + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ips = [dict(address_type="fixed", address=int(fixed_ip_addr), + version=4, address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())] + port = dict(id=2) + + with self._stubs(flip=floating_ip, port=port, + ips=fixed_ips, network=network): + request = dict(floating_network_id=network["id"], + port_id=port["id"], floating_ip_address="10.0.0.1") + flip = self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + self.assertEqual(flip["floating_ip_address"], "10.0.0.1") + self.assertEqual(flip["fixed_ip_address"], "192.168.0.1") + + def test_create_without_network_id_fails(self): + with self._stubs(): + with self.assertRaises(exceptions.BadRequest): + request = dict(port_id=2, floating_ip_address="10.0.0.1") + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + + def test_create_with_invalid_network_fails(self): + with self._stubs(): + with self.assertRaises(exceptions.NetworkNotFound): + request = dict(floating_network_id=123, + port_id=2, floating_ip_address="10.0.0.1") + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + + def test_create_with_invalid_port_fails(self): + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + with self._stubs(network=network): + with self.assertRaises(exceptions.PortNotFound): + request = dict(floating_network_id=network["id"], + port_id=2, floating_ip_address="10.0.0.1") + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + + def test_create_with_invalid_fixed_ip_for_port_fails(self): + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ips = [dict(address_type="fixed", version=4, + address=int(fixed_ip_addr), + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now())] + + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(port=port, ips=fixed_ips, network=network): + with self.assertRaises( + quark_exceptions.FixedIpDoesNotExistsForPort): + request = dict(floating_network_id=network["id"], + port_id=port["id"], + fixed_ip_address="192.168.0.2") + flip = self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + self.assertEqual(flip["address_readable"], "10.0.0.1") + self.assertEqual(flip.fixed_ip["address_readable"], + "192.168.0.2") + + def test_create_with_port_and_fixed_ip_with_existing_flip_fails(self): + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ip = dict(address_type="fixed", version=4, + address=int(fixed_ip_addr), + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now()) + + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(address_type="floating", version=4, + address=int(floating_ip_addr), + address_readable=str(floating_ip_addr), + allocated_at=datetime.datetime.now(), + fixed_ip_addr="192.168.0.1") + + ips = [fixed_ip, floating_ip] + + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(port=port, ips=ips, network=network): + with self.assertRaises( + quark_exceptions.PortAlreadyContainsFloatingIp): + request = dict(floating_network_id=network["id"], + port_id=port["id"], + fixed_ip_address="192.168.0.1") + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + + def test_create_when_port_has_no_fixed_ips_fails(self): + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(port=port, network=network): + with self.assertRaises( + quark_exceptions.NoAvailableFixedIPsForPort): + request = dict(floating_network_id=network["id"], + port_id=port["id"]) + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) + + def test_create_when_port_has_no_available_fixed_ips_fails(self): + network = dict(id="00000000-0000-0000-0000-000000000000", + ipam_strategy="ANY") + + fixed_ip_addr = netaddr.IPAddress("192.168.0.1") + fixed_ip = dict(address_type="fixed", version=4, + address=int(fixed_ip_addr), + address_readable=str(fixed_ip_addr), + allocated_at=datetime.datetime.now()) + + floating_ip_addr = netaddr.IPAddress("10.0.0.1") + floating_ip = dict(address_type="floating", version=4, + address=int(floating_ip_addr), + address_readable=str(floating_ip_addr), + allocated_at=datetime.datetime.now(), + fixed_ip_addr="192.168.0.1") + + ips = [fixed_ip, floating_ip] + + port = dict(id="abcdefgh-1111-2222-3333-1234567890ab") + + with self._stubs(port=port, ips=ips, network=network): + with self.assertRaises( + quark_exceptions.NoAvailableFixedIPsForPort): + request = dict(floating_network_id=network["id"], + port_id=port["id"]) + self.plugin.create_floatingip(self.context, + dict(floatingip=request)) diff --git a/quark/tests/test_ipam.py b/quark/tests/test_ipam.py index 3b48115..277ecbf 100644 --- a/quark/tests/test_ipam.py +++ b/quark/tests/test_ipam.py @@ -1557,6 +1557,19 @@ class QuarkNewIPAddressAllocation(QuarkIpamBaseTest): self.ipam.allocate_ip_address( self.context, [], 0, 0, 0, ip_addresses=["0.0.0.240"]) + def test_allocate_new_ip_address_with_floating_address_type(self): + subnet = dict(id=1, first_ip=0, last_ip=255, + cidr="0.0.0.0/24", ip_version=4, + next_auto_assign_ip=0, + ip_policy=dict(size=1, exclude=[ + models.IPPolicyCIDR(cidr="0.0.0.0/32")])) + with self._stubs(subnets=[(subnet, 0)], addresses=[None, None]): + address = [] + self.ipam.allocate_ip_address(self.context, address, 0, 0, 0, + version=4, + address_type='floating') + self.assertEqual(address[0]["address_type"], 'floating') + class QuarkIPAddressAllocationTestRetries(QuarkIpamBaseTest): @contextlib.contextmanager