Преглед изворни кода

tests: Adds integration test for OS morphing with minion pools

Covers the OS morphing step during deployments using minion pools.

Fixes AttachVolumesToOSMorphingMinionTask reading the wrong task_info key.
_get_volumes_info_from_task_info currently hits a KeyError by accessing
task_info["instance_deployment_info"]["volumes_info"], which starts
being populated in its _run method, *after* running super()._run() (where
_get_volumes_info_from_task_info is first called). Instead, _get_volumes_info_from_task_info
should return task_info["volumes_info"]. Note that _BaseAttachVolumesToTransferMinionTask
already returns task_info["volumes_info"].

Updates TestImportProvider's methods related to OS morphing minions.
The minions should be able to run `modprobe dm-mod`, which is why
`/lib/modules` is mounted in the minions (mirrors deploy_os_morphing_resources).
get_additional_os_morphing_info should return a dict containing "osmorphing_info".
Claudiu Belu пре 2 недеља
родитељ
комит
a15bfcd57c

+ 3 - 2
coriolis/tasks/minion_pool_tasks.py

@@ -565,14 +565,15 @@ class AttachVolumesToOSMorphingMinionTask(
 
     @classmethod
     def _get_volumes_info_from_task_info(cls, task_info):
-        return task_info[
-            "instance_deployment_info"]["volumes_info"]
+        # Similar to _BaseAttachVolumesToTransferMinionTask.
+        return task_info["volumes_info"]
 
     @classmethod
     def get_required_task_info_properties(cls):
         fields = super(
             AttachVolumesToOSMorphingMinionTask,
             cls).get_required_task_info_properties()
+        fields.append("volumes_info")
         fields.append("instance_deployment_info")
         return fields
 

+ 45 - 1
coriolis/tests/integration/deployments/test_osmorphing.py

@@ -12,12 +12,14 @@ import re
 import unittest
 import uuid
 
+from coriolis.db import api as db_api
 from coriolis.tests.integration import base as integration_base
 from coriolis.tests.integration import harness as integration_harness
 from coriolis.tests.integration import osmorphing_utils
 
 
-class OsMorphingDeploymentTest(integration_base.ReplicaIntegrationTestBase):
+class OsMorphingDeploymentTestBase(
+        integration_base.ReplicaIntegrationTestBase):
 
     # NOTE(claudiub): Size must be high enough to contain the tested OS and
     # any new packages to be added during OS morphing.
@@ -36,6 +38,8 @@ class OsMorphingDeploymentTest(integration_base.ReplicaIntegrationTestBase):
         osmorphing_utils.write_os_image_to_disk(
             self._src_device, "ubuntu:24.04")
 
+
+class OsMorphingDeploymentTest(OsMorphingDeploymentTestBase):
     def test_deployment_with_os_morphing(self):
         self.assertFalse(
             osmorphing_utils.path_exists_on_device(
@@ -192,3 +196,43 @@ class OsMorphingDeploymentTest(integration_base.ReplicaIntegrationTestBase):
         if not found:
             raise AssertionError(
                 "Couldn't find the expected first boot script.")
+
+
+class OsMorphingMinionPoolDeploymentTest(
+        integration_base.MinionPoolTestBase, OsMorphingDeploymentTestBase):
+    """OS morphing deployment using a minion pool for the OS morphing phase."""
+
+    _CREATE_MINION_POOLS = True
+
+    def test_deployment_with_os_morphing(self):
+        self.assertFalse(
+            osmorphing_utils.path_exists_on_device(
+                self._src_device, "usr/bin/jq"),
+            "jq was found on the source device before OS morphing",
+        )
+
+        deployment_kwargs = {
+            "instance_osmorphing_minion_pool_mappings": {
+                self._instance_name: self._pool_id,
+            },
+        }
+        self._execute_transfer_and_deployment(deployment_kwargs)
+
+        self.assertTrue(
+            osmorphing_utils.path_exists_on_device(
+                self._dst_device, "usr/bin/jq"),
+            "jq was not found on the destination device after OS morphing",
+        )
+
+        ctxt = self._get_db_context()
+        pool = db_api.get_minion_pool(
+            ctxt, self._pool_id, include_machines=True)
+        self.assertTrue(
+            pool.minion_machines,
+            "OS morphing pool has no minion machines")
+
+        for machine in pool.minion_machines:
+            self.assertIsNotNone(
+                machine.last_used_at,
+                "OS morphing minion machine %s was never used" % machine.id,
+            )

+ 21 - 2
coriolis/tests/integration/test_provider/imp.py

@@ -414,8 +414,12 @@ class TestImportProvider(
         # We must pre-authorize all block devices through the
         # --device-cgroup-rule option, otherwise any device added will be
         # inaccessible ("operation not permitted" error on open).
+        #
+        # Mount the host's /lib/modules tree so that modprobe can
+        # resolve built-in modules.
+        volumes = ["/lib/modules:/lib/modules:ro"]
         result = self._create_minion(
-            "coriolis-pool-minion", connection_info, [],
+            "coriolis-pool-minion", connection_info, [], volumes,
             device_cgroup_rules=["b *:* rwm"])
 
         backup_writer_conn_info = result["backup_writer_connection_info"]
@@ -493,4 +497,19 @@ class TestImportProvider(
     def get_additional_os_morphing_info(
             self, ctxt, connection_info, target_environment,
             instance_deployment_info):
-        return {}
+        devices = list(instance_deployment_info.get("devices", []))
+
+        # lsblk inside the container sees all the host block devices because
+        # Docker containers share the host kernel's sysfs (/sys/block/).
+        # Populate ignore_devices with every host disk except the target
+        # so osmorphing only considers the devices we actually attached.
+        ignore_devices = list(
+            test_utils.get_host_disk_devices() - set(devices)
+        )
+
+        return {
+            "osmorphing_info": {
+                "os_type": instance_deployment_info.get("os_type", "linux"),
+                "ignore_devices": ignore_devices,
+            }
+        }

+ 10 - 1
coriolis/tests/tasks/test_minion_pool_tasks.py

@@ -466,6 +466,13 @@ class AttachVolumesToOSMorphingMinionTaskTestCase(
         super(AttachVolumesToOSMorphingMinionTaskTestCase, self).setUp()
         self.task_runner = mp_tasks.AttachVolumesToOSMorphingMinionTask()
 
+    def test__get_volumes_info_from_task_info(self):
+        task_info = {"volumes_info": [{"id": "vol1"}]}
+        result = (
+            mp_tasks.AttachVolumesToOSMorphingMinionTask.
+            _get_volumes_info_from_task_info(task_info))
+        self.assertEqual([{"id": "vol1"}], result)
+
     def test_get_required_task_info_properties(self):
         mock_super_call = mock.MagicMock(return_value=["field1", "field2"])
         with mock.patch.object(
@@ -476,7 +483,9 @@ class AttachVolumesToOSMorphingMinionTaskTestCase(
                 get_required_task_info_properties()
             self.assertEqual(
                 sorted(result),
-                sorted(['field1', 'field2', 'instance_deployment_info']))
+                sorted([
+                    'field1', 'field2', 'volumes_info',
+                    'instance_deployment_info']))
 
     def test_get_returned_task_info_properties(self):
         mock_super_call = mock.MagicMock(return_value=["field1", "field2"])