diff --git a/blazar/db/api.py b/blazar/db/api.py index cb06505c..0529186e 100644 --- a/blazar/db/api.py +++ b/blazar/db/api.py @@ -418,6 +418,77 @@ def host_get_all_by_queries_including_extracapabilities(queries): return IMPL.host_get_all_by_queries_including_extracapabilities(queries) +# FloatingIP reservation + +def fip_reservation_create(fip_reservation_values): + """Create a floating IP reservation from the values.""" + return IMPL.fip_reservation_create(fip_reservation_values) + + +@to_dict +def fip_reservation_get(fip_reservation_id): + """Return specific floating IP reservation.""" + return IMPL.fip_reservation_get(fip_reservation_id) + + +def fip_reservation_update(fip_reservation_id, fip_reservation_values): + """Update floating IP reservation.""" + return IMPL.fip_reservation_update(fip_reservation_id, + fip_reservation_values) + + +def fip_reservation_destroy(fip_reservation_id): + """Delete specific floating ip reservation.""" + return IMPL.fip_reservation_destroy(fip_reservation_id) + + +# Required FloatingIP + +def required_fip_create(required_fip_values): + """Create a required FIP address from the values.""" + return IMPL.required_fip_create(required_fip_values) + + +@to_dict +def required_fip_get(required_fip_id): + """Return specific required FIP.""" + return IMPL.required_fip_get(required_fip_id) + + +def required_fip_update(required_fip_id, required_fip_values): + """Update required FIP.""" + return IMPL.required_fip_update(required_fip_id, + required_fip_values) + + +def required_fip_destroy(required_fip_id): + """Delete specific required FIP.""" + return IMPL.required_fip_destroy(required_fip_id) + + +# FloatingIP Allocation + +def fip_allocation_create(allocation_values): + """Create a floating ip allocation from the values.""" + return IMPL.fip_allocation_create(allocation_values) + + +@to_dict +def fip_allocation_get_all_by_values(**kwargs): + """Returns all entries filtered by col=value.""" + return IMPL.fip_allocation_get_all_by_values(**kwargs) + + +def fip_allocation_destroy(allocation_id): + """Delete specific floating ip allocation.""" + IMPL.fip_allocation_destroy(allocation_id) + + +def fip_allocation_update(allocation_id, allocation_values): + """Update floating ip allocation.""" + IMPL.fip_allocation_update(allocation_id, allocation_values) + + # Floating ip def floatingip_create(values): @@ -437,6 +508,12 @@ def floatingip_list(): return IMPL.floatingip_list() +@to_dict +def reservable_fip_get_all_by_queries(queries): + """Returns reservable fips filtered by an array of queries.""" + return IMPL.reservable_fip_get_all_by_queries(queries) + + def floatingip_destroy(floatingip_id): """Delete specific floating ip.""" IMPL.floatingip_destroy(floatingip_id) diff --git a/blazar/db/sqlalchemy/api.py b/blazar/db/sqlalchemy/api.py index 0c7e3334..81a3b038 100644 --- a/blazar/db/sqlalchemy/api.py +++ b/blazar/db/sqlalchemy/api.py @@ -830,6 +830,172 @@ def host_extra_capability_get_all_per_name(host_id, capability_name): return query.filter_by(capability_name=capability_name).all() +# FloatingIP reservation + +def fip_reservation_create(fip_reservation_values): + values = fip_reservation_values.copy() + fip_reservation = models.FloatingIPReservation() + fip_reservation.update(values) + + session = get_session() + with session.begin(): + try: + fip_reservation.save(session=session) + except common_db_exc.DBDuplicateEntry as e: + # raise exception about duplicated columns (e.columns) + raise db_exc.BlazarDBDuplicateEntry( + model=fip_reservation.__class__.__name__, columns=e.columns) + + return fip_reservation_get(fip_reservation.id) + + +def _fip_reservation_get(session, fip_reservation_id): + query = model_query(models.FloatingIPReservation, session) + return query.filter_by(id=fip_reservation_id).first() + + +def fip_reservation_get(fip_reservation_id): + return _fip_reservation_get(get_session(), fip_reservation_id) + + +def fip_reservation_update(fip_reservation_id, fip_reservation_values): + session = get_session() + + with session.begin(): + fip_reservation = _fip_reservation_get(session, fip_reservation_id) + fip_reservation.update(fip_reservation_values) + fip_reservation.save(session=session) + + return fip_reservation_get(fip_reservation_id) + + +def fip_reservation_destroy(fip_reservation_id): + session = get_session() + with session.begin(): + fip_reservation = _fip_reservation_get(session, fip_reservation_id) + + if not fip_reservation: + # raise not found error + raise db_exc.BlazarDBNotFound( + id=fip_reservation_id, model='FloatingIPReservation') + + session.delete(fip_reservation) + + +# Required FIP + +def required_fip_create(required_fip_values): + values = required_fip_values.copy() + required_fip = models.RequiredFloatingIP() + required_fip.update(values) + + session = get_session() + with session.begin(): + try: + required_fip.save(session=session) + except common_db_exc.DBDuplicateEntry as e: + # raise exception about duplicated columns (e.columns) + raise db_exc.BlazarDBDuplicateEntry( + model=required_fip.__class__.__name__, columns=e.columns) + + return required_fip_get(required_fip.id) + + +def _required_fip_get(session, required_fip_id): + query = model_query(models.RequiredFloatingIP, session) + return query.filter_by(id=required_fip_id).first() + + +def required_fip_get(required_fip_id): + return _required_fip_get(get_session(), required_fip_id) + + +def required_fip_update(required_fip_id, required_fip_values): + session = get_session() + + with session.begin(): + required_fip = _required_fip_get(session, required_fip_id) + required_fip.update(required_fip_values) + required_fip.save(session=session) + + return required_fip_get(required_fip_id) + + +def required_fip_destroy(required_fip_id): + session = get_session() + with session.begin(): + required_fip = _required_fip_get(session, required_fip_id) + + if not required_fip: + # raise not found error + raise db_exc.BlazarDBNotFound( + id=required_fip_id, model='RequiredFloatingIP') + + session.delete(required_fip) + + +# FloatingIP Allocation + +def _fip_allocation_get(session, fip_allocation_id): + query = model_query(models.FloatingIPAllocation, session) + return query.filter_by(id=fip_allocation_id).first() + + +def fip_allocation_get(fip_allocation_id): + return _fip_allocation_get(get_session(), fip_allocation_id) + + +def fip_allocation_create(allocation_values): + values = allocation_values.copy() + fip_allocation = models.FloatingIPAllocation() + fip_allocation.update(values) + + session = get_session() + with session.begin(): + try: + fip_allocation.save(session=session) + except common_db_exc.DBDuplicateEntry as e: + # raise exception about duplicated columns (e.columns) + raise db_exc.BlazarDBDuplicateEntry( + model=fip_allocation.__class__.__name__, columns=e.columns) + + return fip_allocation_get(fip_allocation.id) + + +def fip_allocation_get_all_by_values(**kwargs): + """Returns all entries filtered by col=value.""" + allocation_query = model_query(models.FloatingIPAllocation, get_session()) + for name, value in kwargs.items(): + column = getattr(models.FloatingIPAllocation, name, None) + if column: + allocation_query = allocation_query.filter(column == value) + return allocation_query.all() + + +def fip_allocation_destroy(allocation_id): + session = get_session() + with session.begin(): + fip_allocation = _fip_allocation_get(session, allocation_id) + + if not fip_allocation: + # raise not found error + raise db_exc.BlazarDBNotFound( + id=allocation_id, model='FloatingIPAllocation') + + session.delete(fip_allocation) + + +def fip_allocation_update(allocation_id, allocation_values): + session = get_session() + + with session.begin(): + fip_allocation = _fip_allocation_get(session, allocation_id) + fip_allocation.update(allocation_values) + fip_allocation.save(session=session) + + return fip_allocation_get(allocation_id) + + # Floating IP def _floatingip_get(session, floatingip_id): query = model_query(models.FloatingIP, session) @@ -841,6 +1007,69 @@ def _floatingip_get_all(session): return query +def fip_get_all_by_queries(queries): + """Returns Floating IPs filtered by an array of queries. + + :param queries: array of queries "key op value" where op can be + http://docs.sqlalchemy.org/en/rel_0_7/core/expression_api.html + #sqlalchemy.sql.operators.ColumnOperators + + """ + fips_query = model_query(models.FloatingIP, get_session()) + + oper = { + '<': ['lt', lambda a, b: a >= b], + '>': ['gt', lambda a, b: a <= b], + '<=': ['le', lambda a, b: a > b], + '>=': ['ge', lambda a, b: a < b], + '==': ['eq', lambda a, b: a != b], + '!=': ['ne', lambda a, b: a == b], + } + + for query in queries: + try: + key, op, value = query.split(' ', 2) + except ValueError: + raise db_exc.BlazarDBInvalidFilter(query_filter=query) + + column = getattr(models.FloatingIP, key, None) + if column is not None: + if op == 'in': + filt = column.in_(value.split(',')) + else: + if op in oper: + op = oper[op][0] + try: + attr = [e for e in ['%s', '%s_', '__%s__'] + if hasattr(column, e % op)][0] % op + except IndexError: + raise db_exc.BlazarDBInvalidFilterOperator( + filter_operator=op) + + if value == 'null': + value = None + + filt = getattr(column, attr)(value) + + fips_query = fips_query.filter(filt) + else: + raise db_exc.BlazarDBInvalidFilter(query_filter=query) + + return fips_query.all() + + +def reservable_fip_get_all_by_queries(queries): + """Returns reservable fips filtered by an array of queries. + + :param queries: array of queries "key op value" where op can be + http://docs.sqlalchemy.org/en/rel_0_7/core/expression_api.html + #sqlalchemy.sql.operators.ColumnOperators + + """ + queries.append('reservable == 1') + return fip_get_all_by_queries(queries) + + def floatingip_get(floatingip_id): return _floatingip_get(get_session(), floatingip_id) diff --git a/blazar/db/sqlalchemy/utils.py b/blazar/db/sqlalchemy/utils.py index ec0f0e3a..b1a91648 100644 --- a/blazar/db/sqlalchemy/utils.py +++ b/blazar/db/sqlalchemy/utils.py @@ -61,6 +61,20 @@ def _get_leases_from_host_id(host_id, start_date, end_date): yield lease +def _get_leases_from_fip_id(fip_id, start_date, end_date): + session = get_session() + border0 = sa.and_(models.Lease.start_date < start_date, + models.Lease.end_date < start_date) + border1 = sa.and_(models.Lease.start_date > end_date, + models.Lease.end_date > end_date) + query = (session.query(models.Lease).join(models.Reservation) + .join(models.FloatingIPAllocation) + .filter(models.FloatingIPAllocation.floatingip_id == fip_id) + .filter(~sa.or_(border0, border1))) + for lease in query: + yield lease + + def get_reservations_by_host_id(host_id, start_date, end_date): session = get_session() border0 = sa.and_(models.Lease.start_date < start_date, @@ -115,12 +129,14 @@ def get_plugin_reservation(resource_type, resource_id): raise mgr_exceptions.UnsupportedResourceType(resource_type) -def get_free_periods(resource_id, start_date, end_date, duration): +def get_free_periods(resource_id, start_date, end_date, duration, + resource_type='host'): """Returns a list of free periods.""" reserved_periods = get_reserved_periods(resource_id, start_date, end_date, - duration) + duration, + resource_type=resource_type) free_periods = [] previous = (start_date, start_date) if len(reserved_periods) >= 1: @@ -137,10 +153,17 @@ def get_free_periods(resource_id, start_date, end_date, duration): return free_periods -def _get_events(host_id, start_date, end_date): +def _get_events(resource_id, start_date, end_date, resource_type): """Create a list of events.""" events = {} - for lease in _get_leases_from_host_id(host_id, start_date, end_date): + if resource_type == 'host': + leases = _get_leases_from_host_id(resource_id, start_date, end_date) + elif resource_type == 'floatingip': + leases = _get_leases_from_fip_id(resource_id, start_date, end_date) + else: + mgr_exceptions.UnsupportedResourceType(resource_type) + + for lease in leases: if lease.start_date < start_date: min_date = start_date else: @@ -202,26 +225,30 @@ def _merge_periods(reserved_periods, start_date, end_date, duration): return merged_reserved_periods -def get_reserved_periods(host_id, start_date, end_date, duration): - """Returns a list of reserved periods for a host. +def get_reserved_periods(resource_id, start_date, end_date, duration, + resource_type='host'): + """Returns a list of reserved periods for a resource. The get_reserved_periods function returns a list of periods during which - the host passed as parameter is reserved. The duration parameter allows to - choose the minimum length of time for a period to be considered free. + the resource passed as parameter is reserved. The duration parameter + allows to choose the minimum length of time for a period to be + considered free. - :param host_id: the host to consider + :param resource_id: the resource to consider :param start_date: start datetime of the entire period to consider :param end_date: end datetime of the entire period to consider :param duration: minimum length of time for a period to be considered free - :returns: the list of reserved periods for the host, expressed as a list of - two-element tuples, where the first element is the start datetime - of the reserved period and the second is the end datetime + :param resource_type: A type of resource to consider + :returns: the list of reserved periods for the resource, expressed as a + list of two-element tuples, where the first element is the start + datetime of the reserved period and the second is + the end datetime """ capacity = 1 # The resource status is binary (free or reserved) quantity = 1 # One reservation per host at the same time if end_date - start_date < duration: return [(start_date, end_date)] - events = _get_events(host_id, start_date, end_date) + events = _get_events(resource_id, start_date, end_date, resource_type) reserved_periods = _find_reserved_periods(events, quantity, capacity) return _merge_periods(reserved_periods, start_date, end_date, duration) diff --git a/blazar/db/utils.py b/blazar/db/utils.py index 61c77a87..770752c5 100644 --- a/blazar/db/utils.py +++ b/blazar/db/utils.py @@ -124,15 +124,18 @@ def get_plugin_reservation(resource_type, resource_id): return IMPL.get_plugin_reservation(resource_type, resource_id) -def get_free_periods(resource_id, start_date, end_date, duration): +def get_free_periods(resource_id, start_date, end_date, duration, + resource_type='host'): """Returns a list of free periods.""" - return IMPL.get_free_periods(resource_id, start_date, end_date, duration) + return IMPL.get_free_periods(resource_id, start_date, end_date, duration, + resource_type=resource_type) -def get_reserved_periods(resource_id, start_date, end_date, duration): +def get_reserved_periods(resource_id, start_date, end_date, duration, + resource_type='host'): """Returns a list of reserved periods.""" return IMPL.get_reserved_periods(resource_id, start_date, end_date, - duration) + duration, resource_type=resource_type) def reservation_ratio(resource_id, start_date, end_date): diff --git a/blazar/manager/exceptions.py b/blazar/manager/exceptions.py index 8751f773..a4b75dc3 100644 --- a/blazar/manager/exceptions.py +++ b/blazar/manager/exceptions.py @@ -202,3 +202,17 @@ class FloatingIPNotFound(exceptions.NotFound): class CantDeleteFloatingIP(exceptions.BlazarException): code = 409 msg_fmt = _("Can't delete floating IP %(floatingip)s. %(msg)s") + + +class InvalidIPFormat(exceptions.InvalidInput): + msg_fmt = _("IP address %(ip)s is invalid form.") + + +class TooLongFloatingIPs(exceptions.InvalidInput): + msg_fmt = _("Invalid values for required_floatingips and amount. " + "The amount must be equal to or longer than length of " + "required_floatingips.") + + +class NotEnoughFloatingIPAvailable(exceptions.InvalidInput): + msg_fmt = _("Not enough floating IPs available") diff --git a/blazar/plugins/floatingips/floatingip_plugin.py b/blazar/plugins/floatingips/floatingip_plugin.py index 22d51133..242aa260 100644 --- a/blazar/plugins/floatingips/floatingip_plugin.py +++ b/blazar/plugins/floatingips/floatingip_plugin.py @@ -12,17 +12,25 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime + +from oslo_config import cfg from oslo_log import log as logging +from oslo_utils import netutils +from oslo_utils import strutils from blazar.db import api as db_api from blazar.db import exceptions as db_ex +from blazar.db import utils as db_utils from blazar import exceptions from blazar.manager import exceptions as manager_ex from blazar.plugins import base from blazar.plugins import floatingips as plugin from blazar.utils.openstack import neutron +from blazar.utils import plugins as plugins_utils +CONF = cfg.CONF LOG = logging.getLogger(__name__) @@ -33,8 +41,58 @@ class FloatingIpPlugin(base.BasePlugin): title = 'Floating IP Plugin' description = 'This plugin creates and assigns floating IPs.' + def check_params(self, values): + if 'network_id' not in values: + raise manager_ex.MissingParameter(param='network_id') + + if 'amount' not in values: + raise manager_ex.MissingParameter(param='amount') + + if not strutils.is_int_like(values['amount']): + raise manager_ex.MalformedParameter(param='amount') + + # required_floatingips param is an optional parameter + fips = values.get('required_floatingips', []) + if not isinstance(fips, list): + manager_ex.MalformedParameter(param='required_floatingips') + + for ip in fips: + if not (netutils.is_valid_ipv4(ip) or netutils.is_valid_ipv6(ip)): + raise manager_ex.InvalidIPFormat(ip=ip) + def reserve_resource(self, reservation_id, values): - raise NotImplementedError + """Create floating IP reservation.""" + self.check_params(values) + + required_fips = values.get('required_floatingips', []) + amount = int(values['amount']) + + if len(required_fips) > amount: + raise manager_ex.TooLongFloatingIPs() + + floatingip_ids = self._matching_fips(values['network_id'], + required_fips, + amount, + values['start_date'], + values['end_date']) + + floatingip_rsrv_values = { + 'reservation_id': reservation_id, + 'network_id': values['network_id'], + 'amount': amount + } + + fip_reservation = db_api.fip_reservation_create(floatingip_rsrv_values) + for fip_address in required_fips: + fip_address_values = { + 'address': fip_address, + 'floatingip_reservation_id': fip_reservation['id'] + } + db_api.required_fip_create(fip_address_values) + for fip_id in floatingip_ids: + db_api.fip_allocation_create({'floatingip_id': fip_id, + 'reservation_id': reservation_id}) + return fip_reservation['id'] def on_start(self, resource_id): raise NotImplementedError @@ -42,6 +100,54 @@ class FloatingIpPlugin(base.BasePlugin): def on_end(self, resource_id): raise NotImplementedError + def _matching_fips(self, network_id, fip_addresses, amount, + start_date, end_date): + filter_array = [] + start_date_with_margin = start_date - datetime.timedelta( + minutes=CONF.cleaning_time) + end_date_with_margin = end_date + datetime.timedelta( + minutes=CONF.cleaning_time) + + fip_query = ["==", "$floating_network_id", network_id] + filter_array = plugins_utils.convert_requirements(fip_query) + + fip_ids = [] + not_allocated_fip_ids = [] + allocated_fip_ids = [] + for fip in db_api.reservable_fip_get_all_by_queries(filter_array): + if not db_api.fip_allocation_get_all_by_values( + floatingip_id=fip['id']): + if fip['floating_ip_address'] in fip_addresses: + fip_ids.append(fip['id']) + else: + not_allocated_fip_ids.append(fip['id']) + elif db_utils.get_free_periods( + fip['id'], + start_date_with_margin, + end_date_with_margin, + end_date_with_margin - start_date_with_margin, + resource_type='floatingip' + ) == [ + (start_date_with_margin, end_date_with_margin), + ]: + if fip['floating_ip_address'] in fip_addresses: + fip_ids.append(fip['id']) + else: + allocated_fip_ids.append(fip['id']) + + if len(fip_ids) != len(fip_addresses): + raise manager_ex.NotEnoughFloatingIPAvailable() + + fip_ids += not_allocated_fip_ids + if len(fip_ids) >= amount: + return fip_ids[:amount] + + fip_ids += allocated_fip_ids + if len(fip_ids) >= amount: + return fip_ids[:amount] + + raise manager_ex.NotEnoughFloatingIPAvailable() + def validate_floatingip_params(self, values): marshall_attributes = set(['floating_network_id', 'floating_ip_address']) diff --git a/blazar/tests/plugins/floatingips/test_floatingip_plugin.py b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py index 14df5620..c958f26c 100644 --- a/blazar/tests/plugins/floatingips/test_floatingip_plugin.py +++ b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py @@ -12,20 +12,36 @@ # License for the specific language governing permissions and limitations # under the License. +import datetime + import mock +from oslo_config import cfg from blazar.db import api as db_api +from blazar.db import utils as db_utils from blazar.manager import exceptions as mgr_exceptions +from blazar.plugins import floatingips as plugin from blazar.plugins.floatingips import floatingip_plugin from blazar import tests from blazar.utils.openstack import exceptions as opst_exceptions from blazar.utils.openstack import neutron +CONF = cfg.CONF + + class FloatingIpPluginTest(tests.TestCase): def setUp(self): super(FloatingIpPluginTest, self).setUp() + + self.cfg = cfg + + # Make sure we clean up any override which could impact other tests + self.addCleanup(self.cfg.CONF.reset) + + self.db_api = db_api + self.db_utils = db_utils self.fip_pool = self.patch(neutron, 'FloatingIPPool') def test_create_floatingip(self): @@ -143,3 +159,265 @@ class FloatingIpPluginTest(tests.TestCase): self.assertRaises(mgr_exceptions.FloatingIPNotFound, fip_plugin.delete_floatingip, 'non-exists-id') + + def test_create_reservation_fips_available(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'network_id': u'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'amount': 2 + } + matching_fips = self.patch(fip_plugin, '_matching_fips') + matching_fips.return_value = ['fip1', 'fip2'] + fip_reservation_create = self.patch(self.db_api, + 'fip_reservation_create') + fip_allocation_create = self.patch( + self.db_api, 'fip_allocation_create') + fip_plugin.reserve_resource( + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + fip_values = { + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': u'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'amount': 2 + } + fip_reservation_create.assert_called_once_with(fip_values) + calls = [ + mock.call( + {'floatingip_id': 'fip1', + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + }), + mock.call( + {'floatingip_id': 'fip2', + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + }), + ] + fip_allocation_create.assert_has_calls(calls) + + def test_create_reservation_fips_with_required(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'network_id': u'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'amount': 2, + 'required_floatingips': ['172.24.4.100'] + } + matching_fips = self.patch(fip_plugin, '_matching_fips') + matching_fips.return_value = ['fip1', 'fip2'] + fip_reservation_create = self.patch(self.db_api, + 'fip_reservation_create') + fip_reservation_create.return_value = {'id': 'fip_resv_id1'} + fip_allocation_create = self.patch( + self.db_api, 'fip_allocation_create') + required_addr_create = self.patch(self.db_api, 'required_fip_create') + fip_plugin.reserve_resource( + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + fip_values = { + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': u'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'amount': 2 + } + fip_reservation_create.assert_called_once_with(fip_values) + required_addr_create.assert_called_once_with( + { + 'address': '172.24.4.100', + 'floatingip_reservation_id': 'fip_resv_id1' + }) + calls = [ + mock.call( + {'floatingip_id': 'fip1', + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + }), + mock.call( + {'floatingip_id': 'fip2', + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + }), + ] + fip_allocation_create.assert_has_calls(calls) + + def test_create_reservation_with_missing_param_network(self): + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'amount': 2, + 'start_date': datetime.datetime(2017, 3, 1, 20, 0), + 'end_date': datetime.datetime(2017, 3, 2, 20, 0), + 'resource_type': plugin.RESOURCE_TYPE, + } + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises( + mgr_exceptions.MissingParameter, + fip_plugin.reserve_resource, + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + + def test_create_reservation_with_invalid_fip(self): + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'network_id': u'a37a14f3-e3eb-4fe2-9e36-082b67f12ea0', + 'amount': 2, + 'required_floatingips': ['aaa.aaa.aaa.aaa'], + 'start_date': datetime.datetime(2017, 3, 1, 20, 0), + 'end_date': datetime.datetime(2017, 3, 2, 20, 0), + 'resource_type': plugin.RESOURCE_TYPE, + } + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises( + mgr_exceptions.InvalidIPFormat, + fip_plugin.reserve_resource, + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + + def test_create_reservation_required_bigger_than_amount(self): + values = { + 'lease_id': u'018c1b43-e69e-4aef-a543-09681539cf4c', + 'network_id': u'a37a14f3-e3eb-4fe2-9e36-082b67f12ea0', + 'amount': 1, + 'required_floatingips': ['172.24.4.100', '172.24.4.101'], + 'start_date': datetime.datetime(2017, 3, 1, 20, 0), + 'end_date': datetime.datetime(2017, 3, 2, 20, 0), + 'resource_type': plugin.RESOURCE_TYPE, + } + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises( + mgr_exceptions.TooLongFloatingIPs, + fip_plugin.reserve_resource, + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + + def test_matching_fips_not_allocated_fips(self): + def fip_allocation_get_all_by_values(**kwargs): + if kwargs['floatingip_id'] == 'fip1': + return [{'id': 'allocation-id1'}] + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + fip_get = self.patch(self.db_api, 'reservable_fip_get_all_by_queries') + fip_get.return_value = [ + {'id': 'fip1', 'floating_ip_address': '172.24.4.101'}, + {'id': 'fip2', 'floating_ip_address': '172.24.4.102'}, + {'id': 'fip3', 'floating_ip_address': '172.24.4.103'}, + ] + fip_get = self.patch(self.db_api, 'fip_allocation_get_all_by_values') + fip_get.side_effect = fip_allocation_get_all_by_values + fip_get = self.patch(self.db_utils, 'get_free_periods') + fip_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)), + ] + result = fip_plugin._matching_fips( + 'network-id', [], 2, + datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)) + self.assertEqual(['fip2', 'fip3'], result) + + def test_matching_fips_allocated_fips(self): + def fip_allocation_get_all_by_values(**kwargs): + return [{'id': kwargs['floatingip_id']}] + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + fip_get = self.patch(self.db_api, 'reservable_fip_get_all_by_queries') + fip_get.return_value = [ + {'id': 'fip1', 'floating_ip_address': '172.24.4.101'}, + {'id': 'fip2', 'floating_ip_address': '172.24.4.102'}, + {'id': 'fip3', 'floating_ip_address': '172.24.4.103'}, + ] + fip_get = self.patch(self.db_api, 'fip_allocation_get_all_by_values') + fip_get.side_effect = fip_allocation_get_all_by_values + fip_get = self.patch(self.db_utils, 'get_free_periods') + fip_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)), + ] + result = fip_plugin._matching_fips( + 'network-id', [], 3, + datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)) + self.assertEqual(['fip1', 'fip2', 'fip3'], result) + + def test_matching_fips_allocated_fips_with_required(self): + def fip_allocation_get_all_by_values(**kwargs): + if kwargs['floatingip_id'] == 'fip1': + return [{'id': 'allocation-id1'}] + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + fip_get = self.patch(self.db_api, 'reservable_fip_get_all_by_queries') + fip_get.return_value = [ + {'id': 'fip1', 'floating_ip_address': '172.24.4.101'}, + {'id': 'fip2', 'floating_ip_address': '172.24.4.102'}, + {'id': 'fip3', 'floating_ip_address': '172.24.4.103'}, + {'id': 'fip4', 'floating_ip_address': '172.24.4.104'}, + ] + fip_get = self.patch(self.db_api, 'fip_allocation_get_all_by_values') + fip_get.side_effect = fip_allocation_get_all_by_values + fip_get = self.patch(self.db_utils, 'get_free_periods') + fip_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)), + ] + result = fip_plugin._matching_fips( + 'network-id', ['172.24.4.102'], 4, + datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)) + # The order must be 1. required fips, 2. non-allocated fips, + # then 3. allocated fips + self.assertEqual(['fip2', 'fip3', 'fip4', 'fip1'], result) + + def test_matching_fips_allocated_fips_with_cleaning_time(self): + def fip_allocation_get_all_by_values(**kwargs): + return [{'id': kwargs['floatingip_id']}] + + self.cfg.CONF.set_override('cleaning_time', '5') + fip_get = self.patch( + self.db_api, + 'reservable_fip_get_all_by_queries') + fip_get.return_value = [ + {'id': 'fip1', 'floating_ip_address': '172.24.4.101'}, + {'id': 'fip2', 'floating_ip_address': '172.24.4.102'}, + {'id': 'fip3', 'floating_ip_address': '172.24.4.103'}, + ] + fip_get = self.patch( + self.db_api, + 'fip_allocation_get_all_by_values') + fip_get.side_effect = fip_allocation_get_all_by_values + fip_get = self.patch( + self.db_utils, + 'get_free_periods') + fip_get.return_value = [ + (datetime.datetime(2013, 12, 19, 20, 0) + - datetime.timedelta(minutes=5), + datetime.datetime(2013, 12, 19, 21, 0) + + datetime.timedelta(minutes=5)) + ] + fip_plugin = floatingip_plugin.FloatingIpPlugin() + result = fip_plugin._matching_fips( + 'network-id', [], 3, + datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0)) + self.assertEqual(['fip1', 'fip2', 'fip3'], result) + start_mergin = (datetime.datetime(2013, 12, 19, 20, 0) + - datetime.timedelta(minutes=5)) + end_mergin = (datetime.datetime(2013, 12, 19, 21, 0) + + datetime.timedelta(minutes=5)) + calls = [mock.call(fip, start_mergin, end_mergin, + end_mergin - start_mergin, + resource_type='floatingip') + for fip in ['fip1', 'fip2', 'fip3']] + fip_get.assert_has_calls(calls) + + def test_matching_fips_not_matching(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + fip_get = self.patch( + self.db_api, + 'reservable_fip_get_all_by_queries') + fip_get.return_value = [] + self.assertRaises(mgr_exceptions.NotEnoughFloatingIPAvailable, + fip_plugin._matching_fips, + 'network-id', [], 2, + datetime.datetime(2013, 12, 19, 20, 0), + datetime.datetime(2013, 12, 19, 21, 0))