Sfoglia il codice sorgente

Improve image mount failure handling

As shown in the following logs, ISO image mounting can fail silently.
We won't get a drive letter, nor a non-zero exit code. However,
attemptng to subsequently dismount the image fails.

This change will raise an exception if no drive letter was received,
including the original stderr as well as the image file size, which
can help us determine if an invalid image was received.

```
2026-06-05 13:39:01.124 DEBUG coriolis.wsman [-] Executing PS command: (Mount-DiskImage 'c:\virtio-win.iso' -PassThru | Get-Volume).DriveLetter from (pid=4517) exec_ps_command /var/log/coriolis/cori
olis/coriolis/wsman.py:149
2026-06-05 13:39:08.868 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k25\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:11.554 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k22\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:14.094 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k19\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:16.689 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k16\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:19.331 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\w10\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:21.893 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k12R2\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:24.450 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k12\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:27.054 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k8R2\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:29.755 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k8\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:32.449 DEBUG coriolis.wsman [-] Executing PS command: Test-Path -Path ":\Balloon\2k3\amd64" from (pid=4517) exec_ps_command /var/log/coriolis/coriolis/coriolis/wsman.py:149
2026-06-05 13:39:35.011 INFO coriolis.osmorphing.windows [-] Unmounting disk image: c:\virtio-win.iso
...
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server     self._conn.exec_ps_command("Dismount-DiskImage '%s'" % path,
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server   File "/var/log/coriolis/coriolis/coriolis/wsman.py", line 151, in exec_ps_command
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server     return self.exec_command(
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server   File "/var/log/coriolis/coriolis/coriolis/wsman.py", line 141, in exec_command
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server     raise exception.CoriolisException(
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server coriolis.exception.CoriolisException: Command "['powershell.exe', '-EncodedCommand', 'RABpAHMAbQBvAHUAbgB0AC0ARABpAHMAawBJAG0AYQBnAGUAIAAnAGMAOgBcAHYAaQByAHQAaQBvAC0AdwBpAG4ALgBpAHMAbwAnAA==', '-NonInteractive', '-ExecutionPolicy', 'RemoteSigned']" failed with exit code: 1
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server stdout:
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server std_err: #< CLIXML
2026-06-05 13:40:04.250 TRACE coriolis.worker.rpc.server <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><Obj S="progress" RefId="1"><TNRef RefId="0" /><MS><I64 N="SourceId">2</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><Obj S="progress" RefId="2"><TNRef RefId="0" /><MS><I64 N="SourceId">3</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><S S="Error">Dismount-DiskImage : The request could not be performed because of an I/O device error. _x000D__x000A_</S><S S="Error">At line:1 char:1_x000D__x000A_</S><S S="Error">+ Dismount-DiskImage 'c:\virtio-win.iso'_x000D__x000A_</S><S S="Error">+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~_x000D__x000A_</S><S S="Error">    + CategoryInfo          : NotSpecified: (MSFT_DiskImage:ROOT/Microsoft/.../MSFT_DiskImage) [Dismount-DiskImage], C _x000D__x000A_</S><S S="Error">   imException_x000D__x000A_</S><S S="Error">    + FullyQualifiedErrorId : HRESULT 0x8007045d,Dismount-DiskImage_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S><Obj S="progress" RefId="3"><TNRef RefId="0" /><MS><I64 N="SourceId">4</I64><PR N="Record"><AV>Dismount-DiskImage 'c:\virtio-win.iso'</AV><AI>1393322851</AI><Nil /><PI>-1</PI><PC>100</PC><T>Completed</T><SR>0</SR><SD>1/1 completed</SD></PR></MS></Obj></Objs
```
Lucian Petrut 2 settimane fa
parent
commit
da5cea6e31

+ 12 - 2
coriolis/osmorphing/windows.py

@@ -274,9 +274,19 @@ class BaseWindowsMorphingTools(base.BaseOSMorphingTools):
 
     def _mount_disk_image(self, path):
         LOG.info("Mounting disk image: %s" % path)
-        return self._conn.exec_ps_command(
+        drive_letter, stderr = self._conn.exec_ps_command(
             "(Mount-DiskImage '%s' -PassThru | Get-Volume).DriveLetter" %
-            path)
+            path,
+            include_stderr=True)
+        if not drive_letter:
+            # Couldn't mount the image, let's fetch the file size. It may
+            # help us spot invalid images.
+            file_size = self._conn.exec_ps_command(f"(ls '{path}').Length")
+            raise exception.CoriolisException(
+                f"Could not mount image: '{path}', "
+                f"file size: {file_size}, "
+                f"mount stderr: {stderr}")
+        return drive_letter
 
     def _dismount_disk_image(self, path):
         LOG.info("Unmounting disk image: %s" % path)

+ 12 - 2
coriolis/tests/osmorphing/test_windows.py

@@ -203,14 +203,24 @@ class BaseWindowsMorphingToolsTestCase(test_base.CoriolisBaseTestCase):
         mock_get_worker_os_drive_path.assert_called()
 
     def test__mount_disk_image(self):
+        self.conn.exec_ps_command.return_value = ("fake-letter", "fake stderr")
         result = self.morphing_tools._mount_disk_image(
             mock.sentinel.path)
 
         self.conn.exec_ps_command.assert_called_once_with(
             "(Mount-DiskImage '%s' -PassThru | Get-Volume).DriveLetter" %
-            mock.sentinel.path)
+            mock.sentinel.path,
+            include_stderr=True)
 
-        self.assertEqual(result, self.conn.exec_ps_command.return_value)
+        self.assertEqual("fake-letter", result)
+
+    def test__mount_disk_image_missing_letter(self):
+        self.conn.exec_ps_command.side_effect = [
+            ("", "fake stderr"), "fake-size"]
+        self.assertRaises(
+            exception.CoriolisException,
+            self.morphing_tools._mount_disk_image,
+            mock.sentinel.path)
 
     def test__dismount_disk_image(self):
         self.morphing_tools._dismount_disk_image(mock.sentinel.path)

+ 26 - 2
coriolis/tests/test_wsman.py

@@ -153,7 +153,10 @@ class WSManConnectionTestCase(test_base.CoriolisBaseTestCase):
     def test_exec_ps_command(self):
         self.conn.exec_command = mock.Mock()
         self.conn.exec_command.return_value = "std_out\n\n"
-        result = self.conn.exec_ps_command(self.cmd)
+        result = self.conn.exec_ps_command(
+            self.cmd,
+            include_stderr=False,
+        )
         self.conn.exec_command.assert_called_once_with(
             "powershell.exe",
             [
@@ -163,9 +166,30 @@ class WSManConnectionTestCase(test_base.CoriolisBaseTestCase):
                 '-ExecutionPolicy', 'RemoteSigned',
             ],
             timeout=None,
-            sanitizable=False)
+            sanitizable=False,
+            include_stderr=False)
         self.assertEqual(result, "std_out")
 
+    def test_exec_ps_command_with_stderr(self):
+        self.conn.exec_command = mock.Mock()
+        self.conn.exec_command.return_value = "std_out\n\n", "stderr"
+        result = self.conn.exec_ps_command(
+            self.cmd,
+            include_stderr=True,
+        )
+        self.conn.exec_command.assert_called_once_with(
+            "powershell.exe",
+            [
+                "-EncodedCommand",
+                'dABlAHMAdABfAGMAbQBkAA==',
+                '-NonInteractive',
+                '-ExecutionPolicy', 'RemoteSigned',
+            ],
+            timeout=None,
+            sanitizable=False,
+            include_stderr=True)
+        self.assertEqual(result, ("std_out", "stderr"))
+
     def test_test_path(self):
         self.conn.exec_ps_command = mock.Mock()
         self.conn.exec_ps_command.return_value = "True"

+ 26 - 4
coriolis/wsman.py

@@ -139,7 +139,14 @@ class WSManConnection(object):
             if shell_id:
                 self._protocol.close_shell(shell_id)
 
-    def exec_command(self, cmd, args=[], timeout=None, sanitizable=True):
+    def exec_command(
+        self,
+        cmd,
+        args=[],
+        timeout=None,
+        sanitizable=True,
+        include_stderr=False,
+    ):
         # Our sanitization helpers do not work for base64 encoded commands,
         # in which case we'll avoid logging it so that we won't leak
         # sensitive information.
@@ -158,12 +165,20 @@ class WSManConnection(object):
                 "stdout: %s\nstd_err: %s" %
                 (sanitized_cmd, exit_code, std_out, std_err))
 
+        if include_stderr:
+            return std_out, std_err
         return std_out
 
-    def exec_ps_command(self, cmd, ignore_stdout=False, timeout=None):
+    def exec_ps_command(
+        self,
+        cmd,
+        ignore_stdout=False,
+        timeout=None,
+        include_stderr=False,
+    ):
         LOG.debug("Executing PS command: %s", strutils.mask_password(cmd))
         base64_cmd = base64.b64encode(cmd.encode('utf-16le')).decode()
-        return self.exec_command(
+        ret = self.exec_command(
             "powershell.exe",
             [
                 "-EncodedCommand",
@@ -173,7 +188,14 @@ class WSManConnection(object):
                 "RemoteSigned",
             ],
             timeout=timeout,
-            sanitizable=False)[:-2]
+            sanitizable=False,
+            include_stderr=include_stderr)
+        if include_stderr:
+            stdout, stderr = ret
+            return stdout[:-2], stderr
+        else:
+            stdout = ret
+            return stdout[:-2]
 
     def test_path(self, remote_path):
         ret_val = self.exec_ps_command("Test-Path -Path \"%s\"" % remote_path)