Jelajahi Sumber

Merge pull request #164 from Dany9966/user_scripts_column

Add 'user_scripts' column to migration table
Nashwan Azhari 5 tahun lalu
induk
melakukan
d2389e6e57

+ 4 - 2
coriolis/api/v1/migrations.py

@@ -104,11 +104,13 @@ class MigrationController(api_wsgi.Controller):
                 shutdown_instances, network_map, storage_mappings)
                 shutdown_instances, network_map, storage_mappings)
 
 
     def create(self, req, body):
     def create(self, req, body):
-        # TODO(alexpilotti): validate body
         migration_body = body.get("migration", {})
         migration_body = body.get("migration", {})
         context = req.environ['coriolis.context']
         context = req.environ['coriolis.context']
         context.can(migration_policies.get_migrations_policy_label("create"))
         context.can(migration_policies.get_migrations_policy_label("create"))
-        user_scripts = migration_body.get("user_scripts", {})
+        user_scripts = migration_body.get('user_scripts', {})
+        api_utils.validate_user_scripts(user_scripts)
+        user_scripts = api_utils.normalize_user_scripts(
+            user_scripts, migration_body.get("instances", []))
         replica_id = migration_body.get("replica_id")
         replica_id = migration_body.get("replica_id")
         if replica_id:
         if replica_id:
             clone_disks = migration_body.get("clone_disks", True)
             clone_disks = migration_body.get("clone_disks", True)

+ 37 - 4
coriolis/api/v1/replicas.py

@@ -78,6 +78,11 @@ class ReplicaController(api_wsgi.Controller):
         instance_osmorphing_minion_pool_mappings = replica.get(
         instance_osmorphing_minion_pool_mappings = replica.get(
             'instance_osmorphing_minion_pool_mappings', {})
             'instance_osmorphing_minion_pool_mappings', {})
 
 
+        user_scripts = replica.get('user_scripts', {})
+        api_utils.validate_user_scripts(user_scripts)
+        user_scripts = api_utils.normalize_user_scripts(
+            user_scripts, instances)
+
         # NOTE(aznashwan): we validate the destination environment for the
         # NOTE(aznashwan): we validate the destination environment for the
         # import provider before appending the 'storage_mappings' parameter
         # import provider before appending the 'storage_mappings' parameter
         # for plugins with strict property name checks which do not yet
         # for plugins with strict property name checks which do not yet
@@ -97,7 +102,7 @@ class ReplicaController(api_wsgi.Controller):
                 source_environment, destination_environment, instances,
                 source_environment, destination_environment, instances,
                 network_map, storage_mappings, notes,
                 network_map, storage_mappings, notes,
                 origin_minion_pool_id, destination_minion_pool_id,
                 origin_minion_pool_id, destination_minion_pool_id,
-                instance_osmorphing_minion_pool_mappings)
+                instance_osmorphing_minion_pool_mappings, user_scripts)
 
 
     def create(self, req, body):
     def create(self, req, body):
         context = req.environ["coriolis.context"]
         context = req.environ["coriolis.context"]
@@ -107,7 +112,7 @@ class ReplicaController(api_wsgi.Controller):
          source_environment, destination_environment, instances, network_map,
          source_environment, destination_environment, instances, network_map,
          storage_mappings, notes, origin_minion_pool_id,
          storage_mappings, notes, origin_minion_pool_id,
          destination_minion_pool_id,
          destination_minion_pool_id,
-         instance_osmorphing_minion_pool_mappings) = (
+         instance_osmorphing_minion_pool_mappings, user_scripts) = (
             self._validate_create_body(context, body))
             self._validate_create_body(context, body))
 
 
         return replica_view.single(req, self._replica_api.create(
         return replica_view.single(req, self._replica_api.create(
@@ -115,7 +120,7 @@ class ReplicaController(api_wsgi.Controller):
             origin_minion_pool_id, destination_minion_pool_id,
             origin_minion_pool_id, destination_minion_pool_id,
             instance_osmorphing_minion_pool_mappings, source_environment,
             instance_osmorphing_minion_pool_mappings, source_environment,
             destination_environment, instances, network_map,
             destination_environment, instances, network_map,
-            storage_mappings, notes))
+            storage_mappings, notes, user_scripts))
 
 
     def delete(self, req, id):
     def delete(self, req, id):
         context = req.environ["coriolis.context"]
         context = req.environ["coriolis.context"]
@@ -171,12 +176,31 @@ class ReplicaController(api_wsgi.Controller):
 
 
         return storage_mappings
         return storage_mappings
 
 
+    @staticmethod
+    def _get_updated_user_scripts(original_user_scripts, new_user_scripts):
+        global_scripts = original_user_scripts.get('global', {})
+        new_global_scripts = new_user_scripts.get('global', {})
+        if new_global_scripts:
+            global_scripts.update(new_global_scripts)
+
+        instance_scripts = original_user_scripts.get('instances', {})
+        new_instance_scripts = new_user_scripts.get('instances', {})
+        if new_instance_scripts:
+            instance_scripts.update(new_instance_scripts)
+
+        user_scripts = {
+            "global": global_scripts,
+            "instances": instance_scripts,
+        }
+
+        return user_scripts
+
     def _get_merged_replica_values(self, replica, updated_values):
     def _get_merged_replica_values(self, replica, updated_values):
         """ Looks for the following keys in the original replica body and
         """ Looks for the following keys in the original replica body and
         updated values (preferring the updated values where needed, but using
         updated values (preferring the updated values where needed, but using
         `.update()` on dicts):
         `.update()` on dicts):
         "source_environment", "destination_environment", "network_map", "notes"
         "source_environment", "destination_environment", "network_map", "notes"
-        Does special merging for the "storage_mappings"
+        Does special merging for the "storage_mappings" and "user_scripts"
         Returns a dict with the merged values (or at least all if the keys
         Returns a dict with the merged values (or at least all if the keys
         having a default value of {})
         having a default value of {})
         """
         """
@@ -207,6 +231,10 @@ class ReplicaController(api_wsgi.Controller):
         final_values['storage_mappings'] = self._update_storage_mappings(
         final_values['storage_mappings'] = self._update_storage_mappings(
             original_storage_mappings, new_storage_mappings)
             original_storage_mappings, new_storage_mappings)
 
 
+        final_values['user_scripts'] = self._get_updated_user_scripts(
+            replica.get('user_scripts', {}),
+            updated_values.get('user_scripts', {}))
+
         if 'notes' in updated_values:
         if 'notes' in updated_values:
             final_values['notes'] = updated_values.get('notes', '')
             final_values['notes'] = updated_values.get('notes', '')
         else:
         else:
@@ -276,6 +304,11 @@ class ReplicaController(api_wsgi.Controller):
         api_utils.validate_storage_mappings(
         api_utils.validate_storage_mappings(
             merged_body["storage_mappings"])
             merged_body["storage_mappings"])
 
 
+        user_scripts = merged_body['user_scripts']
+        api_utils.validate_user_scripts(user_scripts)
+        merged_body['user_scripts'] = api_utils.normalize_user_scripts(
+            user_scripts, replica.get('instances', []))
+
         return merged_body
         return merged_body
 
 
     def update(self, req, id, body):
     def update(self, req, id, body):

+ 42 - 2
coriolis/api/v1/utils.py

@@ -7,6 +7,7 @@ import json
 from oslo_log import log as logging
 from oslo_log import log as logging
 from webob import exc
 from webob import exc
 
 
+from coriolis import constants
 from coriolis import exception
 from coriolis import exception
 from coriolis import schemas
 from coriolis import schemas
 
 
@@ -65,8 +66,8 @@ def format_keyerror_message(resource='', method=''):
                 raise exception.InvalidInput(exc_message)
                 raise exception.InvalidInput(exc_message)
             except Exception as err:
             except Exception as err:
                 LOG.exception(err)
                 LOG.exception(err)
-                msg = getattr(err, "message", str(err))
-                raise exception.InvalidInput(msg)
+                msg = getattr(err, "msg", str(err))
+                raise exception.InvalidInput(reason=msg)
         return _wrapper
         return _wrapper
     return _format_keyerror_message
     return _format_keyerror_message
 
 
@@ -85,3 +86,42 @@ def _build_keyerror_message(resource, method, key):
             resource, method_mapping[method], key)
             resource, method_mapping[method], key)
 
 
     return msg
     return msg
+
+
+def validate_user_scripts(user_scripts):
+    if not isinstance(user_scripts, dict):
+        raise exception.InvalidInput(
+            reason='"user_scripts" must be of JSON object format')
+
+    global_scripts = user_scripts.get('global', {})
+    if not isinstance(global_scripts, dict):
+        raise exception.InvalidInput(
+            reason='"global" must be a mapping between the identifiers of the '
+                   'supported OS types and their respective scripts.')
+    for os_type in global_scripts.keys():
+        if os_type not in constants.VALID_OS_TYPES:
+            raise exception.InvalidInput(
+                reason='The provided global user script os_type "%s" is '
+                       'invalid. Must be one of the '
+                       'following: %s' % (os_type, constants.VALID_OS_TYPES))
+
+    instance_scripts = user_scripts.get('instances', {})
+    if not isinstance(instance_scripts, dict):
+        raise exception.InvalidInput(
+            reason='"instances" must be a mapping between the identifiers of '
+                   'the instances in the Replica/Migration and their '
+                   'respective scripts.')
+
+
+def normalize_user_scripts(user_scripts, instances):
+    """ Removes instance user_scripts if said instance is not one of the
+        selected instances for the replica/migration """
+    for instance in user_scripts.get('instances', {}).keys():
+        if instance not in instances:
+            LOG.warn("Removing provided instance '%s' from user_scripts body "
+                     "because it's not included in one of the selected "
+                     "instances for this replica/migration: %s",
+                     instance, instances)
+            user_scripts.pop(instance, None)
+
+    return user_scripts

+ 3 - 2
coriolis/conductor/rpc/client.py

@@ -161,7 +161,7 @@ class ConductorClient(rpc.BaseRPCClient):
                                  instance_osmorphing_minion_pool_mappings,
                                  instance_osmorphing_minion_pool_mappings,
                                  source_environment, destination_environment,
                                  source_environment, destination_environment,
                                  instances, network_map, storage_mappings,
                                  instances, network_map, storage_mappings,
-                                 notes=None):
+                                 notes=None, user_scripts=None):
         return self._call(
         return self._call(
             ctxt, 'create_instances_replica',
             ctxt, 'create_instances_replica',
             origin_endpoint_id=origin_endpoint_id,
             origin_endpoint_id=origin_endpoint_id,
@@ -175,7 +175,8 @@ class ConductorClient(rpc.BaseRPCClient):
             notes=notes,
             notes=notes,
             network_map=network_map,
             network_map=network_map,
             storage_mappings=storage_mappings,
             storage_mappings=storage_mappings,
-            source_environment=source_environment)
+            source_environment=source_environment,
+            user_scripts=user_scripts)
 
 
     def get_replicas(self, ctxt, include_tasks_executions=False):
     def get_replicas(self, ctxt, include_tasks_executions=False):
         return self._call(
         return self._call(

+ 8 - 1
coriolis/conductor/rpc/server.py

@@ -1397,7 +1397,8 @@ class ConductorServerEndpoint(object):
                                  instance_osmorphing_minion_pool_mappings,
                                  instance_osmorphing_minion_pool_mappings,
                                  source_environment,
                                  source_environment,
                                  destination_environment, instances,
                                  destination_environment, instances,
-                                 network_map, storage_mappings, notes=None):
+                                 network_map, storage_mappings, notes=None,
+                                 user_scripts=None):
         origin_endpoint = self.get_endpoint(ctxt, origin_endpoint_id)
         origin_endpoint = self.get_endpoint(ctxt, origin_endpoint_id)
         destination_endpoint = self.get_endpoint(ctxt, destination_endpoint_id)
         destination_endpoint = self.get_endpoint(ctxt, destination_endpoint_id)
         self._check_endpoints(ctxt, origin_endpoint, destination_endpoint)
         self._check_endpoints(ctxt, origin_endpoint, destination_endpoint)
@@ -1421,6 +1422,7 @@ class ConductorServerEndpoint(object):
         replica.storage_mappings = storage_mappings
         replica.storage_mappings = storage_mappings
         replica.instance_osmorphing_minion_pool_mappings = (
         replica.instance_osmorphing_minion_pool_mappings = (
             instance_osmorphing_minion_pool_mappings)
             instance_osmorphing_minion_pool_mappings)
+        replica.user_scripts = user_scripts
 
 
         self._check_minion_pools_for_action(ctxt, replica)
         self._check_minion_pools_for_action(ctxt, replica)
 
 
@@ -1505,6 +1507,7 @@ class ConductorServerEndpoint(object):
             replica, licensing_client.RESERVATION_TYPE_REPLICA)
             replica, licensing_client.RESERVATION_TYPE_REPLICA)
         self._check_replica_running_executions(ctxt, replica)
         self._check_replica_running_executions(ctxt, replica)
         self._check_valid_replica_tasks_execution(replica, force)
         self._check_valid_replica_tasks_execution(replica, force)
+        user_scripts = user_scripts or replica.user_scripts
 
 
         destination_endpoint = self.get_endpoint(
         destination_endpoint = self.get_endpoint(
             ctxt, replica.destination_endpoint_id)
             ctxt, replica.destination_endpoint_id)
@@ -1538,6 +1541,7 @@ class ConductorServerEndpoint(object):
         migration.instances = instances
         migration.instances = instances
         migration.replica = replica
         migration.replica = replica
         migration.info = replica.info
         migration.info = replica.info
+        migration.user_scripts = user_scripts
         migration.origin_minion_pool_id = replica.origin_minion_pool_id
         migration.origin_minion_pool_id = replica.origin_minion_pool_id
         migration.destination_minion_pool_id = (
         migration.destination_minion_pool_id = (
             replica.destination_minion_pool_id)
             replica.destination_minion_pool_id)
@@ -2035,6 +2039,7 @@ class ConductorServerEndpoint(object):
         migration.executions = [execution]
         migration.executions = [execution]
         migration.instances = instances
         migration.instances = instances
         migration.info = {}
         migration.info = {}
+        migration.user_scripts = user_scripts
         migration.notes = notes
         migration.notes = notes
         migration.shutdown_instances = shutdown_instances
         migration.shutdown_instances = shutdown_instances
         migration.replication_count = replication_count
         migration.replication_count = replication_count
@@ -3845,6 +3850,8 @@ class ConductorServerEndpoint(object):
 
 
         self._check_replica_running_executions(ctxt, replica)
         self._check_replica_running_executions(ctxt, replica)
         self._check_valid_replica_tasks_execution(replica, force=True)
         self._check_valid_replica_tasks_execution(replica, force=True)
+        if updated_properties.get('user_scripts'):
+            replica.user_scripts = updated_properties['user_scripts']
         execution = models.TasksExecution()
         execution = models.TasksExecution()
         execution.id = str(uuid.uuid4())
         execution.id = str(uuid.uuid4())
         execution.status = constants.EXECUTION_STATUS_UNEXECUTED
         execution.status = constants.EXECUTION_STATUS_UNEXECUTED

+ 17 - 0
coriolis/db/sqlalchemy/migrate_repo/versions/017_adds_user_scripts_column.py

@@ -0,0 +1,17 @@
+# Copyright 2020 Cloudbase Solutions Srl
+# All Rights Reserved.
+
+import sqlalchemy
+
+
+def upgrade(migrate_engine):
+    meta = sqlalchemy.MetaData()
+    meta.bind = migrate_engine
+
+    # add 'user_scripts' column to 'base_transfer_action':
+    base_transfer = sqlalchemy.Table(
+        'base_transfer_action', meta, autoload=True)
+
+    user_scripts = sqlalchemy.Column(
+        "user_scripts", sqlalchemy.Text, nullable=True)
+    base_transfer.create_column(user_scripts)

+ 3 - 1
coriolis/db/sqlalchemy/models.py

@@ -206,6 +206,7 @@ class BaseTransferAction(BASE, models.TimestampMixin, models.ModelBase,
         sqlalchemy.String(36), nullable=True)
         sqlalchemy.String(36), nullable=True)
     instance_osmorphing_minion_pool_mappings = sqlalchemy.Column(
     instance_osmorphing_minion_pool_mappings = sqlalchemy.Column(
         types.Json, nullable=False, default=lambda: {})
         types.Json, nullable=False, default=lambda: {})
+    user_scripts = sqlalchemy.Column(types.Json, nullable=True)
 
 
     __mapper_args__ = {
     __mapper_args__ = {
         'polymorphic_identity': 'base_transfer_action',
         'polymorphic_identity': 'base_transfer_action',
@@ -237,7 +238,8 @@ class BaseTransferAction(BASE, models.TimestampMixin, models.ModelBase,
             "origin_minion_pool_id": self.origin_minion_pool_id,
             "origin_minion_pool_id": self.origin_minion_pool_id,
             "destination_minion_pool_id": self.destination_minion_pool_id,
             "destination_minion_pool_id": self.destination_minion_pool_id,
             "instance_osmorphing_minion_pool_mappings":
             "instance_osmorphing_minion_pool_mappings":
-                self.instance_osmorphing_minion_pool_mappings
+                self.instance_osmorphing_minion_pool_mappings,
+            "user_scripts": self.user_scripts,
         }
         }
         if include_executions:
         if include_executions:
             for ex in self.executions:
             for ex in self.executions:

+ 2 - 2
coriolis/replicas/api.py

@@ -12,13 +12,13 @@ class API(object):
                origin_minion_pool_id, destination_minion_pool_id,
                origin_minion_pool_id, destination_minion_pool_id,
                instance_osmorphing_minion_pool_mappings,
                instance_osmorphing_minion_pool_mappings,
                source_environment, destination_environment, instances,
                source_environment, destination_environment, instances,
-               network_map, storage_mappings, notes=None):
+               network_map, storage_mappings, notes=None, user_scripts=None):
         return self._rpc_client.create_instances_replica(
         return self._rpc_client.create_instances_replica(
             ctxt, origin_endpoint_id, destination_endpoint_id,
             ctxt, origin_endpoint_id, destination_endpoint_id,
             origin_minion_pool_id, destination_minion_pool_id,
             origin_minion_pool_id, destination_minion_pool_id,
             instance_osmorphing_minion_pool_mappings,
             instance_osmorphing_minion_pool_mappings,
             source_environment, destination_environment, instances,
             source_environment, destination_environment, instances,
-            network_map, storage_mappings, notes)
+            network_map, storage_mappings, notes, user_scripts)
 
 
     def update(self, ctxt, replica_id, updated_properties):
     def update(self, ctxt, replica_id, updated_properties):
         return self._rpc_client.update_replica(
         return self._rpc_client.update_replica(