Explorar el Código

fix: Add cryptsetup and modprobe dm-crypt to OS mount tool classes

cryptsetup is required to detect whether a device is LUKS-encrypted or
not. It is now installed on the OS morphing minion.

Now that cryptsetup is present in the OS morphing minion, the
SSHCommandNotFoundException exception case is redundant. Additionally
refines the exception case instead: we only return False if the command
returns the exit code 1, and raise in any other case.
Claudiu Belu hace 1 mes
padre
commit
d2627594bf

+ 3 - 1
coriolis/exception.py

@@ -510,7 +510,9 @@ class MinionMachineCommandTimeout(CoriolisException):
 
 
 class SSHCommandFailed(CoriolisException):
-    pass
+    def __init__(self, message=None, exit_code=None, **kwargs):
+        super(SSHCommandFailed, self).__init__(message, **kwargs)
+        self.exit_code = exit_code
 
 
 class SSHCommandNotFoundException(CoriolisException):

+ 8 - 9
coriolis/osmorphing/osmount/luks_mixin.py

@@ -115,15 +115,14 @@ class LinuxLUKSMixin:
         try:
             self._exec_cmd("sudo cryptsetup isLuks %s" % dev_path)
             return True
-        except exception.SSHCommandNotFoundException:
-            LOG.warn("cryptsetup missing from OS morpher; cannot check if "
-                     "device is LUKS-encrypted.")
-        except Exception:
-            # if it's not LUKS, we'll get exit code 1.
-            # The exception is already logged in self._exec_cmd.
-            pass
-
-        return False
+        except exception.SSHCommandFailed as ex:
+            # cryptsetup exits with 1 specifically when the device is not a
+            # LUKS container. Any other exit code (e.g. 4: device does not
+            # exist or access denied) indicates a real error, not "not LUKS",
+            # and should not be silently swallowed.
+            if ex.exit_code == 1:
+                return False
+            raise
 
     def _close_luks_devices(self):
         """Close any LUKS mapper devices opened by _unlock_luks_devices."""

+ 2 - 1
coriolis/osmorphing/osmount/redhat.py

@@ -20,6 +20,7 @@ class RedHatOSMountTools(base.BaseLinuxOSMountTools):
 
     def setup(self):
         super(RedHatOSMountTools, self).setup()
-        self._exec_cmd("sudo -E yum install -y lvm2 psmisc")
+        self._exec_cmd("sudo -E yum install -y lvm2 psmisc cryptsetup")
         self._exec_cmd("sudo modprobe dm-mod")
+        self._exec_cmd("sudo modprobe dm-crypt")
         self._exec_cmd("sudo rm -f /etc/lvm/devices/system.devices")

+ 3 - 1
coriolis/osmorphing/osmount/suse.py

@@ -40,6 +40,8 @@ class SUSEOSMountTools(base.BaseLinuxOSMountTools):
         super(SUSEOSMountTools, self).setup()
         retry_ssh_cmd = utils.retry_on_error(
             max_attempts=10, sleep_seconds=30)(self._exec_cmd)
-        retry_ssh_cmd("sudo -E zypper --non-interactive install lvm2 psmisc")
+        retry_ssh_cmd(
+            "sudo -E zypper --non-interactive install lvm2 psmisc cryptsetup")
         self._exec_cmd("sudo modprobe dm-mod")
+        self._exec_cmd("sudo modprobe dm-crypt")
         self._exec_cmd("sudo rm -f /etc/lvm/devices/system.devices")

+ 7 - 2
coriolis/osmorphing/osmount/ubuntu.py

@@ -31,8 +31,13 @@ class UbuntuOSMountTools(base.BaseLinuxOSMountTools):
         # NOTE(aznashwan): in case an unattended upgrade is already happening
         # and is at the package installation stage (in which case the
         # /var/lib/dpkg/* locks will be held), we pass a 10-minute timeout:
+        # NOTE: cryptsetup pulls in keyboard-configuration, whose postinst
+        # prompts interactively for a keyboard layout unless
+        # DEBIAN_FRONTEND=noninteractive is set, which would otherwise hang
+        # the install indefinitely.
         self._exec_cmd(
-            "sudo -E apt-get -o DPkg::Lock::Timeout=600 "
-            "install lvm2 psmisc -y")
+            "sudo -E DEBIAN_FRONTEND=noninteractive apt-get "
+            "-o DPkg::Lock::Timeout=600 install lvm2 psmisc cryptsetup -y")
 
         self._exec_cmd("sudo modprobe dm-mod")
+        self._exec_cmd("sudo modprobe dm-crypt")

+ 0 - 1
coriolis/tests/integration/dockerfiles/data-minion/Dockerfile

@@ -8,7 +8,6 @@ FROM ubuntu:24.04
 # kmod is required during OS morphing (modprobe is being called).
 # cryptsetup is required to unlock / lock LUKS-encrypted devices during OS morphing.
 RUN apt-get update && apt-get install -y --no-install-recommends \
-    cryptsetup \
     dbus \
     kmod \
     openssh-server \

+ 9 - 9
coriolis/tests/osmorphing/osmount/test_luks_mixin.py

@@ -135,17 +135,17 @@ class LinuxLUKSMixinTestCase(test_base.CoriolisBaseTestCase):
             "sudo cryptsetup isLuks %s" % _DEV
         )
 
-        # False.
-        mock_exec_cmd.side_effect = Exception("exit code 1")
+        # False (exit code 1: not a LUKS device).
+        mock_exec_cmd.side_effect = exception.SSHCommandFailed(
+            "boom goes the dynamite", exit_code=1)
         self.assertFalse(self.mixin._is_luks(_DEV))
 
-        # SSHCommandNotFoundException, warning.
-        mock_exec_cmd.side_effect = exception.SSHCommandNotFoundException()
-        with self.assertLogs(
-            "coriolis.osmorphing.osmount.luks_mixin", level='WARNING'
-        ):
-            result = self.mixin._is_luks(_DEV)
-        self.assertFalse(result)
+        # Unexpected exit code (e.g. 4: device does not exist or access
+        # denied), hard failure.
+        mock_exec_cmd.side_effect = exception.SSHCommandFailed(
+            "boom goes a different dynamite", exit_code=4)
+        self.assertRaises(
+            exception.SSHCommandFailed, self.mixin._is_luks, _DEV)
 
     @mock.patch.object(base.BaseSSHOSMountTools, "_exec_cmd")
     def test__close_luks_devices(self, mock_exec_cmd):

+ 3 - 2
coriolis/tests/osmorphing/osmount/test_redhat.py

@@ -39,8 +39,9 @@ class BaseRedHatOSMountToolsTestCase(test_base.CoriolisBaseTestCase):
 
         mock_setup.assert_called_once_with()
         mock_exec_cmd.assert_has_calls([
-            mock.call("sudo -E yum install -y lvm2 psmisc"),
-            mock.call("sudo modprobe dm-mod")
+            mock.call("sudo -E yum install -y lvm2 psmisc cryptsetup"),
+            mock.call("sudo modprobe dm-mod"),
+            mock.call("sudo modprobe dm-crypt")
         ])
 
     @mock.patch.object(redhat.base.BaseSSHOSMountTools, '_exec_cmd')

+ 3 - 1
coriolis/tests/osmorphing/osmount/test_suse.py

@@ -65,8 +65,10 @@ class BaseSUSEOSMountToolsTestCase(test_base.CoriolisBaseTestCase):
             max_attempts=10, sleep_seconds=30)
         mock_exec_cmd.assert_has_calls([
             mock.call(
-                "sudo -E zypper --non-interactive install lvm2 psmisc"),
+                "sudo -E zypper --non-interactive install "
+                "lvm2 psmisc cryptsetup"),
             mock.call("sudo modprobe dm-mod"),
+            mock.call("sudo modprobe dm-crypt"),
             mock.call("sudo rm -f /etc/lvm/devices/system.devices")
         ])
 

+ 5 - 3
coriolis/tests/osmorphing/osmount/test_ubuntu.py

@@ -39,9 +39,11 @@ class UbuntuOSMountToolsTestCase(test_base.CoriolisBaseTestCase):
         mock_setup.assert_called_once_with()
         mock_exec_cmd.assert_has_calls([
             mock.call("sudo -E apt-get update -y"),
-            mock.call("sudo -E apt-get -o DPkg::Lock::Timeout=600 "
-                      "install lvm2 psmisc -y"),
-            mock.call("sudo modprobe dm-mod")
+            mock.call("sudo -E DEBIAN_FRONTEND=noninteractive apt-get "
+                      "-o DPkg::Lock::Timeout=600 install lvm2 psmisc "
+                      "cryptsetup -y"),
+            mock.call("sudo modprobe dm-mod"),
+            mock.call("sudo modprobe dm-crypt")
         ])
 
     @mock.patch.object(ubuntu.base.BaseSSHOSMountTools, '_exec_cmd')

+ 3 - 2
coriolis/tests/test_utils.py

@@ -403,8 +403,9 @@ class UtilsTestCase(test_base.CoriolisBaseTestCase):
         self.mock_ssh.exec_command.return_value = (None, self.mock_stdout,
                                                    self.mock_stdout)
 
-        self.assertRaises(exception.SSHCommandFailed, utils.exec_ssh_cmd,
-                          self.mock_ssh, "command")
+        with self.assertRaises(exception.SSHCommandFailed) as ex:
+            utils.exec_ssh_cmd(self.mock_ssh, "command")
+            self.assertEqual(1, ex.exception.exit_code)
 
         self.mock_ssh.exec_command.assert_called_once_with(
             "command", environment=None, get_pty=False, timeout=None)

+ 1 - 1
coriolis/utils.py

@@ -350,7 +350,7 @@ def _exec_ssh_cmd(ssh, cmd, environment=None, get_pty=False, timeout=None):
                 "command not found" in stdout_str or
                 "command not found" in stderr_str):
             raise exception.SSHCommandNotFoundException(msg)
-        raise exception.SSHCommandFailed(msg)
+        raise exception.SSHCommandFailed(msg, exit_code=exit_code)
     # Most of the commands will use pseudo-terminal which unfortunately will
     # include a '\r' to every newline. This will affect all plugins too, so
     # best we can do now is replace them.