diff --git a/blazar/db/api.py b/blazar/db/api.py index 0529186e..243d9b24 100644 --- a/blazar/db/api.py +++ b/blazar/db/api.py @@ -466,6 +466,11 @@ def required_fip_destroy(required_fip_id): return IMPL.required_fip_destroy(required_fip_id) +def required_fip_destroy_by_fip_reservation_id(fip_reservation_id): + """Delete all required FIPs for a floating IP reservation.""" + return IMPL.required_fip_destroy_by_fip_reservation_id(fip_reservation_id) + + # FloatingIP Allocation def fip_allocation_create(allocation_values): diff --git a/blazar/db/sqlalchemy/api.py b/blazar/db/sqlalchemy/api.py index 81a3b038..97752d1d 100644 --- a/blazar/db/sqlalchemy/api.py +++ b/blazar/db/sqlalchemy/api.py @@ -934,6 +934,16 @@ def required_fip_destroy(required_fip_id): session.delete(required_fip) +def required_fip_destroy_by_fip_reservation_id(fip_reservation_id): + session = get_session() + with session.begin(): + required_fips = model_query( + models.RequiredFloatingIP, session).filter_by( + floatingip_reservation_id=fip_reservation_id) + for required_fip in required_fips: + required_fip_destroy(required_fip['id']) + + # FloatingIP Allocation def _fip_allocation_get(session, fip_allocation_id): diff --git a/blazar/manager/exceptions.py b/blazar/manager/exceptions.py index a4b75dc3..c3fbc06c 100644 --- a/blazar/manager/exceptions.py +++ b/blazar/manager/exceptions.py @@ -216,3 +216,9 @@ class TooLongFloatingIPs(exceptions.InvalidInput): class NotEnoughFloatingIPAvailable(exceptions.InvalidInput): msg_fmt = _("Not enough floating IPs available") + + +class CantUpdateFloatingIPReservation(exceptions.BlazarException): + code = 400 + msg_fmt = _("Floating IP reservation cannot be updated with requested " + "parameters. %(msg)s") diff --git a/blazar/plugins/base.py b/blazar/plugins/base.py index 60b0d2e0..395e3134 100644 --- a/blazar/plugins/base.py +++ b/blazar/plugins/base.py @@ -19,8 +19,6 @@ from oslo_config import cfg from oslo_log import log as logging import six -from blazar.db import api as db_api - LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -66,12 +64,10 @@ class BasePlugin(object): """Reserve resource.""" pass + @abc.abstractmethod def update_reservation(self, reservation_id, values): """Update reservation.""" - reservation_values = { - 'resource_id': values['resource_id'] - } - db_api.reservation_update(reservation_id, reservation_values) + pass @abc.abstractmethod def on_end(self, resource_id): diff --git a/blazar/plugins/floatingips/floatingip_plugin.py b/blazar/plugins/floatingips/floatingip_plugin.py index f3e23b9e..e14e6d4e 100644 --- a/blazar/plugins/floatingips/floatingip_plugin.py +++ b/blazar/plugins/floatingips/floatingip_plugin.py @@ -27,6 +27,7 @@ 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 import status from blazar.utils.openstack import neutron from blazar.utils import plugins as plugins_utils @@ -61,6 +62,84 @@ class FloatingIpPlugin(base.BasePlugin): if not (netutils.is_valid_ipv4(ip) or netutils.is_valid_ipv6(ip)): raise manager_ex.InvalidIPFormat(ip=ip) + def _update_allocations(self, dates_before, dates_after, reservation_id, + reservation_status, fip_reservation, values): + amount = int(values.get('amount', fip_reservation['amount'])) + fip_allocations = db_api.fip_allocation_get_all_by_values( + reservation_id=reservation_id) + allocs_to_remove = self._allocations_to_remove( + dates_before, dates_after, fip_allocations, amount) + + if (allocs_to_remove and + reservation_status == status.reservation.ACTIVE): + raise manager_ex.CantUpdateFloatingIPReservation( + msg="Cannot remove allocations from an active reservation") + + kept_fips = len(fip_allocations) - len(allocs_to_remove) + fip_ids_to_add = [] + + if kept_fips < amount: + needed_fips = amount - kept_fips + required_fips = values.get( + 'required_floatingips', + fip_reservation['required_floatingips']) + fip_ids_to_add = self._matching_fips( + fip_reservation['network_id'], required_fips, needed_fips, + dates_after['start_date'], dates_after['end_date']) + + if len(fip_ids_to_add) < needed_fips: + raise manager_ex.NotEnoughFloatingIPAvailable() + + for fip_id in fip_ids_to_add: + LOG.debug('Adding floating IP {} to reservation {}'.format( + fip_id, reservation_id)) + db_api.fip_allocation_create({ + 'floatingip_id': fip_id, + 'reservation_id': reservation_id}) + + for allocation in allocs_to_remove: + LOG.debug('Removing floating IP {} from reservation {}'.format( + allocation['floatingip_id'], reservation_id)) + db_api.fip_allocation_destroy(allocation['id']) + + def _allocations_to_remove(self, dates_before, dates_after, allocs, + amount): + """Find candidate floating IP allocations to remove.""" + allocs_to_remove = [] + + for alloc in allocs: + is_extension = ( + dates_before['start_date'] > dates_after['start_date'] or + dates_before['end_date'] < dates_after['end_date']) + + if is_extension: + reserved_periods = db_utils.get_reserved_periods( + alloc['floatingip_id'], + dates_after['start_date'], + dates_after['end_date'], + datetime.timedelta(seconds=1), + resource_type='floatingip') + + max_start = max(dates_before['start_date'], + dates_after['start_date']) + min_end = min(dates_before['end_date'], + dates_after['end_date']) + + if not (len(reserved_periods) == 0 or + (len(reserved_periods) == 1 and + reserved_periods[0][0] == max_start and + reserved_periods[0][1] == min_end)): + allocs_to_remove.append(alloc) + continue + + allocs_to_keep = [a for a in allocs if a not in allocs_to_remove] + + if len(allocs_to_keep) > amount: + allocs_to_remove.extend( + allocs_to_keep[:(len(allocs_to_keep) - amount)]) + + return allocs_to_remove + def reserve_resource(self, reservation_id, values): """Create floating IP reservation.""" self.check_params(values) @@ -95,6 +174,50 @@ class FloatingIpPlugin(base.BasePlugin): 'reservation_id': reservation_id}) return fip_reservation['id'] + def update_reservation(self, reservation_id, values): + """Update reservation.""" + reservation = db_api.reservation_get(reservation_id) + lease = db_api.lease_get(reservation['lease_id']) + dates_before = {'start_date': lease['start_date'], + 'end_date': lease['end_date']} + dates_after = {'start_date': values['start_date'], + 'end_date': values['end_date']} + fip_reservation = db_api.fip_reservation_get( + reservation['resource_id']) + + if ('network_id' in values and + values.get('network_id') != fip_reservation['network_id']): + raise manager_ex.CantUpdateFloatingIPReservation( + msg="Updating network_id is not supported") + + required_fips = fip_reservation['required_floatingips'] + if ('required_floatingips' in values and + values['required_floatingips'] != required_fips and + values['required_floatingips'] != []): + raise manager_ex.CantUpdateFloatingIPReservation( + msg="Updating required_floatingips is not supported except " + "with an empty list") + + self._update_allocations(dates_before, dates_after, reservation_id, + reservation['status'], fip_reservation, + values) + updates = {} + if 'amount' in values: + updates['amount'] = values.get('amount') + if updates: + db_api.fip_reservation_update(fip_reservation['id'], updates) + + if ('required_floatingips' in values and + values['required_floatingips'] != required_fips): + db_api.required_fip_destroy_by_fip_reservation_id( + fip_reservation['id']) + for fip_address in values.get('required_floatingips'): + fip_address_values = { + 'address': fip_address, + 'floatingip_reservation_id': fip_reservation['id'] + } + db_api.required_fip_create(fip_address_values) + def on_start(self, resource_id): fip_reservation = db_api.fip_reservation_get(resource_id) allocations = db_api.fip_allocation_get_all_by_values( diff --git a/blazar/tests/manager/test_service.py b/blazar/tests/manager/test_service.py index d12197ae..bb8fcc4e 100644 --- a/blazar/tests/manager/test_service.py +++ b/blazar/tests/manager/test_service.py @@ -54,6 +54,9 @@ class FakePlugin(base.BasePlugin): def reserve_resource(self, reservation_id, values): return None + def update_reservation(self, reservation_id, values): + return None + def on_start(self, resource_id): return 'Resource %s should be started this moment.' % resource_id diff --git a/blazar/tests/plugins/floatingips/test_floatingip_plugin.py b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py index 9e2a02b2..afd71b05 100644 --- a/blazar/tests/plugins/floatingips/test_floatingip_plugin.py +++ b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py @@ -320,6 +320,265 @@ class FloatingIpPluginTest(tests.TestCase): u'441c1476-9f8f-4700-9f30-cd9b6fef3509', values) + def test_update_reservation_increase_amount_fips_available(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + '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, + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_allocation_get_all_by_values = self.patch( + self.db_api, 'fip_allocation_get_all_by_values' + ) + fip_allocation_get_all_by_values.return_value = [{ + 'floatingip_id': 'fip1' + }] + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 1, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'required_floatingips': [], + } + matching_fips = self.patch(fip_plugin, '_matching_fips') + matching_fips.return_value = ['fip2'] + fip_reservation_update = self.patch(self.db_api, + 'fip_reservation_update') + fip_allocation_create = self.patch( + self.db_api, 'fip_allocation_create') + fip_plugin.update_reservation( + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + fip_reservation_update.assert_called_once_with( + 'fip_resv_id1', {'amount': 2}) + calls = [ + mock.call( + {'floatingip_id': 'fip2', + 'reservation_id': u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + }) + ] + fip_allocation_create.assert_has_calls(calls) + + def test_update_reservation_increase_amount_fips_unavailable(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + '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, + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_allocation_get_all_by_values = self.patch( + self.db_api, 'fip_allocation_get_all_by_values' + ) + fip_allocation_get_all_by_values.return_value = [{ + 'floatingip_id': 'fip1' + }] + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 1, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'required_floatingips': [], + } + matching_fips = self.patch(fip_plugin, '_matching_fips') + matching_fips.return_value = [] + self.assertRaises(mgr_exceptions.NotEnoughFloatingIPAvailable, + fip_plugin.update_reservation, + '441c1476-9f8f-4700-9f30-cd9b6fef3509', values) + + def test_update_reservation_decrease_amount(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'amount': 1, + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_allocation_get_all_by_values = self.patch( + self.db_api, 'fip_allocation_get_all_by_values' + ) + fip_allocation_get_all_by_values.return_value = [ + {'id': 'fip_alloc_1', 'floatingip_id': 'fip1'}, + {'id': 'fip_alloc_2', 'floatingip_id': 'fip2'}, + ] + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 2, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'required_floatingips': [], + } + fip_allocation_destroy = self.patch(self.db_api, + 'fip_allocation_destroy') + fip_reservation_update = self.patch(self.db_api, + 'fip_reservation_update') + fip_plugin.update_reservation( + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + fip_reservation_update.assert_called_once_with( + 'fip_resv_id1', {'amount': 1}) + calls = [ + mock.call('fip_alloc_1') + ] + fip_allocation_destroy.assert_has_calls(calls) + + def test_update_reservation_remove_required_fips(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'required_floatingips': [], + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_allocation_get_all_by_values = self.patch( + self.db_api, 'fip_allocation_get_all_by_values' + ) + fip_allocation_get_all_by_values.return_value = [{ + 'floatingip_id': 'fip1' + }] + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 1, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'required_floatingips': ['172.24.4.100'] + } + required_fip_destroy_by_fip_reservation_id = self.patch( + self.db_api, 'required_fip_destroy_by_fip_reservation_id') + fip_plugin.update_reservation( + u'441c1476-9f8f-4700-9f30-cd9b6fef3509', + values) + calls = [mock.call('fip_resv_id1')] + required_fip_destroy_by_fip_reservation_id.assert_has_calls(calls) + + def test_update_reservation_change_required_fips(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'required_floatingips': ['172.24.4.101'], + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 1, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + 'required_floatingips': ['172.24.4.100'] + } + self.assertRaises(mgr_exceptions.CantUpdateFloatingIPReservation, + fip_plugin.update_reservation, + '441c1476-9f8f-4700-9f30-cd9b6fef3509', values) + + def test_update_reservation_change_network_id(self): + fip_plugin = floatingip_plugin.FloatingIpPlugin() + values = { + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + 'resource_type': plugin.RESOURCE_TYPE, + 'network_id': 'new-network-id', + } + lease_get = self.patch(self.db_api, 'lease_get') + lease_get.return_value = { + 'id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'start_date': datetime.datetime(2013, 12, 19, 20, 0), + 'end_date': datetime.datetime(2013, 12, 19, 21, 0), + } + reservation_get = self.patch(self.db_api, 'reservation_get') + reservation_get.return_value = { + 'id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'lease_id': '018c1b43-e69e-4aef-a543-09681539cf4c', + 'resource_id': 'fip-reservation-id-1', + 'resource_type': 'virtual:floatingip', + 'status': 'pending', + } + fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') + fip_reservation_get.return_value = { + 'id': 'fip_resv_id1', + 'amount': 1, + 'reservation_id': '441c1476-9f8f-4700-9f30-cd9b6fef3509', + 'network_id': 'f548089e-fb3e-4013-a043-c5ed809c7a67', + } + self.assertRaises(mgr_exceptions.CantUpdateFloatingIPReservation, + fip_plugin.update_reservation, + '441c1476-9f8f-4700-9f30-cd9b6fef3509', values) + def test_on_start(self): fip_reservation_get = self.patch(self.db_api, 'fip_reservation_get') fip_reservation_get.return_value = { diff --git a/releasenotes/notes/floatingip-reservation-update-f53c0c6239ccf9ee.yaml b/releasenotes/notes/floatingip-reservation-update-f53c0c6239ccf9ee.yaml new file mode 100644 index 00000000..479ea780 --- /dev/null +++ b/releasenotes/notes/floatingip-reservation-update-f53c0c6239ccf9ee.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for updating floating IP reservations, with some limitations. + The ``amount`` of reserved floating IPs can be updated; however, updating + ``network_id`` is denied and ``required_floatingips`` can only be changed + to an empty list.