diff --git a/quark/api/extensions/scalingip.py b/quark/api/extensions/scalingip.py new file mode 100644 index 0000000..637c42f --- /dev/null +++ b/quark/api/extensions/scalingip.py @@ -0,0 +1,139 @@ +# Copyright (c) 2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from neutron.api import extensions +from neutron.api.v2 import attributes as attr +from neutron.api.v2 import resource_helper +from oslo_log import log as logging + + +LOG = logging.getLogger(__name__) + + +def _validate_list_of_port_dicts(values, data): + if not isinstance(values, list): + msg = _("'%s' is not a list") % data + return msg + + for item in values: + msg = _validate_port_dict(item) + if msg: + return msg + + items = [tuple(entry.items()) for entry in values] + if len(items) != len(set(items)): + msg = _("Duplicate items in the list: '%s'") % values + return msg + + +def _validate_port_dict(values): + if not isinstance(values, dict): + msg = _("%s is not a valid dictionary") % values + LOG.debug(msg) + return msg + port_id = values.get('port_id') + fixed_ip = values.get('fixed_ip_address') + msg = attr._validate_uuid(port_id) + if msg: + return msg + if fixed_ip is None: + return + msg = attr._validate_ip_address(fixed_ip) + if msg: + return msg + +attr.validators['type:validate_list_of_port_dicts'] = ( + _validate_list_of_port_dicts +) + +RESOURCE_NAME = "scalingip" +RESOURCE_COLLECTION = RESOURCE_NAME + "s" + +RESOURCE_ATTRIBUTE_MAP = { + RESOURCE_COLLECTION: { + 'id': { + 'allow_post': False, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True, + 'primary_key': True + }, + "scaling_ip_address": { + 'allow_post': True, 'allow_put': False, + 'validate': {'type:ip_address_or_none': None}, + 'is_visible': True, 'default': None, + 'enforce_policy': True + }, + "tenant_id": { + 'allow_post': True, 'allow_put': False, + 'required_by_policy': True, + 'validate': {'type:string': attr.TENANT_ID_MAX_LEN}, + 'is_visible': True + }, + "scaling_network_id": { + 'allow_post': True, 'allow_put': False, + 'validate': {'type:uuid': None}, + 'is_visible': True + }, + "ports": { + 'allow_post': True, 'allow_put': True, + 'validate': { + 'type:validate_list_of_port_dicts': None + }, + 'is_visible': True, + 'required_by_policy': True + } + } +} + + +class Scalingip(extensions.ExtensionDescriptor): + + @classmethod + def get_name(cls): + return RESOURCE_NAME + + @classmethod + def get_alias(cls): + return RESOURCE_NAME + + @classmethod + def get_description(cls): + return "Scaling IPs" + + @classmethod + def get_namespace(cls): + return ("http://docs.openstack.org/network/ext/" + "networks_quark/api/v2.0") + + @classmethod + def get_updated(cls): + return "2016-01-20T19:00:00-00:00" + + @classmethod + def get_resources(cls): + """Returns Ext Resources.""" + plural_mappings = resource_helper.build_plural_mappings( + {}, RESOURCE_ATTRIBUTE_MAP) + attr.PLURALS.update(plural_mappings) + return resource_helper.build_resource_info(plural_mappings, + RESOURCE_ATTRIBUTE_MAP, + None, + register_quota=True) + + def get_extended_resources(self, version): + if version == "2.0": + return RESOURCE_ATTRIBUTE_MAP + else: + return {} diff --git a/quark/db/api.py b/quark/db/api.py index d234e3d..34a355b 100644 --- a/quark/db/api.py +++ b/quark/db/api.py @@ -1010,12 +1010,22 @@ def floating_ip_find(context, lock_mode=False, limit=None, sorts=None, def floating_ip_associate_fixed_ip(context, floating_ip, fixed_ip): - floating_ip.fixed_ip = fixed_ip + floating_ip.fixed_ips.append(fixed_ip) return floating_ip -def floating_ip_disassociate_fixed_ip(context, floating_ip): - floating_ip.fixed_ip = None +def floating_ip_disassociate_fixed_ip(context, floating_ip, fixed_ip): + found_index = None + for index, flip_fixed_ip in enumerate(floating_ip.fixed_ips): + if flip_fixed_ip.id == fixed_ip.id: + found_index = index + break + floating_ip.fixed_ips.pop(found_index) + return floating_ip + + +def floating_ip_disassociate_all_fixed_ips(context, floating_ip): + floating_ip.fixed_ips = [] return floating_ip diff --git a/quark/db/ip_types.py b/quark/db/ip_types.py index e473ace..0e95b4e 100644 --- a/quark/db/ip_types.py +++ b/quark/db/ip_types.py @@ -1,3 +1,4 @@ SHARED = 'shared' FIXED = 'fixed' FLOATING = 'floating' +SCALING = 'scaling' diff --git a/quark/db/migration/alembic/versions/3f0c11478a5d_add_scaling_ip_address_type_enum.py b/quark/db/migration/alembic/versions/3f0c11478a5d_add_scaling_ip_address_type_enum.py new file mode 100644 index 0000000..ae28d9f --- /dev/null +++ b/quark/db/migration/alembic/versions/3f0c11478a5d_add_scaling_ip_address_type_enum.py @@ -0,0 +1,30 @@ +"""add scaling ip address type enum + +Revision ID: 3f0c11478a5d +Revises: a0798b3b7418 +Create Date: 2016-01-22 23:41:03.214930 + +""" + +# revision identifiers, used by Alembic. +revision = '3f0c11478a5d' +down_revision = 'a0798b3b7418' + +from alembic import op +import sqlalchemy as sa + + +existing_enum = sa.Enum("shared", "floating", "fixed") +new_enum = sa.Enum("shared", "floating", "fixed", "scaling") + + +def upgrade(): + op.alter_column("quark_ip_addresses", "address_type", + existing_type=existing_enum, + type_=new_enum) + + +def downgrade(): + op.alter_column("quark_ip_addresses", "address_type", + existing_type=new_enum, + type_=existing_enum) diff --git a/quark/db/migration/alembic/versions/HEAD b/quark/db/migration/alembic/versions/HEAD index 3b0a137..5f223e1 100644 --- a/quark/db/migration/alembic/versions/HEAD +++ b/quark/db/migration/alembic/versions/HEAD @@ -1 +1 @@ -a0798b3b7418 +3f0c11478a5d diff --git a/quark/db/models.py b/quark/db/models.py index d26d412..c109eaf 100644 --- a/quark/db/models.py +++ b/quark/db/models.py @@ -167,7 +167,7 @@ class IPAddress(BASEV2, models.HasId): used_by_tenant_id = sa.Column(sa.String(255)) address_type = sa.Column(sa.Enum(ip_types.FIXED, ip_types.FLOATING, - ip_types.SHARED, + ip_types.SHARED, ip_types.SCALING, name="quark_ip_address_types")) associations = orm.relationship(PortIpAssociation, backref="ip_address") transaction_id = sa.Column(sa.Integer(), @@ -246,19 +246,13 @@ flip_to_fixed_ip_assoc_tbl = sa.Table( 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) +IPAddress.fixed_ips = 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=True) class Route(BASEV2, models.HasTenant, models.HasId, IsHazTags): diff --git a/quark/drivers/unicorn_driver.py b/quark/drivers/unicorn_driver.py index 59ddfab..79e36f8 100644 --- a/quark/drivers/unicorn_driver.py +++ b/quark/drivers/unicorn_driver.py @@ -49,10 +49,19 @@ class UnicornDriver(object): def get_name(cls): return "Unicorn" - def register_floating_ip(self, floating_ip, port, fixed_ip): + def register_floating_ip(self, floating_ip, port_fixed_ips): + """Register a floating ip with Unicorn + + :param floating_ip: The quark.db.models.IPAddress to register + :param port_fixed_ips: A dictionary containing the port and fixed ips + to associate the floating IP with. Has the structure of: + {"": {"port": , + "fixed_ip": ""}} + :return: None + """ url = CONF.QUARK.floating_ip_base_url timeout = CONF.QUARK.unicorn_api_timeout_seconds - req = self._build_request_body(floating_ip, port, fixed_ip) + req = self._build_request_body(floating_ip, port_fixed_ips) try: LOG.info("Calling unicorn to register floating ip: %s %s" @@ -70,14 +79,23 @@ class UnicornDriver(object): LOG.error("register_floating_ip: %s" % msg) raise ex.RegisterFloatingIpFailure(id=floating_ip.id) - def update_floating_ip(self, floating_ip, port, fixed_ip): + def update_floating_ip(self, floating_ip, port_fixed_ips): + """Update an existing floating ip with Unicorn + + :param floating_ip: The quark.db.models.IPAddress to update + :param port_fixed_ips: A dictionary containing the port and fixed ips + to associate the floating IP with. Has the structure of: + {"": {"port": , + "fixed_ip": ""}} + :return: None + """ url = "%s/%s" % (CONF.QUARK.floating_ip_base_url, floating_ip["address_readable"]) timeout = CONF.QUARK.unicorn_api_timeout_seconds - req = self._build_request_body(floating_ip, port, fixed_ip) + req = self._build_request_body(floating_ip, port_fixed_ips) try: - LOG.info("Calling unicorn to register floating ip: %s %s" + LOG.info("Calling unicorn to update floating ip: %s %s" % (url, req)) r = requests.put(url, data=json.dumps(req), timeout=timeout) except Exception as e: @@ -93,6 +111,11 @@ class UnicornDriver(object): raise ex.RegisterFloatingIpFailure(id=floating_ip.id) def remove_floating_ip(self, floating_ip): + """Register a floating ip with Unicorn + + :param floating_ip: The quark.db.models.IPAddress to remove + :return: None + """ url = "%s/%s" % (CONF.QUARK.floating_ip_base_url, floating_ip.address_readable) timeout = CONF.QUARK.unicorn_api_timeout_seconds @@ -115,8 +138,8 @@ class UnicornDriver(object): LOG.error("remove_floating_ip: %s" % msg) raise ex.RemoveFloatingIpFailure(id=floating_ip.id) - @staticmethod - def _build_request_body(floating_ip, port, fixed_ip): + @classmethod + def _build_fixed_ips(cls, port): fixed_ips = [{"ip_address": ip.address_readable, "version": ip.version, "subnet_id": ip.subnet.id, @@ -124,14 +147,27 @@ class UnicornDriver(object): "address_type": ip.address_type} for ip in port.ip_addresses if (ip.get("address_type") == ip_types.FIXED)] + return fixed_ips + + @classmethod + def _build_endpoints(cls, port_fixed_ips): + endpoints = [] + for port_id in port_fixed_ips: + port = port_fixed_ips[port_id]["port"] + fixed_ip = port_fixed_ips[port_id]["fixed_ip"] + endpoint_port = {"uuid": port.id, + "name": port.name, + "network_uuid": port.network_id, + "mac_address": port.mac_address, + "device_id": port.device_id, + "device_owner": port.device_owner, + "fixed_ip": cls._build_fixed_ips(port)} + endpoints.append({"port": endpoint_port, + "private_ip": fixed_ip.address_readable}) + return endpoints + + @classmethod + def _build_request_body(cls, floating_ip, port_fixed_ips): content = {"public_ip": floating_ip["address_readable"], - "endpoints": [ - {"port": {"uuid": port.id, - "name": port.name, - "network_uuid": port.network_id, - "mac_address": port.mac_address, - "device_id": port.device_id, - "device_owner": port.device_owner, - "fixed_ip": fixed_ips}, - "private_ip": fixed_ip.address_readable}]} + "endpoints": cls._build_endpoints(port_fixed_ips)} return {"floating_ip": content} diff --git a/quark/exceptions.py b/quark/exceptions.py index 85463e2..54f0084 100644 --- a/quark/exceptions.py +++ b/quark/exceptions.py @@ -172,6 +172,10 @@ class FloatingIpNotFound(n_exc.NotFound): message = _("Floating IP %(id)s not found.") +class ScalingIpNotFound(n_exc.NotFound): + message = _("Scaling IP %(id)s not found.") + + class RemoveFloatingIpFailure(n_exc.NeutronException): message = _("An error occurred when trying to remove the " "floating IP %(id)s.") @@ -190,6 +194,10 @@ class FixedIpDoesNotExistsForPort(n_exc.BadRequest): message = _("Fixed IP %(fixed_ip)s does not exist on Port %(port_id)s") +class PortAlreadyContainsScalingIp(n_exc.Conflict): + message = _("Port %(port_id)s already has an associated scaling IP.") + + class NoAvailableFixedIpsForPort(n_exc.Conflict): message = _("There are no available fixed IPs for port %(port_id)s") diff --git a/quark/ipam.py b/quark/ipam.py index 7af14ec..efde8d4 100644 --- a/quark/ipam.py +++ b/quark/ipam.py @@ -749,9 +749,9 @@ class QuarkIpam(object): # fixed IP address. context.session.flush() for ip in ips_to_remove: - if ip["address_type"] == ip_types.FLOATING: - if ip.fixed_ip: - db_api.floating_ip_disassociate_fixed_ip(context, ip) + if ip["address_type"] in (ip_types.FLOATING, ip_types.SCALING): + if ip.fixed_ips: + db_api.floating_ip_disassociate_all_fixed_ips(context, ip) driver = registry.DRIVER_REGISTRY.get_driver() driver.remove_floating_ip(ip) else: diff --git a/quark/plugin.py b/quark/plugin.py index beca6f0..5a4bfac 100644 --- a/quark/plugin.py +++ b/quark/plugin.py @@ -130,7 +130,8 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, "provider", "ip_policies", "quotas", "networks_quark", "router", "ip_availabilities", "ports_quark", - "floatingip", "segment_allocation_ranges"] + "floatingip", "segment_allocation_ranges", + "scalingip"] def __init__(self): LOG.info("Starting quark plugin") @@ -472,3 +473,29 @@ class Plugin(neutron_plugin_base_v2.NeutronPluginBaseV2, def delete_segment_allocation_range(self, context, id): segment_allocation_ranges.delete_segment_allocation_range( context, id) + + def create_scalingip(self, context, scalingip): + self._fix_missing_tenant_id(context, scalingip["scalingip"]) + return floating_ips.create_scalingip(context, scalingip["scalingip"]) + + @sessioned + def update_scalingip(self, context, id, scalingip): + return floating_ips.update_scalingip(context, id, + scalingip["scalingip"]) + + @sessioned + def get_scalingip(self, context, id, fields=None): + return floating_ips.get_scalingip(context, id, fields) + + @sessioned + def delete_scalingip(self, context, id): + return floating_ips.delete_scalingip(context, id) + + @sessioned + def get_scalingips(self, context, filters=None, fields=None, + sorts=None, limit=None, marker=None, + page_reverse=False): + return floating_ips.get_scalingips(context, filters=filters, + fields=fields, sorts=sorts, + limit=limit, marker=marker, + page_reverse=page_reverse) diff --git a/quark/plugin_modules/floating_ips.py b/quark/plugin_modules/floating_ips.py index 789e341..4cf7c11 100644 --- a/quark/plugin_modules/floating_ips.py +++ b/quark/plugin_modules/floating_ips.py @@ -40,6 +40,276 @@ quark_router_opts = [ CONF.register_opts(quark_router_opts, 'QUARK') +def _get_network(context, network_id): + network = db_api.network_find(context, id=network_id, scope=db_api.ONE) + if not network: + raise n_exc.NetworkNotFound(net_id=network_id) + return network + + +def _get_port(context, port_id): + port = db_api.port_find(context, id=port_id, scope=db_api.ONE) + if not port: + raise n_exc.PortNotFound(port_id=port_id) + + if not port.ip_addresses or len(port.ip_addresses) == 0: + raise q_exc.NoAvailableFixedIpsForPort(port_id=port_id) + return port + + +def _get_fixed_ip(context, given_fixed_ip, port): + if not given_fixed_ip: + fixed_ip = _get_next_available_fixed_ip(port) + if not fixed_ip: + raise q_exc.NoAvailableFixedIpsForPort( + port_id=port.id) + else: + fixed_ip = next((ip for ip in port.ip_addresses + if (ip['address_readable'] == given_fixed_ip and + ip.get('address_type') == ip_types.FIXED)), + None) + + if not fixed_ip: + raise q_exc.FixedIpDoesNotExistsForPort( + fixed_ip=given_fixed_ip, port_id=port.id) + + if any(ip for ip in port.ip_addresses + if (ip.get('address_type') in (ip_types.FLOATING, + ip_types.SCALING) and + ip.fixed_ip['address_readable'] == given_fixed_ip)): + raise q_exc.PortAlreadyContainsFloatingIp( + port_id=port.id) + return fixed_ip + + +def _allocate_ip(context, network, port, requested_ip_address, address_type): + new_addresses = [] + ip_addresses = [] + if requested_ip_address: + ip_addresses.append(requested_ip_address) + + seg_name = CONF.QUARK.floating_ip_segment_name + strategy_name = CONF.QUARK.floating_ip_ipam_strategy + + if strategy_name.upper() == 'NETWORK': + strategy_name = network.get("ipam_strategy") + + port_id = port + if port: + port_id = port.id + + 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=address_type) + + return new_addresses[0] + + +def _get_next_available_fixed_ip(port): + floating_ips = [ip for ip in port.ip_addresses + if ip.get('address_type') in + (ip_types.FLOATING, ip_types.SCALING)] + 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) + + +def _get_ips_by_type(context, ip_type, filters=None, fields=None): + filters = filters or {} + filters['_deallocated'] = False + filters['address_type'] = ip_type + ips = db_api.floating_ip_find(context, scope=db_api.ALL, **filters) + return ips + + +def _create_flip(context, flip, port_fixed_ips): + """Associates the flip with ports and creates it with the flip driver + + :param context: neutron api request context. + :param flip: quark.db.models.IPAddress object representing a floating IP + :param port_fixed_ips: dictionary of the structure: + {"": {"port": , + "fixed_ip": ""}} + :return: None + """ + if port_fixed_ips: + context.session.begin() + try: + ports = [val['port'] for val in port_fixed_ips.values()] + flip = db_api.port_associate_ip(context, ports, flip, + port_fixed_ips.keys()) + + for port_id in port_fixed_ips: + fixed_ip = port_fixed_ips[port_id]['fixed_ip'] + flip = db_api.floating_ip_associate_fixed_ip(context, flip, + fixed_ip) + + flip_driver = registry.DRIVER_REGISTRY.get_driver() + + flip_driver.register_floating_ip(flip, port_fixed_ips) + context.session.commit() + except Exception: + context.session.rollback() + raise + + +def _get_flip_fixed_ip_by_port_id(flip, port_id): + for fixed_ip in flip.fixed_ips: + if fixed_ip.ports[0].id == port_id: + return fixed_ip + + +def _update_flip(context, flip_id, ip_type, requested_ports): + """Update a flip based IPAddress + + :param context: neutron api request context. + :param flip_id: id of the flip or scip + :param ip_type: ip_types.FLOATING | ip_types.SCALING + :param requested_ports: dictionary of the structure: + {"port_id": "", "fixed_ip": ""} + :return: quark.models.IPAddress + """ + context.session.begin() + try: + flip = db_api.floating_ip_find(context, id=flip_id, scope=db_api.ONE) + if not flip: + if ip_type == ip_types.SCALING: + raise q_exc.ScalingIpNotFound(id=flip_id) + raise q_exc.FloatingIpNotFound(id=flip_id) + current_ports = flip.ports + + # Determine what ports are being removed, being added, and remain + req_port_ids = [request_port.get('port_id') + for request_port in requested_ports] + curr_port_ids = [curr_port.id for curr_port in current_ports] + added_port_ids = [port_id for port_id in req_port_ids + if port_id and port_id not in curr_port_ids] + removed_port_ids = [port_id for port_id in curr_port_ids + if port_id not in req_port_ids] + remaining_port_ids = set(curr_port_ids) - set(removed_port_ids) + + # Validations just for floating ip types + if (ip_type == ip_types.FLOATING and curr_port_ids and + curr_port_ids == req_port_ids): + d = dict(flip_id=flip_id, port_id=curr_port_ids[0]) + raise q_exc.PortAlreadyAssociatedToFloatingIp(**d) + if (ip_type == ip_types.FLOATING and + not curr_port_ids and not req_port_ids): + raise q_exc.FloatingIpUpdateNoPortIdSupplied() + + port_fixed_ips = {} + + # Keep the ports and fixed ips that have not changed + for port_id in remaining_port_ids: + port = db_api.port_find(context, id=port_id, scope=db_api.ONE) + fixed_ip = _get_flip_fixed_ip_by_port_id(flip, port_id) + port_fixed_ips[port_id] = {'port': port, 'fixed_ip': fixed_ip} + + # Disassociate the ports and fixed ips from the flip that were + # associated to the flip but are not anymore + for port_id in removed_port_ids: + port = db_api.port_find(context, id=port_id, scope=db_api.ONE) + flip = db_api.port_disassociate_ip(context, [port], flip) + fixed_ip = _get_flip_fixed_ip_by_port_id(flip, port_id) + if fixed_ip: + flip = db_api.floating_ip_disassociate_fixed_ip( + context, flip, fixed_ip) + + # Validate the new ports with the flip and associate the new ports + # and fixed ips with the flip + for port_id in added_port_ids: + port = db_api.port_find(context, id=port_id, scope=db_api.ONE) + if not port: + raise n_exc.PortNotFound(port_id=port_id) + if any(ip for ip in port.ip_addresses + if (ip.get('address_type') == ip_types.FLOATING)): + raise q_exc.PortAlreadyContainsFloatingIp(port_id=port_id) + if any(ip for ip in port.ip_addresses + if (ip.get('address_type') == ip_types.SCALING)): + raise q_exc.PortAlreadyContainsScalingIp(port_id=port_id) + fixed_ip = _get_next_available_fixed_ip(port) + LOG.info('new fixed ip: %s' % fixed_ip) + if not fixed_ip: + raise q_exc.NoAvailableFixedIpsForPort(port_id=port_id) + port_fixed_ips[port_id] = {'port': port, 'fixed_ip': fixed_ip} + flip = db_api.port_associate_ip(context, [port], flip, [port_id]) + flip = db_api.floating_ip_associate_fixed_ip(context, flip, + fixed_ip) + + flip_driver = registry.DRIVER_REGISTRY.get_driver() + # If there are not any remaining ports and no new ones are being added, + # remove the floating ip from unicorn + if not remaining_port_ids and not added_port_ids: + flip_driver.remove_floating_ip(flip) + # If new ports are being added but there previously was not any ports, + # then register a new floating ip with the driver because it is + # assumed it does not exist + elif added_port_ids and not curr_port_ids: + flip_driver.register_floating_ip(flip, port_fixed_ips) + else: + flip_driver.update_floating_ip(flip, port_fixed_ips) + context.session.commit() + except Exception: + context.session.rollback() + raise + # NOTE(blogan): ORM does not seem to update the model to the real state + # of the database, so I'm doing an explicit refresh for now. + context.session.refresh(flip) + return flip + + +def _delete_flip(context, id, address_type): + filters = {'address_type': address_type, '_deallocated': False} + + flip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, **filters) + if not flip: + raise q_exc.FloatingIpNotFound(id=id) + + current_ports = flip.ports + if address_type == ip_types.FLOATING: + if current_ports: + current_ports = [flip.ports[0]] + elif address_type == ip_types.SCALING: + current_ports = flip.ports + + context.session.begin() + try: + strategy_name = flip.network.get('ipam_strategy') + ipam_driver = ipam.IPAM_REGISTRY.get_strategy(strategy_name) + ipam_driver.deallocate_ip_address(context, flip) + + if current_ports: + db_api.port_disassociate_ip(context, current_ports, flip) + if flip.fixed_ips: + db_api.floating_ip_disassociate_all_fixed_ips(context, flip) + + context.session.commit() + except Exception: + context.session.rollback() + raise + + try: + driver = registry.DRIVER_REGISTRY.get_driver() + driver.remove_floating_ip(flip) + except Exception as e: + LOG.error('There was an error when trying to delete the floating ip ' + 'on the unicorn API. The ip has been cleaned up, but ' + 'may need to be handled manually in the unicorn API. ' + 'Error: %s' % e.message) + + def create_floatingip(context, content): """Allocate or reallocate a floating IP. @@ -54,91 +324,25 @@ def create_floatingip(context, content): """ LOG.info('create_floatingip %s for tenant %s and body %s' % (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') - - if not tenant_id: - tenant_id = context.tenant_id - + # TODO(blogan): Since the extension logic will reject any requests without + # floating_network_id, is this still needed? if not network_id: raise n_exc.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 n_exc.NetworkNotFound(net_id=network_id) - - fixed_ip = None + fixed_ip_address = content.get('fixed_ip_address') + ip_address = content.get('floating_ip_address') + port_id = content.get('port_id') port = None + port_fixed_ip = {} + + network = _get_network(context, network_id) if port_id: - port = db_api.port_find(context, id=port_id, scope=db_api.ONE) - - if not port: - raise n_exc.PortNotFound(port_id=port_id) - - if not port.ip_addresses or len(port.ip_addresses) == 0: - raise q_exc.NoAvailableFixedIpsForPort(port_id=port_id) - - if not fixed_ip_address: - fixed_ip = _get_next_available_fixed_ip(port) - if not fixed_ip: - raise q_exc.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 q_exc.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 q_exc.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 = CONF.QUARK.floating_ip_ipam_strategy - - if strategy_name.upper() == 'NETWORK': - 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) - - flip = new_addresses[0] - - if fixed_ip and port: - context.session.begin() - try: - flip = db_api.port_associate_ip(context, [port], flip, [port_id]) - flip = db_api.floating_ip_associate_fixed_ip(context, flip, - fixed_ip) - - flip_driver = registry.DRIVER_REGISTRY.get_driver() - - flip_driver.register_floating_ip(flip, port, fixed_ip) - context.session.commit() - except Exception: - context.session.rollback() - raise - + port = _get_port(context, port_id) + fixed_ip = _get_fixed_ip(context, fixed_ip_address, port) + port_fixed_ip = {port.id: {'port': port, 'fixed_ip': fixed_ip}} + flip = _allocate_ip(context, network, port, ip_address, ip_types.FLOATING) + _create_flip(context, flip, port_fixed_ip) return v._make_floating_ip_dict(flip, port_id) @@ -164,78 +368,11 @@ def update_floatingip(context, id, content): raise n_exc.BadRequest(resource='floating_ip', msg='port_id is required.') - port_id = content.get('port_id') - port = None - fixed_ip = None - current_port = None - - context.session.begin() - try: - flip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE) - if not flip: - raise q_exc.FloatingIpNotFound(id=id) - - current_ports = flip.ports - - if current_ports and len(current_ports) > 0: - current_port = current_ports[0] - - if not port_id and not current_port: - raise q_exc.FloatingIpUpdateNoPortIdSupplied() - - if port_id: - port = db_api.port_find(context, id=port_id, scope=db_api.ONE) - if not port: - raise n_exc.PortNotFound(port_id=port_id) - - if any(ip for ip in port.ip_addresses - if (ip.get('address_type') == ip_types.FLOATING)): - raise q_exc.PortAlreadyContainsFloatingIp(port_id=port_id) - - if current_port and current_port.id == port_id: - d = dict(flip_id=id, port_id=port_id) - raise q_exc.PortAlreadyAssociatedToFloatingIp(**d) - - fixed_ip = _get_next_available_fixed_ip(port) - LOG.info('new fixed ip: %s' % fixed_ip) - if not fixed_ip: - raise q_exc.NoAvailableFixedIpsForPort(port_id=port_id) - - LOG.info('current ports: %s' % current_ports) - - if current_port: - flip = db_api.port_disassociate_ip(context, [current_port], flip) - - if flip.fixed_ip: - flip = db_api.floating_ip_disassociate_fixed_ip(context, flip) - - if port: - flip = db_api.port_associate_ip(context, [port], flip, [port_id]) - flip = db_api.floating_ip_associate_fixed_ip(context, flip, - fixed_ip) - - flip_driver = registry.DRIVER_REGISTRY.get_driver() - - if port: - if current_port: - flip_driver.update_floating_ip(flip, port, fixed_ip) - else: - flip_driver.register_floating_ip(flip, port, fixed_ip) - else: - flip_driver.remove_floating_ip(flip) - - context.session.commit() - except (q_exc.RegisterFloatingIpFailure, q_exc.RemoveFloatingIpFailure): - context.session.rollback() - raise - - # Note(alanquillin) The ports parameters on the model is not - # properly getting cleaned up when removed. Manually cleaning them up. - # Need to fix the db api to correctly update the model. - if not port: - flip.ports = [] - - return v._make_floating_ip_dict(flip, port_id) + requested_ports = [] + if content.get('port_id'): + requested_ports = [{'port_id': content.get('port_id')}] + flip = _update_flip(context, id, ip_types.FLOATING, requested_ports) + return v._make_floating_ip_dict(flip) def delete_floatingip(context, id): @@ -247,43 +384,7 @@ def delete_floatingip(context, id): LOG.info('delete_floatingip %s for tenant %s' % (id, context.tenant_id)) - filters = {'address_type': ip_types.FLOATING, '_deallocated': False} - - flip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, **filters) - if not flip: - raise q_exc.FloatingIpNotFound(id=id) - - current_ports = flip.ports - current_port = None - - if current_ports and len(current_ports) > 0: - current_port = current_ports[0] - - context.session.begin() - try: - strategy_name = flip.network.get('ipam_strategy') - ipam_driver = ipam.IPAM_REGISTRY.get_strategy(strategy_name) - ipam_driver.deallocate_ip_address(context, flip) - - if current_port: - flip = db_api.port_disassociate_ip(context, [current_port], - flip) - if flip.fixed_ip: - flip = db_api.floating_ip_disassociate_fixed_ip(context, flip) - - context.session.commit() - except Exception: - context.session.rollback() - raise - - try: - driver = registry.DRIVER_REGISTRY.get_driver() - driver.remove_floating_ip(flip) - except Exception as e: - LOG.error('There was an error when trying to delete the floating ip ' - 'on the unicorn API. The ip has been cleaned up, but ' - 'may need to be handled manually in the unicorn API. ' - 'Error: %s' % e.message) + _delete_flip(context, id, ip_types.FLOATING) def get_floatingip(context, id, fields=None): @@ -337,14 +438,8 @@ def get_floatingips(context, filters=None, fields=None, sorts=None, limit=None, LOG.info('get_floatingips for tenant %s filters %s fields %s' % (context.tenant_id, filters, fields)) - if filters is None: - filters = {} - - filters['_deallocated'] = False - filters['address_type'] = ip_types.FLOATING - - floating_ips = db_api.floating_ip_find(context, scope=db_api.ALL, - **filters) + floating_ips = _get_ips_by_type(context, ip_types.FLOATING, + filters=filters, fields=fields) return [v._make_floating_ip_dict(flip) for flip in floating_ips] @@ -383,18 +478,113 @@ def get_floatingips_count(context, filters=None): return count -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] +def create_scalingip(context, content): + """Allocate or reallocate a scaling IP. - if not fixed_ips or len(fixed_ips) == 0: - return None + :param context: neutron api request context. + :param content: dictionary describing the scaling ip, with keys + as listed in the RESOURCE_ATTRIBUTE_MAP object in + neutron/api/v2/attributes.py. All keys will be populated. - used = [ip.fixed_ip.address for ip in floating_ips - if ip and ip.fixed_ip] + :returns: Dictionary containing details for the new scaling IP. If values + are declared in the fields parameter, then only those keys will be + present. + """ + LOG.info('create_scalingip for tenant %s and body %s', + context.tenant_id, content) + network_id = content.get('scaling_network_id') + ip_address = content.get('scaling_ip_address') + requested_ports = content.get('ports', []) - return next((ip for ip in sorted(fixed_ips, - key=lambda ip: ip.get('allocated_at')) - if ip.address not in used), None) + network = _get_network(context, network_id) + port_fixed_ips = {} + for req_port in requested_ports: + port = _get_port(context, req_port['port_id']) + fixed_ip = _get_fixed_ip(context, req_port.get('fixed_ip_address'), + port) + port_fixed_ips[port.id] = {"port": port, "fixed_ip": fixed_ip} + scip = _allocate_ip(context, network, None, ip_address, ip_types.SCALING) + _create_flip(context, scip, port_fixed_ips) + return v._make_scaling_ip_dict(scip) + + +def update_scalingip(context, id, content): + """Update an existing scaling IP. + + :param context: neutron api request context. + :param id: id of the scaling ip + :param content: dictionary with keys indicating fields to update. + valid keys are those that have a value of True for 'allow_put' + as listed in the RESOURCE_ATTRIBUTE_MAP object in + neutron/api/v2/attributes.py. + + :returns: Dictionary containing details for the new scaling IP. If values + are declared in the fields parameter, then only those keys will be + present. + """ + LOG.info('update_scalingip %s for tenant %s and body %s' % + (id, context.tenant_id, content)) + requested_ports = content.get('ports', []) + flip = _update_flip(context, id, ip_types.SCALING, requested_ports) + return v._make_scaling_ip_dict(flip) + + +def delete_scalingip(context, id): + """Deallocate a scaling IP. + + :param context: neutron api request context. + :param id: id of the scaling ip + """ + LOG.info('delete_scalingip %s for tenant %s' % (id, context.tenant_id)) + _delete_flip(context, id, ip_types.SCALING) + + +def get_scalingip(context, id, fields=None): + """Retrieve a scaling IP. + + :param context: neutron api request context. + :param id: The UUID of the scaling IP. + :param fields: a list of strings that are valid keys in a + scaling IP dictionary as listed in the RESOURCE_ATTRIBUTE_MAP + object in neutron/api/v2/attributes.py. Only these fields + will be returned. + + :returns: Dictionary containing details for the scaling IP. If values + are declared in the fields parameter, then only those keys will be + present. + """ + LOG.info('get_scalingip %s for tenant %s' % (id, context.tenant_id)) + filters = {'address_type': ip_types.SCALING, '_deallocated': False} + scaling_ip = db_api.floating_ip_find(context, id=id, scope=db_api.ONE, + **filters) + if not scaling_ip: + raise q_exc.ScalingIpNotFound(id=id) + return v._make_scaling_ip_dict(scaling_ip) + + +def get_scalingips(context, filters=None, fields=None, sorts=None, limit=None, + marker=None, page_reverse=False): + """Retrieve a list of scaling ips. + + :param context: neutron api request context. + :param filters: a dictionary with keys that are valid keys for + a scaling ip as listed in the RESOURCE_ATTRIBUTE_MAP object + in neutron/api/v2/attributes.py. Values in this dictionary + are an iterable containing values that will be used for an exact + match comparison for that value. Each result returned by this + function will have matched one of the values for each key in + filters. + :param fields: a list of strings that are valid keys in a + scaling IP dictionary as listed in the RESOURCE_ATTRIBUTE_MAP + object in neutron/api/v2/attributes.py. Only these fields + will be returned. + + :returns: List of scaling IPs that are accessible to the tenant who + submits the request (as indicated by the tenant id of the context) + as well as any filters. + """ + LOG.info('get_scalingips for tenant %s filters %s fields %s' % + (context.tenant_id, filters, fields)) + scaling_ips = _get_ips_by_type(context, ip_types.SCALING, + filters=filters, fields=fields) + return [v._make_scaling_ip_dict(scip) for scip in scaling_ips] diff --git a/quark/plugin_views.py b/quark/plugin_views.py index 8fe9c8b..54c12e6 100644 --- a/quark/plugin_views.py +++ b/quark/plugin_views.py @@ -330,7 +330,7 @@ def _make_floating_ip_dict(flip, port_id=None): if ports and len(ports) > 0: port_id = None if not ports[0] else ports[0].id - fixed_ip = flip.fixed_ip + fixed_ip = flip.fixed_ips[0] if flip.fixed_ips else None return {"id": flip.get("id"), "floating_network_id": flip.get("network_id"), @@ -340,3 +340,16 @@ def _make_floating_ip_dict(flip, port_id=None): "tenant_id": flip.get("used_by_tenant_id"), "status": "RESERVED" if not port_id else "ASSOCIATED", "port_id": port_id} + + +def _make_scaling_ip_dict(flip): + # Can an IPAddress.fixed_ip have more than one port associated with it? + ports = [{"port_id": fixed_ip.ports[0].id, + "fixed_ip_address": fixed_ip.address_readable} + for fixed_ip in flip.fixed_ips] + return {"id": flip.get("id"), + "scaling_ip_address": None if not flip else flip.formatted(), + "scaling_network_id": flip.get("network_id"), + "tenant_id": flip.get("used_by_tenant_id"), + "status": flip.get("status"), + "ports": ports} diff --git a/quark/tests/functional/plugin_modules/test_floating_ips.py b/quark/tests/functional/plugin_modules/test_floating_ips.py new file mode 100644 index 0000000..990038a --- /dev/null +++ b/quark/tests/functional/plugin_modules/test_floating_ips.py @@ -0,0 +1,201 @@ +import json +import mock +import netaddr +from neutron import context +from oslo_config import cfg + +from quark.db import api as db_api +from quark import exceptions as qexceptions +import quark.ipam +from quark import network_strategy +import quark.plugin +import quark.plugin_modules.mac_address_ranges as macrng_api +from quark.tests.functional import base + + +class BaseFloatingIPTest(base.BaseFunctionalTest): + + FAKE_UNICORN_URL = 'http://unicorn.xxx' + + def _setup_mock_requests(self): + cfg.CONF.set_override('floating_ip_base_url', self.FAKE_UNICORN_URL, + group='QUARK') + patcher = mock.patch('quark.drivers.unicorn_driver.requests') + self.mock_requests = patcher.start() + self.addCleanup(patcher.stop) + self.mock_requests.post.return_value.status_code = 200 + self.mock_requests.delete.return_value.status_code = 204 + self.mock_requests.put.return_value.status_code = 200 + + def _build_expected_unicorn_request_body(self, floating_ip_address, ports, + actual_body=None): + if actual_body: + # Since the port order is non-deterministic, we need to ensure + # that the order is correct + actual_port_ids = [endpoint['port']['uuid'] for endpoint in + actual_body['floating_ip']['endpoints']] + reordered_ports = [] + for port_id in actual_port_ids: + for port in ports: + if port['id'] == port_id: + reordered_ports.append(port) + ports = reordered_ports + endpoints = [] + for port in ports: + fixed_ips = [] + for fixed_ip in port['fixed_ips']: + fixed_ips.append({ + 'ip_address': fixed_ip['ip_address'], + 'version': self.user_subnet['ip_version'], + 'subnet_id': self.user_subnet['id'], + 'cidr': self.user_subnet['cidr'], + 'address_type': 'fixed' + }) + port_mac = int(netaddr.EUI(port['mac_address'].replace(':', '-'))) + endpoints.append({ + 'port': { + 'uuid': port['id'], + 'name': port['name'], + 'network_uuid': port['network_id'], + 'mac_address': port_mac, + 'device_id': port['device_id'], + 'device_owner': port['device_owner'], + 'fixed_ip': fixed_ips + }, + 'private_ip': port['fixed_ips'][0]['ip_address'] + }) + body = {'public_ip': floating_ip_address, + 'endpoints': endpoints} + return {'floating_ip': body} + + def setUp(self): + super(BaseFloatingIPTest, self).setUp() + self.public_net_id = "00000000-0000-0000-0000-000000000000" + net_stat = '{"%s": {}}' % self.public_net_id + cfg.CONF.set_override('default_net_strategy', net_stat, group='QUARK') + old_strat = db_api.STRATEGY + + def reset_strategy(): + db_api.STRATEGY = old_strat + + db_api.STRATEGY = network_strategy.JSONStrategy() + self.addCleanup(reset_strategy) + admin_ctx = context.get_admin_context() + self._setup_mock_requests() + self.plugin = quark.plugin.Plugin() + mac = {'mac_address_range': dict(cidr="AA:BB:CC")} + macrng_api.create_mac_address_range(admin_ctx, mac) + with admin_ctx.session.begin(): + tenant = 'rackspace' + floating_net = dict(name='publicnet', tenant_id=tenant, + id=self.public_net_id) + self.floating_network = db_api.network_create( + self.context, **floating_net) + self.pub_net_cidr = "10.1.1.0/24" + floating_subnet = dict(id=self.public_net_id, + cidr=self.pub_net_cidr, + ip_policy=None, tenant_id=tenant, + segment_id='floating_ip', + network_id=self.floating_network.id) + self.floating_subnet = db_api.subnet_create( + self.context, **floating_subnet) + user_net = dict(name='user_network', tenant_id='fake') + self.user_network = self.plugin.create_network( + self.context, {'network': user_net}) + user_subnet = dict(cidr="192.168.1.0/24", + ip_policy=None, tenant_id="fake", + network_id=self.user_network['id']) + self.user_subnet = self.plugin.create_subnet( + self.context, {'subnet': user_subnet}) + user_port1 = dict(name='user_port1', + network_id=self.user_network['id']) + self.user_port1 = self.plugin.create_port( + self.context, {'port': user_port1}) + user_port2 = dict(name='user_port2', + network_id=self.user_network['id']) + self.user_port2 = self.plugin.create_port( + self.context, {'port': user_port2}) + + +class TestFloatingIPs(BaseFloatingIPTest): + + def test_create(self): + flip_req = dict( + floating_network_id=self.floating_network['id'], + port_id=self.user_port1['id'] + ) + flip_req = {'floatingip': flip_req} + flip = self.plugin.create_floatingip(self.context, flip_req) + self.assertIn(netaddr.IPAddress(flip['floating_ip_address']), + list(netaddr.IPNetwork(self.pub_net_cidr))) + self.assertEqual(self.floating_network['id'], + flip['floating_network_id']) + self.assertEqual(self.user_port1['id'], flip['port_id']) + self.assertEqual(self.user_port1['fixed_ips'][0]['ip_address'], + flip['fixed_ip_address']) + self.mock_requests.post.assert_called_once_with( + self.FAKE_UNICORN_URL, data=mock.ANY, timeout=2 + ) + actual_body = json.loads(self.mock_requests.post.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + flip['floating_ip_address'], [self.user_port1] + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + get_flip = self.plugin.get_floatingip(self.context, flip['id']) + self.assertEqual(flip['floating_ip_address'], + get_flip['floating_ip_address']) + + def test_update_floating_ip(self): + floating_ip = dict( + floating_network_id=self.floating_network.id, + port_id=self.user_port1['id'] + ) + floating_ip = {'floatingip': floating_ip} + flip = self.plugin.create_floatingip(self.context, floating_ip) + fixed_ip_address2 = self.user_port2['fixed_ips'][0]['ip_address'] + floating_ip = dict(port_id=self.user_port2['id'], + fixed_ip_address=fixed_ip_address2) + updated_flip = self.plugin.update_floatingip( + self.context, flip['id'], {"floatingip": floating_ip}) + self.assertEqual(self.floating_network['id'], + updated_flip['floating_network_id']) + self.assertEqual(updated_flip['floating_ip_address'], + flip['floating_ip_address']) + self.assertEqual(self.user_port2['id'], updated_flip['port_id']) + self.assertEqual(self.user_port2['fixed_ips'][0]['ip_address'], + updated_flip['fixed_ip_address']) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + flip['floating_ip_address']]) + self.mock_requests.put.assert_called_once_with( + expected_url, data=mock.ANY, timeout=2 + ) + actual_body = json.loads(self.mock_requests.put.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + flip['floating_ip_address'], [self.user_port2] + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + get_flip = self.plugin.get_floatingip(self.context, flip['id']) + self.assertEqual(flip['floating_ip_address'], + get_flip['floating_ip_address']) + + def test_delete_floating_ip(self): + floating_ip = dict( + floating_network_id=self.floating_network.id, + port_id=self.user_port1['id'] + ) + flip = self.plugin.create_floatingip( + self.context, {"floatingip": floating_ip}) + self.plugin.delete_floatingip(self.context, flip['id']) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + flip['floating_ip_address']]) + self.mock_requests.delete.assert_called_once_with( + expected_url, timeout=2 + ) + self.assertRaises(qexceptions.FloatingIpNotFound, + self.plugin.get_floatingip, self.context, flip['id']) + flips = self.plugin.get_floatingips(self.context) + self.assertEqual(0, len(flips)) diff --git a/quark/tests/functional/plugin_modules/test_scaling_ips.py b/quark/tests/functional/plugin_modules/test_scaling_ips.py new file mode 100644 index 0000000..f7052fc --- /dev/null +++ b/quark/tests/functional/plugin_modules/test_scaling_ips.py @@ -0,0 +1,248 @@ +import json +import mock +import netaddr +from neutron_lib import exceptions as n_exc + +from quark import exceptions as qexceptions +from quark.tests.functional.plugin_modules import test_floating_ips + + +class TestScalingIP(test_floating_ips.BaseFloatingIPTest): + + def setUp(self): + super(TestScalingIP, self).setUp() + self.scaling_network = self.floating_network + + def test_create_scaling_ip(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + scaling_ip = {'scalingip': scaling_ip} + scip = self.plugin.create_scalingip(self.context, scaling_ip) + self.assertIn(netaddr.IPAddress(scip['scaling_ip_address']), + list(netaddr.IPNetwork(self.pub_net_cidr))) + self.assertEqual(self.scaling_network['id'], + scip['scaling_network_id']) + self.assertEqual(2, len(scip['ports'])) + scip_ports = {scip_port['port_id']: scip_port['fixed_ip_address'] + for scip_port in scip['ports']} + port1_fixed_ip = self.user_port1['fixed_ips'][0]['ip_address'] + port2_fixed_ip = self.user_port2['fixed_ips'][0]['ip_address'] + self.assertIn(self.user_port1['id'], scip_ports) + self.assertIn(self.user_port2['id'], scip_ports) + self.assertIn(port1_fixed_ip, scip_ports.values()) + self.assertIn(port2_fixed_ip, scip_ports.values()) + self.mock_requests.post.assert_called_once_with( + self.FAKE_UNICORN_URL, data=mock.ANY, timeout=2 + ) + actual_body = json.loads(self.mock_requests.post.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + scip['scaling_ip_address'], [self.user_port1, self.user_port2], + actual_body=actual_body + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + get_scip = self.plugin.get_scalingip(self.context, scip['id']) + self.assertEqual(scip['scaling_ip_address'], + get_scip['scaling_ip_address']) + + def test_create_with_invalid_scaling_network_id(self): + scaling_ip = dict( + scaling_network_id='some-wrong-network-id', + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + self.assertRaises(n_exc.NetworkNotFound, + self.plugin.create_scalingip, + self.context, {"scalingip": scaling_ip}) + + def test_create_with_scaling_network_invalid_segment(self): + scaling_ip = dict( + scaling_network_id=self.user_network['id'], + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + self.assertRaises(n_exc.IpAddressGenerationFailure, + self.plugin.create_scalingip, + self.context, {"scalingip": scaling_ip}) + + def test_update_scaling_ip_add_port(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id'])] + ) + scaling_ip = {'scalingip': scaling_ip} + scip = self.plugin.create_scalingip(self.context, scaling_ip) + self.mock_requests.reset_mock() + scaling_ip = dict(ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])]) + updated_scip = self.plugin.update_scalingip( + self.context, scip['id'], {"scalingip": scaling_ip}) + self.assertEqual(self.scaling_network['id'], + updated_scip['scaling_network_id']) + self.assertEqual(updated_scip['scaling_ip_address'], + scip['scaling_ip_address']) + self.assertEqual(2, len(updated_scip['ports'])) + scip_ports = {scip_port['port_id']: scip_port['fixed_ip_address'] + for scip_port in updated_scip['ports']} + port1_fixed_ip = self.user_port1['fixed_ips'][0]['ip_address'] + port2_fixed_ip = self.user_port2['fixed_ips'][0]['ip_address'] + self.assertIn(self.user_port1['id'], scip_ports) + self.assertIn(self.user_port2['id'], scip_ports) + self.assertIn(port1_fixed_ip, scip_ports.values()) + self.assertIn(port2_fixed_ip, scip_ports.values()) + self.assertFalse(self.mock_requests.post.called) + self.assertFalse(self.mock_requests.delete.called) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + scip['scaling_ip_address']]) + self.mock_requests.put.assert_called_once_with( + expected_url, data=mock.ANY, timeout=2) + actual_body = json.loads(self.mock_requests.put.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + scip['scaling_ip_address'], [self.user_port1, self.user_port2], + actual_body=actual_body + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + + def test_update_scaling_ip_remove_port_with_remaining_ports(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + scaling_ip = {'scalingip': scaling_ip} + scip = self.plugin.create_scalingip(self.context, scaling_ip) + self.mock_requests.reset_mock() + scaling_ip = dict(ports=[dict(port_id=self.user_port1['id'])]) + updated_scip = self.plugin.update_scalingip( + self.context, scip['id'], {"scalingip": scaling_ip}) + self.assertEqual(self.scaling_network['id'], + updated_scip['scaling_network_id']) + self.assertEqual(updated_scip['scaling_ip_address'], + scip['scaling_ip_address']) + self.assertEqual(1, len(updated_scip['ports'])) + scip_ports = {scip_port['port_id']: scip_port['fixed_ip_address'] + for scip_port in updated_scip['ports']} + port1_fixed_ip = self.user_port1['fixed_ips'][0]['ip_address'] + self.assertIn(self.user_port1['id'], scip_ports) + self.assertIn(port1_fixed_ip, scip_ports.values()) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + scip['scaling_ip_address']]) + self.assertFalse(self.mock_requests.post.called) + self.assertFalse(self.mock_requests.delete.called) + self.mock_requests.put.assert_called_once_with( + expected_url, data=mock.ANY, timeout=2) + actual_body = json.loads(self.mock_requests.put.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + scip['scaling_ip_address'], [self.user_port1], + actual_body=actual_body + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + + def test_update_scaling_ip_clear_ports(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + scaling_ip = {'scalingip': scaling_ip} + scip = self.plugin.create_scalingip(self.context, scaling_ip) + self.mock_requests.reset_mock() + scaling_ip = dict(ports=[]) + updated_scip = self.plugin.update_scalingip( + self.context, scip['id'], {"scalingip": scaling_ip}) + self.assertEqual(self.scaling_network['id'], + updated_scip['scaling_network_id']) + self.assertEqual(updated_scip['scaling_ip_address'], + scip['scaling_ip_address']) + self.assertEqual(0, len(updated_scip['ports'])) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + scip['scaling_ip_address']]) + self.assertFalse(self.mock_requests.post.called) + self.assertFalse(self.mock_requests.put.called) + self.mock_requests.delete.assert_called_once_with( + expected_url, timeout=2) + + def test_update_scaling_ip_add_ports_from_none(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[] + ) + scaling_ip = {'scalingip': scaling_ip} + scip = self.plugin.create_scalingip(self.context, scaling_ip) + self.mock_requests.reset_mock() + scaling_ip = dict(ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])]) + updated_scip = self.plugin.update_scalingip( + self.context, scip['id'], {"scalingip": scaling_ip}) + self.assertEqual(self.scaling_network['id'], + updated_scip['scaling_network_id']) + self.assertEqual(updated_scip['scaling_ip_address'], + scip['scaling_ip_address']) + self.assertEqual(2, len(updated_scip['ports'])) + scip_ports = {scip_port['port_id']: scip_port['fixed_ip_address'] + for scip_port in updated_scip['ports']} + port1_fixed_ip = self.user_port1['fixed_ips'][0]['ip_address'] + port2_fixed_ip = self.user_port2['fixed_ips'][0]['ip_address'] + self.assertIn(self.user_port1['id'], scip_ports) + self.assertIn(self.user_port2['id'], scip_ports) + self.assertIn(port1_fixed_ip, scip_ports.values()) + self.assertIn(port2_fixed_ip, scip_ports.values()) + self.assertFalse(self.mock_requests.put.called) + self.assertFalse(self.mock_requests.delete.called) + self.mock_requests.post.assert_called_once_with( + self.FAKE_UNICORN_URL, data=mock.ANY, timeout=2) + actual_body = json.loads(self.mock_requests.post.call_args[1]['data']) + unicorn_body = self._build_expected_unicorn_request_body( + scip['scaling_ip_address'], [self.user_port1, self.user_port2], + actual_body=actual_body + ) + self.assertEqual(unicorn_body, actual_body, + msg="Request to the unicorn API is not what is " + "expected.") + + def test_delete_scaling_ip(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id']), + dict(port_id=self.user_port2['id'])] + ) + scip = self.plugin.create_scalingip( + self.context, {"scalingip": scaling_ip}) + self.plugin.delete_scalingip(self.context, scip['id']) + expected_url = '/'.join([self.FAKE_UNICORN_URL, + scip['scaling_ip_address']]) + self.mock_requests.delete.assert_called_once_with( + expected_url, timeout=2 + ) + self.assertRaises(qexceptions.ScalingIpNotFound, + self.plugin.get_scalingip, self.context, scip['id']) + scips = self.plugin.get_scalingips(self.context) + self.assertEqual(0, len(scips)) + + def test_scaling_ip_not_in_floating_ip_list(self): + scaling_ip = dict( + scaling_network_id=self.scaling_network.id, + ports=[dict(port_id=self.user_port1['id'])] + ) + scaling_ip = {'scalingip': scaling_ip} + self.plugin.create_scalingip(self.context, scaling_ip) + flips = self.plugin.get_floatingips(self.context) + self.assertEqual(0, len(flips)) + + def test_floating_ip_not_in_scaling_ip_list(self): + floating_ip = dict( + floating_network_id=self.scaling_network.id, + port_id=self.user_port1['id'] + ) + floating_ip = {'floatingip': floating_ip} + self.plugin.create_floatingip(self.context, floating_ip) + scips = self.plugin.get_scalingips(self.context) + self.assertEqual(0, len(scips)) diff --git a/quark/tests/plugin_modules/test_floating_ips.py b/quark/tests/plugin_modules/test_floating_ips.py index 1cfc9dd..43d54ef 100644 --- a/quark/tests/plugin_modules/test_floating_ips.py +++ b/quark/tests/plugin_modules/test_floating_ips.py @@ -222,7 +222,7 @@ class TestCreateFloatingIPs(test_quark_plugin.TestQuarkPlugin): return addr def _flip_fixed_ip_assoc(context, addr, fixed_ip): - addr.fixed_ip = fixed_ip + addr.fixed_ips.append(fixed_ip) return addr with contextlib.nested( @@ -463,6 +463,19 @@ class TestCreateFloatingIPs(test_quark_plugin.TestQuarkPlugin): class TestUpdateFloatingIPs(test_quark_plugin.TestQuarkPlugin): + + def setUp(self): + super(TestUpdateFloatingIPs, self).setUp() + # NOTE(blogan): yuck yuck yuck, but since the models are being mocked + # and not attached to the session, the refresh call will fail. + old_refresh = self.context.session.refresh + + def reset_refresh(context): + context.session.refresh = old_refresh + + self.context.session.refresh = mock.Mock() + self.addCleanup(reset_refresh, self.context) + @contextlib.contextmanager def _stubs(self, flip=None, curr_port=None, new_port=None, ips=None): curr_port_model = None @@ -508,7 +521,7 @@ class TestUpdateFloatingIPs(test_quark_plugin.TestQuarkPlugin): else new_port_model) def _flip_assoc(context, addr, fixed_ip): - addr.fixed_ip = fixed_ip + addr.fixed_ips.append(fixed_ip) return addr def _flip_disassoc(context, addr):