Forráskód Böngészése

Add user defined scripts for OS morphing

These user defined scripts will run before any OS morphing operation
will take place.
Gabriel-Adrian Samfira 6 éve
szülő
commit
a9d21e9c2d

+ 5 - 3
coriolis/api/v1/migrations.py

@@ -98,7 +98,7 @@ class MigrationController(api_wsgi.Controller):
         migration_body = body.get("migration", {})
         context = req.environ['coriolis.context']
         context.can(migration_policies.get_migrations_policy_label("create"))
-
+        user_scripts = migration_body.get("user_scripts")
         replica_id = migration_body.get("replica_id")
         if replica_id:
             clone_disks = migration_body.get("clone_disks", True)
@@ -108,7 +108,8 @@ class MigrationController(api_wsgi.Controller):
             # NOTE: destination environment for replica should have been
             # validated upon its creation.
             migration = self._migration_api.deploy_replica_instances(
-                context, replica_id, clone_disks, force, skip_os_morphing)
+                context, replica_id, clone_disks, force, skip_os_morphing,
+                user_scripts=user_scripts)
         else:
             (origin_endpoint_id,
              destination_endpoint_id,
@@ -127,7 +128,8 @@ class MigrationController(api_wsgi.Controller):
                 source_environment, destination_environment, instances,
                 network_map, storage_mappings, replication_count,
                 shutdown_instances, notes=notes,
-                skip_os_morphing=skip_os_morphing)
+                skip_os_morphing=skip_os_morphing,
+                user_scripts=user_scripts)
 
         return migration_view.single(req, migration)
 

+ 7 - 4
coriolis/conductor/rpc/client.py

@@ -196,7 +196,7 @@ class ConductorClient(object):
                           destination_environment, instances, network_map,
                           storage_mappings, replication_count,
                           shutdown_instances=False, notes=None,
-                          skip_os_morphing=False):
+                          skip_os_morphing=False, user_scripts=None):
         return self._client.call(
             ctxt, 'migrate_instances',
             origin_endpoint_id=origin_endpoint_id,
@@ -209,14 +209,17 @@ class ConductorClient(object):
             skip_os_morphing=skip_os_morphing,
             network_map=network_map,
             storage_mappings=storage_mappings,
-            source_environment=source_environment)
+            source_environment=source_environment,
+            user_scripts=user_scripts)
 
     def deploy_replica_instances(self, ctxt, replica_id, clone_disks=False,
-                                 force=False, skip_os_morphing=False):
+                                 force=False, skip_os_morphing=False,
+                                 user_scripts=None):
         return self._client.call(
             ctxt, 'deploy_replica_instances', replica_id=replica_id,
             clone_disks=clone_disks, force=force,
-            skip_os_morphing=skip_os_morphing)
+            skip_os_morphing=skip_os_morphing,
+            user_scripts=user_scripts)
 
     def delete_migration(self, ctxt, migration_id):
         self._client.call(

+ 19 - 3
coriolis/conductor/rpc/server.py

@@ -574,7 +574,7 @@ class ConductorServerEndpoint(object):
 
     @replica_synchronized
     def deploy_replica_instances(self, ctxt, replica_id, clone_disks, force,
-                                 skip_os_morphing=False):
+                                 skip_os_morphing=False, user_scripts=None):
         replica = self._get_replica(ctxt, replica_id)
         self._check_reservation_for_transfer(replica)
         self._check_replica_running_executions(ctxt, replica)
@@ -609,6 +609,8 @@ class ConductorServerEndpoint(object):
 
         for instance in instances:
             migration.info[instance]["clone_disks"] = clone_disks
+            scripts = self._get_instance_scripts(user_scripts, instance)
+            migration.info[instance]["user_scripts"] = scripts
 
         execution = models.TasksExecution()
         migration.executions = [execution]
@@ -690,12 +692,24 @@ class ConductorServerEndpoint(object):
 
         return self.get_migration(ctxt, migration.id)
 
+    def _get_instance_scripts(self, user_scripts, instance):
+        ret = {
+            "global": user_scripts.get("global", {}),
+            "instances": {},
+        }
+        if user_scripts:
+            instance_script = user_scripts.get(
+                "instances", {}).get(instance)
+            if instance_script:
+                ret["instances"][instance] = instance_script
+        return ret
+
     def migrate_instances(self, ctxt, origin_endpoint_id,
                           destination_endpoint_id, source_environment,
                           destination_environment, instances, network_map,
                           storage_mappings, replication_count,
                           shutdown_instances=False, notes=None,
-                          skip_os_morphing=False):
+                          skip_os_morphing=False, user_scripts=None):
         origin_endpoint = self.get_endpoint(ctxt, origin_endpoint_id)
         destination_endpoint = self.get_endpoint(ctxt, destination_endpoint_id)
         self._check_endpoints(ctxt, origin_endpoint, destination_endpoint)
@@ -729,6 +743,8 @@ class ConductorServerEndpoint(object):
             # NOTE: we must explicitly set this in each VM's info
             # to prevent the Replica disks from being cloned:
             migration.info[instance] = {"clone_disks": False}
+            scripts = self._get_instance_scripts(user_scripts, instance)
+            migration.info[instance]["user_scripts"] = scripts
 
             validate_migration_source_inputs_task = self._create_task(
                 instance,
@@ -1039,7 +1055,7 @@ class ConductorServerEndpoint(object):
                     db_api.set_transfer_action_result(
                         ctxt, execution.action_id, task.instance,
                         transfer_result)
-                except exception.SchemaValidationException as ex:
+                except exception.SchemaValidationException:
                     LOG.warn(
                         "Could not validate transfer result '%s' against the "
                         "VM export info schema. NOT saving value in Database. "

+ 7 - 4
coriolis/migrations/api.py

@@ -13,18 +13,21 @@ class API(object):
                           destination_environment, instances, network_map,
                           storage_mappings, replication_count,
                           shutdown_instances, notes=None,
-                          skip_os_morphing=False):
+                          skip_os_morphing=False, user_scripts=None):
         return self._rpc_client.migrate_instances(
             ctxt, origin_endpoint_id, destination_endpoint_id,
             source_environment, destination_environment, instances,
             network_map, storage_mappings,
             replication_count, shutdown_instances=shutdown_instances,
-            notes=notes, skip_os_morphing=skip_os_morphing)
+            notes=notes, skip_os_morphing=skip_os_morphing,
+            user_scripts=user_scripts)
 
     def deploy_replica_instances(self, ctxt, replica_id, clone_disks=False,
-                                 force=False, skip_os_morphing=False):
+                                 force=False, skip_os_morphing=False,
+                                 user_scripts=None):
         return self._rpc_client.deploy_replica_instances(
-            ctxt, replica_id, clone_disks, force, skip_os_morphing)
+            ctxt, replica_id, clone_disks, force, skip_os_morphing,
+            user_scripts=user_scripts)
 
     def delete(self, ctxt, migration_id):
         self._rpc_client.delete_migration(ctxt, migration_id)

+ 36 - 0
coriolis/osmorphing/base.py

@@ -7,6 +7,7 @@ import os
 import re
 import uuid
 
+from coriolis import exception
 from oslo_log import log as logging
 from six import with_metaclass
 
@@ -49,6 +50,9 @@ class BaseOSMorphingTools(object, with_metaclass(abc.ABCMeta)):
     def get_packages(self):
         return [], []
 
+    def run_user_script(self, user_script):
+        pass
+
     def pre_packages_install(self, package_names):
         pass
 
@@ -96,6 +100,38 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
 
         return add, remove
 
+    def run_user_script(self, user_script):
+        if len(user_script) == 0:
+            return
+
+        script_path = "/tmp/coriolis_user_script"
+        try:
+            utils.write_ssh_file(
+                self._conn,
+                script_path,
+                user_script)
+        except Exception as err:
+            LOG.exception(err)
+            raise exception.CoriolisException(
+                "Failed to copy user script to target system."
+                " Error was: %s" % err)
+
+        try:
+            utils.exec_ssh_cmd(
+                self._conn,
+                "sudo chmod +x %s" % script_path,
+                get_pty=True)
+
+            utils.exec_ssh_cmd(
+                self._conn,
+                'sudo "%s" "%s"' % (script_path, self._os_root_dir),
+                get_pty=True)
+        except Exception as err:
+            LOG.exception(err)
+            raise exception.CoriolisException(
+                "Failed to run user script."
+                " Error was: %s" % err)
+
     def pre_packages_install(self, package_names):
         self._copy_resolv_conf()
 

+ 9 - 1
coriolis/osmorphing/manager.py

@@ -39,7 +39,7 @@ def _get_proxy_settings():
 
 
 def morph_image(origin_provider, destination_provider, connection_info,
-                osmorphing_info, event_handler):
+                osmorphing_info, user_script, event_handler):
     event_manager = events.EventManager(event_handler)
 
     event_manager.progress_update("Preparing instance for target platform")
@@ -76,6 +76,14 @@ def morph_image(origin_provider, destination_provider, connection_info,
      os_info) = destination_provider.get_os_morphing_tools(
          conn, osmorphing_info)
 
+    if user_script:
+        event_manager.progress_update(
+            'Running OS morphing user script')
+        import_os_morphing_tools.run_user_script(user_script)
+    else:
+        event_manager.progress_update(
+            'No OS morphing user script specified')
+
     if not import_os_morphing_tools:
         event_manager.progress_update(
             'No OS morphing tools found for this instance')

+ 31 - 3
coriolis/osmorphing/windows.py

@@ -74,18 +74,18 @@ class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
         key_name = str(uuid.uuid4())
 
         self._load_registry_hive(
-            "HKLM\%s" % key_name,
+            "HKLM\\%s" % key_name,
             "%sWindows\\System32\\config\\SOFTWARE" % self._os_root_dir)
         try:
             version_info_str = self._conn.exec_ps_command(
                 "Get-ItemProperty "
-                "'HKLM:\%s\Microsoft\Windows NT\CurrentVersion' "
+                "'HKLM:\\%s\\Microsoft\\Windows NT\\CurrentVersion' "
                 "| select CurrentVersion, CurrentMajorVersionNumber, "
                 "CurrentMinorVersionNumber,  CurrentBuildNumber, "
                 "InstallationType, ProductName, EditionID | FL" %
                 key_name).replace(self._conn.EOL, os.linesep)
         finally:
-            self._unload_registry_hive("HKLM\%s" % key_name)
+            self._unload_registry_hive("HKLM\\%s" % key_name)
 
         version_info = {}
         for n in ["CurrentVersion", "CurrentMajorVersionNumber",
@@ -221,3 +221,31 @@ class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
              "service_account": service_account,
              "start_mode": start_mode},
             ignore_stdout=True)
+
+    def run_user_script(self, user_script):
+        if len(user_script) == 0:
+            return
+
+        script_path = "$env:TMP\\coriolis_user_script.ps1"
+        try:
+            utils.write_winrm_file(
+                self._conn,
+                script_path,
+                user_script)
+        except Exception as err:
+            LOG.exception(err)
+            raise exception.CoriolisException(
+                "Failed to copy user script to target system."
+                " Error was: %s" % err)
+
+        cmd = '& "%(script)s" "%(os_root_dir)s"' % {
+            "script": script_path,
+            "os_root_dir": self._os_root_dir,
+        }
+        try:
+            self._conn.exec_ps_command(cmd)
+        except Exception as err:
+            LOG.exception(err)
+            raise exception.CoriolisException(
+                "Failed to run user script."
+                " Error was: %s" % err)

+ 11 - 0
coriolis/tasks/osmorphing_tasks.py

@@ -25,11 +25,22 @@ class OSMorphingTask(base.TaskRunner):
             task_info['osmorphing_connection_info'])
         osmorphing_info = task_info.get('osmorphing_info', {})
 
+        user_scripts = task_info.get("user_scripts")
+        instance_script = None
+        if user_scripts:
+            instance_script = user_scripts.get("instances", {}).get(instance)
+            if not instance_script:
+                os_type = osmorphing_info.get("os_type")
+                if os_type:
+                    instance_script = user_scripts.get(
+                        "global", {}).get(os_type)
+
         osmorphing_manager.morph_image(
             origin_provider,
             destination_provider,
             osmorphing_connection_info,
             osmorphing_info,
+            instance_script,
             event_handler)
 
         return task_info

+ 22 - 0
coriolis/utils.py

@@ -204,6 +204,28 @@ def write_ssh_file(ssh, remote_path, content):
     fd.close()
 
 
+@retry_on_error()
+def write_winrm_file(conn, remote_path, content):
+    """This is a poor man's scp command that transfers small
+    files, in chunks, over WinRM.
+    """
+    conn.exec_ps_command("rm -Force %s" % remote_path)
+    idx = 0
+    while True:
+        data = content[idx:idx+2048]
+        if not data:
+            break
+        asb64 = base64.b64encode(data).decode()
+        cmd = ("$ErrorActionPreference = 'Stop';"
+               "$x = [System.IO.FileStream]::new('%s', "
+               "[System.IO.FileMode]::Append); $bytes = "
+               "[Convert]::FromBase64String('%s'); $x.Write($bytes, "
+               "0, $bytes.Length); $x.Close()") % (
+                    remote_path, asb64)
+        conn.exec_ps_command(cmd)
+        idx += 2048
+
+
 @retry_on_error()
 def list_ssh_dir(ssh, remote_path):
     sftp = ssh.open_sftp()