Răsfoiți Sursa

Add SAP licence support

Fabian Fulga 2 săptămâni în urmă
părinte
comite
a8985fae10

+ 49 - 21
coriolis/conductor/rpc/server.py

@@ -53,6 +53,27 @@ SCENARIO_TYPE_TO_LICENSING_RESERVATION_MAP = {
         licensing_client.RESERVATION_TYPE_MIGRATION
 }
 
+ENDPOINT_TYPE_TO_LICENSING_RESERVATION_OVERRIDES = {
+    constants.ENDPOINT_TYPE_SAP_LIBVIRT: {
+        constants.TRANSFER_SCENARIO_REPLICA:
+            licensing_client.RESERVATION_TYPE_SAP_REPLICA,
+        constants.TRANSFER_SCENARIO_LIVE_MIGRATION:
+            licensing_client.RESERVATION_TYPE_SAP_MIGRATION,
+    },
+}
+
+
+def _get_reservation_type(destination_endpoint, scenario):
+    """Returns the licensing reservation type for the given transfer
+    scenario, taking into account any provider-specific overrides for
+    the given destination endpoint's type (e.g. SAP transfers require
+    dedicated SAP reservation types)."""
+    overrides = ENDPOINT_TYPE_TO_LICENSING_RESERVATION_OVERRIDES.get(
+        destination_endpoint.type, {})
+    return overrides.get(
+        scenario,
+        SCENARIO_TYPE_TO_LICENSING_RESERVATION_MAP.get(scenario))
+
 
 def endpoint_synchronized(func):
     @functools.wraps(func)
@@ -294,11 +315,11 @@ class ConductorServerEndpoint(object):
                     "action with ID '%s'. Skipping. Exception\n%s",
                     reservation_id, action_id, utils.get_exception_details())
 
-    def _create_reservation_for_transfer(self, transfer):
+    def _create_reservation_for_transfer(self, transfer, destination_endpoint):
         action_id = transfer.base_id
         scenario = transfer.scenario
-        reservation_type = SCENARIO_TYPE_TO_LICENSING_RESERVATION_MAP.get(
-            scenario, None)
+        reservation_type = _get_reservation_type(
+            destination_endpoint, scenario)
         if not reservation_type:
             raise exception.LicensingException(
                 message="Could not determine reservation type for transfer "
@@ -371,10 +392,12 @@ class ConductorServerEndpoint(object):
                 f"Successfully marked reservation with ID '{reservation_id}' "
                 f"for transfer action '{action_id}' as fulfilled")
 
-    def _check_reservation_for_transfer(self, transfer):
+    def _check_reservation_for_transfer(self, ctxt, transfer):
         scenario = transfer.scenario
-        reservation_type = SCENARIO_TYPE_TO_LICENSING_RESERVATION_MAP.get(
-            scenario, None)
+        destination_endpoint = self.get_endpoint(
+            ctxt, transfer.destination_endpoint_id)
+        reservation_type = _get_reservation_type(
+            destination_endpoint, scenario)
         if not reservation_type:
             raise exception.LicensingException(
                 message="Could not determine reservation type for transfer "
@@ -394,21 +417,24 @@ class ConductorServerEndpoint(object):
                 "Attempting to check reservation with ID '%s' for transfer "
                 "action '%s'", reservation_id, action_id)
             try:
-                reservation = self._licensing_client.get_reservation(
-                    reservation_id)
-
-                fulfilled_at = reservation.get("fulfilled_at", None)
-                if scenario == constants.TRANSFER_SCENARIO_LIVE_MIGRATION and (
-                        fulfilled_at):
-                    raise exception.MigrationLicenceFulfilledException(
-                        action_id=transfer.id, reservation_id=reservation_id,
-                        fulfilled_at=fulfilled_at)
-
+                # NOTE: the licensing server handles fulfilled reservations
+                # on refresh itself, depending on the type of reservation
                 transfer.reservation_id = (
                     self._licensing_client.check_refresh_reservation(
                         reservation_id)['id'])
             except Exception as ex:
                 exc_code = getattr(ex, 'code', None)
+                if exc_code == 403:
+                    LOG.debug(
+                        "Licensing server refused refresh of fulfilled '%s' "
+                        "reservation '%s' for action '%s'. Trace was: %s",
+                        reservation_type, reservation_id, action_id,
+                        utils.get_exception_details())
+                    raise exception.LicenceReservationFulfilledException(
+                        scenario=scenario.replace('_', ' ').title(),
+                        action_id=transfer.id,
+                        reservation_id=reservation_id,
+                        reservation_type=reservation_type)
                 if exc_code in [404, 409]:
                     if exc_code == 409:
                         LOG.debug(
@@ -425,14 +451,16 @@ class ConductorServerEndpoint(object):
                             "reservation. Trace was: %s",
                             reservation_id, action_id,
                             utils.get_exception_details())
-                    self._create_reservation_for_transfer(transfer)
+                    self._create_reservation_for_transfer(
+                        transfer, destination_endpoint)
                 else:
                     raise ex
         else:
             LOG.info(
                 f"Transfer action '{action_id}' has no reservation ID set, "
                 f"attempting to create a new one for it")
-            self._create_reservation_for_transfer(transfer)
+            self._create_reservation_for_transfer(
+                transfer, destination_endpoint)
 
     def create_endpoint(self, ctxt, name, endpoint_type, description,
                         connection_info, mapped_regions=None):
@@ -904,7 +932,7 @@ class ConductorServerEndpoint(object):
             ctxt, transfer_id, include_task_info=True)
         self._check_transfer_running_executions(ctxt, transfer)
         self._check_minion_pools_for_action(ctxt, transfer)
-        self._check_reservation_for_transfer(transfer)
+        self._check_reservation_for_transfer(ctxt, transfer)
 
         execution = models.TasksExecution()
         execution.id = str(uuid.uuid4())
@@ -1347,7 +1375,7 @@ class ConductorServerEndpoint(object):
 
         self._check_minion_pools_for_action(ctxt, transfer)
 
-        self._create_reservation_for_transfer(transfer)
+        self._create_reservation_for_transfer(transfer, destination_endpoint)
 
         db_api.add_transfer(ctxt, transfer)
         LOG.info("Transfer created: %s", transfer.id)
@@ -1451,7 +1479,7 @@ class ConductorServerEndpoint(object):
                     "deleted, the transfer needs to be executed anew "
                     "before a deployment can occur")
         self._check_minion_pools_for_action(ctxt, deployment)
-        self._check_reservation_for_transfer(transfer)
+        self._check_reservation_for_transfer(ctxt, transfer)
 
     def _execute_deployment(self, ctxt, deployment, force):
         transfer = self._get_transfer(

+ 2 - 0
coriolis/constants.py

@@ -6,6 +6,8 @@ DEFAULT_CORIOLIS_REGION_NAME = "Default Region"
 TRANSFER_SCENARIO_REPLICA = "replica"
 TRANSFER_SCENARIO_LIVE_MIGRATION = "live_migration"
 
+ENDPOINT_TYPE_SAP_LIBVIRT = "sap_libvirt"
+
 EXECUTION_STATUS_UNEXECUTED = "UNEXECUTED"
 EXECUTION_STATUS_RUNNING = "RUNNING"
 EXECUTION_STATUS_COMPLETED = "COMPLETED"

+ 5 - 5
coriolis/exception.py

@@ -553,9 +553,9 @@ class ChecksumMismatch(CoriolisException):
         "source=%(source_checksum)s, destination=%(dest_checksum)s")
 
 
-class MigrationLicenceFulfilledException(Invalid):
+class LicenceReservationFulfilledException(Invalid):
     message = (
-        "The Live Migration operation with ID '%(action_id)s' (licensing "
-        "reservation '%(reservation_id)s') has already been fulfilled on "
-        "%(fulfilled_at)s. Please create a new Live Migration operation to "
-        "create a new licensing reservation.")
+        "The %(scenario)s operation with ID '%(action_id)s' (licensing "
+        "reservation '%(reservation_id)s' of type '%(reservation_type)s') "
+        "has already been fulfilled. Please create a new %(scenario)s "
+        "operation to create a new licensing reservation.")

+ 4 - 1
coriolis/licensing/client.py

@@ -17,6 +17,8 @@ CONF = cfg.CONF
 
 RESERVATION_TYPE_REPLICA = "replica"
 RESERVATION_TYPE_MIGRATION = "migration"
+RESERVATION_TYPE_SAP_REPLICA = "sap_replica"
+RESERVATION_TYPE_SAP_MIGRATION = "sap_migration"
 
 
 class LicensingClient(object):
@@ -182,7 +184,8 @@ class LicensingClient(object):
     def add_reservation(self, reservation_type, num_vms):
         """ Creates a reservation of the given type. """
         allowed_values = [
-            RESERVATION_TYPE_MIGRATION, RESERVATION_TYPE_REPLICA]
+            RESERVATION_TYPE_MIGRATION, RESERVATION_TYPE_REPLICA,
+            RESERVATION_TYPE_SAP_MIGRATION, RESERVATION_TYPE_SAP_REPLICA]
         if reservation_type not in allowed_values:
             raise ValueError(
                 "Reservation type must be one of %s" % allowed_values)

+ 32 - 2
coriolis/tests/conductor/rpc/test_server.py

@@ -17,6 +17,7 @@ from coriolis.db import api as db_api
 from coriolis.db.sqlalchemy import models
 from coriolis import exception
 from coriolis import keystone
+from coriolis.licensing import client as licensing_client
 from coriolis import schemas
 from coriolis.tests import test_base
 from coriolis.tests import testutils
@@ -113,6 +114,33 @@ class ConductorServerEndpointTestCase(test_base.CoriolisBaseTestCase):
 
         self.assertEqual(result, mock_from_service_definition.return_value)
 
+    def test__get_reservation_type(self):
+        standard_endpoint = mock.Mock(type="openstack")
+        sap_endpoint = mock.Mock(type=constants.ENDPOINT_TYPE_SAP_LIBVIRT)
+
+        self.assertEqual(
+            licensing_client.RESERVATION_TYPE_REPLICA,
+            server._get_reservation_type(
+                standard_endpoint, constants.TRANSFER_SCENARIO_REPLICA))
+        self.assertEqual(
+            licensing_client.RESERVATION_TYPE_MIGRATION,
+            server._get_reservation_type(
+                standard_endpoint,
+                constants.TRANSFER_SCENARIO_LIVE_MIGRATION))
+        self.assertEqual(
+            licensing_client.RESERVATION_TYPE_SAP_REPLICA,
+            server._get_reservation_type(
+                sap_endpoint, constants.TRANSFER_SCENARIO_REPLICA))
+        self.assertEqual(
+            licensing_client.RESERVATION_TYPE_SAP_MIGRATION,
+            server._get_reservation_type(
+                sap_endpoint, constants.TRANSFER_SCENARIO_LIVE_MIGRATION))
+        self.assertIsNone(
+            server._get_reservation_type(
+                standard_endpoint, "unknown_scenario"))
+        self.assertIsNone(
+            server._get_reservation_type(sap_endpoint, "unknown_scenario"))
+
     def test_check_delete_reservation_for_transfer(self):
         transfer_action = mock.Mock()
         self.server._check_delete_reservation_for_transfer(transfer_action)
@@ -1337,7 +1365,8 @@ class ConductorServerEndpointTestCase(test_base.CoriolisBaseTestCase):
             mock.sentinel.transfer_id,
             include_task_info=True,
         )
-        mock_check_reservation.assert_called_once_with(mock_transfer)
+        mock_check_reservation.assert_called_once_with(
+            mock.sentinel.context, mock_transfer)
         mock_check_transfer_running_executions.assert_called_once_with(
             mock.sentinel.context, mock_transfer)
         mock_check_minion_pools_for_action.assert_called_once_with(
@@ -2035,7 +2064,8 @@ class ConductorServerEndpointTestCase(test_base.CoriolisBaseTestCase):
         mock_check_minion_pools_for_action.assert_called_once_with(
             mock.sentinel.context, mock_transfer.return_value)
         mock_create_reservation_for_transfer.assert_called_once_with(
-            mock_transfer.return_value)
+            mock_transfer.return_value,
+            mock.sentinel.destination_endpoint_id)
         mock_add_transfer.assert_called_once_with(
             mock.sentinel.context, mock_transfer.return_value)
         mock_get_transfer.assert_called_once_with(

+ 17 - 0
coriolis/tests/licensing/test_client.py

@@ -364,6 +364,23 @@ class LicensingClientTestCase(test_base.CoriolisBaseTestCase):
             response_key='reservation'
         )
 
+    @mock.patch.object(licensing_module.LicensingClient, '_post')
+    def test_add_reservation_sap_types(self, mock_post):
+        for sap_type in [licensing_module.RESERVATION_TYPE_SAP_MIGRATION,
+                         licensing_module.RESERVATION_TYPE_SAP_REPLICA]:
+            mock_post.reset_mock()
+            result = self.client.add_reservation(sap_type, 2)
+            self.assertEqual(result, mock_post.return_value)
+
+            mock_post.assert_called_once_with(
+                '/reservations',
+                {
+                    'type': sap_type,
+                    'count': 2
+                },
+                response_key='reservation'
+            )
+
     def test_add_reservation_invalid_type(self):
         self.assertRaises(
             ValueError, self.client.add_reservation,