Parcourir la source

Add post-hook functionality when finalizing osmorphing

Fabian Fulga il y a 5 jours
Parent
commit
781aab109f

+ 23 - 2
coriolis/osmorphing/base.py

@@ -228,6 +228,7 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
             conn, os_root_dir, os_root_dev, hypervisor, event_manager,
             conn, os_root_dir, os_root_dev, hypervisor, event_manager,
             detected_os_info, osmorphing_parameters, operation_timeout)
             detected_os_info, osmorphing_parameters, operation_timeout)
         self._ssh = conn
         self._ssh = conn
+        self._grub2_update_scheduled = False
 
 
     @classmethod
     @classmethod
     def get_required_detected_os_info_fields(cls):
     def get_required_detected_os_info_fields(cls):
@@ -335,6 +336,7 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
         self._copy_resolv_conf()
         self._copy_resolv_conf()
 
 
     def post_packages_install(self, package_names):
     def post_packages_install(self, package_names):
+        self._run_scheduled_grub2_update()
         self._restore_resolv_conf()
         self._restore_resolv_conf()
 
 
     def pre_packages_uninstall(self, package_names):
     def pre_packages_uninstall(self, package_names):
@@ -517,7 +519,7 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
         if self._test_path(grub_conf_disabler):
         if self._test_path(grub_conf_disabler):
             self._exec_cmd_chroot(
             self._exec_cmd_chroot(
                 "sed -i '/cloud-init=disabled/d' %s" % grub_conf_disabler)
                 "sed -i '/cloud-init=disabled/d' %s" % grub_conf_disabler)
-            self._execute_update_grub()
+            self._schedule_grub2_update()
 
 
     def _reset_cloud_init_run(self):
     def _reset_cloud_init_run(self):
         self._exec_cmd_chroot("cloud-init clean --logs")
         self._exec_cmd_chroot("cloud-init clean --logs")
@@ -707,6 +709,25 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
         update_cmd = self.get_update_grub2_command()
         update_cmd = self.get_update_grub2_command()
         self._exec_cmd_chroot(update_cmd)
         self._exec_cmd_chroot(update_cmd)
 
 
+    def _schedule_grub2_update(self):
+        """Flags that the GRUB2 config needs to be regenerated.
+
+        This is used to avoid running the potentially slow update command
+        multiple times during OSMorphing. Instead, the update is deferred and
+        run once at the end of the process, in 'post_packages_install'.
+        """
+        self._grub2_update_scheduled = True
+
+    def _run_scheduled_grub2_update(self):
+        """Runs the deferred GRUB2 config regeneration, if one was scheduled.
+
+        The pending flag is cleared so that a subsequent call is a no-op
+        unless another GRUB modification schedules an update again.
+        """
+        if self._grub2_update_scheduled:
+            self._execute_update_grub()
+            self._grub2_update_scheduled = False
+
     def _apply_grub2_config(self, config_obj,
     def _apply_grub2_config(self, config_obj,
                             execute_update_grub=True):
                             execute_update_grub=True):
         self._validate_grub_config_obj(config_obj)
         self._validate_grub_config_obj(config_obj)
@@ -714,7 +735,7 @@ class BaseLinuxOSMorphingTools(BaseOSMorphingTools):
             "mv -f %s %s" % (
             "mv -f %s %s" % (
                 config_obj["location"], config_obj["source"]))
                 config_obj["location"], config_obj["source"]))
         if execute_update_grub:
         if execute_update_grub:
-            self._execute_update_grub()
+            self._schedule_grub2_update()
 
 
     def _set_grub2_console_settings(self, consoles=None, speed=None,
     def _set_grub2_console_settings(self, consoles=None, speed=None,
                                     parity=None, grub_conf=None,
                                     parity=None, grub_conf=None,

+ 2 - 2
coriolis/osmorphing/debian.py

@@ -60,7 +60,7 @@ class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
             "GRUB_CMDLINE_LINUX",
             "GRUB_CMDLINE_LINUX",
             {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0})
             {"opt_type": "key_val", "opt_key": "biosdevname", "opt_val": 0})
         self._write_file_sudo("etc/default/grub", cfg.dump())
         self._write_file_sudo("etc/default/grub", cfg.dump())
-        self._exec_cmd_chroot("/usr/sbin/update-grub")
+        self._schedule_grub2_update()
 
 
     def get_update_grub2_command(self):
     def get_update_grub2_command(self):
         return "update-grub"
         return "update-grub"
@@ -134,7 +134,7 @@ class BaseDebianMorphingTools(base.BaseLinuxOSMorphingTools):
             f"grub-install --removable --target={arch}-efi "
             f"grub-install --removable --target={arch}-efi "
             "--efi-directory=/boot/efi --uefi-secure-boot"
             "--efi-directory=/boot/efi --uefi-secure-boot"
         )
         )
-        self._exec_cmd_chroot("update-grub")
+        self._schedule_grub2_update()
 
 
     def set_net_config(self, nics_info, dhcp):
     def set_net_config(self, nics_info, dhcp):
         if not dhcp:
         if not dhcp:

+ 50 - 10
coriolis/tests/osmorphing/test_base.py

@@ -262,10 +262,14 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
         self.os_morphing_tools.pre_packages_install(mock.sentinel.package_name)
         self.os_morphing_tools.pre_packages_install(mock.sentinel.package_name)
         mock_copy_resolv_conf.assert_called_once_with()
         mock_copy_resolv_conf.assert_called_once_with()
 
 
+    @mock.patch.object(
+        base.BaseLinuxOSMorphingTools, '_run_scheduled_grub2_update')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_restore_resolv_conf')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_restore_resolv_conf')
-    def test_post_packages_install(self, mock_restore_resolv_conf):
+    def test_post_packages_install(self, mock_restore_resolv_conf,
+                                   mock_run_scheduled_grub2_update):
         self.os_morphing_tools.post_packages_install(
         self.os_morphing_tools.post_packages_install(
             mock.sentinel.package_name)
             mock.sentinel.package_name)
+        mock_run_scheduled_grub2_update.assert_called_once_with()
         mock_restore_resolv_conf.assert_called_once_with()
         mock_restore_resolv_conf.assert_called_once_with()
 
 
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_copy_resolv_conf')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_copy_resolv_conf')
@@ -636,12 +640,13 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
         ], True)
         ], True)
     )
     )
     @ddt.unpack
     @ddt.unpack
-    @mock.patch.object(base.BaseLinuxOSMorphingTools, "_execute_update_grub")
+    @mock.patch.object(base.BaseLinuxOSMorphingTools, "_schedule_grub2_update")
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_exec_cmd_chroot")
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_exec_cmd_chroot")
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_test_path")
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_test_path")
     def test__ensure_cloud_init_not_disabled(
     def test__ensure_cloud_init_not_disabled(
             self, test_path_results, expected_cmds, updates_grub,
             self, test_path_results, expected_cmds, updates_grub,
-            mock__test_path, mock__exec_cmd_chroot, mock__execute_update_grub):
+            mock__test_path, mock__exec_cmd_chroot,
+            mock__schedule_grub2_update):
         mock__test_path.side_effect = test_path_results
         mock__test_path.side_effect = test_path_results
 
 
         self.os_morphing_tools._ensure_cloud_init_not_disabled()
         self.os_morphing_tools._ensure_cloud_init_not_disabled()
@@ -650,9 +655,9 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
             call.args[0] for call in mock__exec_cmd_chroot.call_args_list]
             call.args[0] for call in mock__exec_cmd_chroot.call_args_list]
         self.assertEqual(called_cmds, expected_cmds)
         self.assertEqual(called_cmds, expected_cmds)
         if updates_grub:
         if updates_grub:
-            mock__execute_update_grub.assert_called_once()
+            mock__schedule_grub2_update.assert_called_once()
         else:
         else:
-            mock__execute_update_grub.assset_not_called()
+            mock__schedule_grub2_update.assert_not_called()
 
 
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_exec_cmd_chroot")
     @mock.patch.object(base.BaseLinuxOSMorphingTools, "_exec_cmd_chroot")
     def test__reset_cloud_init_run(self, mock__exec_cmd_chroot):
     def test__reset_cloud_init_run(self, mock__exec_cmd_chroot):
@@ -1066,9 +1071,44 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
         )
         )
 
 
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_execute_update_grub')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_execute_update_grub')
+    def test__schedule_grub2_update(self, mock_execute_update_grub):
+        self.assertFalse(self.os_morphing_tools._grub2_update_scheduled)
+
+        self.os_morphing_tools._schedule_grub2_update()
+
+        self.assertTrue(self.os_morphing_tools._grub2_update_scheduled)
+        # Scheduling must not run the (slow) update command itself.
+        mock_execute_update_grub.assert_not_called()
+
+    @mock.patch.object(base.BaseLinuxOSMorphingTools, '_execute_update_grub')
+    def test__run_scheduled_grub2_update_when_scheduled(
+            self, mock_execute_update_grub):
+        self.os_morphing_tools._grub2_update_scheduled = True
+
+        self.os_morphing_tools._run_scheduled_grub2_update()
+
+        mock_execute_update_grub.assert_called_once_with()
+        # The pending flag must be cleared so subsequent calls are no-ops.
+        self.assertFalse(self.os_morphing_tools._grub2_update_scheduled)
+
+        # Running the update again must be a no-op.
+        self.os_morphing_tools._run_scheduled_grub2_update()
+        # Check that the update command was called only once.
+        self.assertEqual(1, mock_execute_update_grub.call_count)
+
+    @mock.patch.object(base.BaseLinuxOSMorphingTools, '_execute_update_grub')
+    def test__run_scheduled_grub2_update_when_not_scheduled(
+            self, mock_execute_update_grub):
+        self.os_morphing_tools._grub2_update_scheduled = False
+
+        self.os_morphing_tools._run_scheduled_grub2_update()
+
+        mock_execute_update_grub.assert_not_called()
+
+    @mock.patch.object(base.BaseLinuxOSMorphingTools, '_schedule_grub2_update')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot')
     def test__apply_grub2_config(self, mock_exec_cmd_chroot,
     def test__apply_grub2_config(self, mock_exec_cmd_chroot,
-                                 mock_execute_update_grub):
+                                 mock_schedule_grub2_update):
         config_obj = {
         config_obj = {
             'location': mock.sentinel.location,
             'location': mock.sentinel.location,
             'source': mock.sentinel.source,
             'source': mock.sentinel.source,
@@ -1083,12 +1123,12 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
         mock_exec_cmd_chroot.assert_called_once_with(
         mock_exec_cmd_chroot.assert_called_once_with(
             'mv -f %s %s' % (config_obj['location'], config_obj['source'])
             'mv -f %s %s' % (config_obj['location'], config_obj['source'])
         )
         )
-        mock_execute_update_grub.assert_called_once_with()
+        mock_schedule_grub2_update.assert_called_once_with()
 
 
-    @mock.patch.object(base.BaseLinuxOSMorphingTools, '_execute_update_grub')
+    @mock.patch.object(base.BaseLinuxOSMorphingTools, '_schedule_grub2_update')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot')
     @mock.patch.object(base.BaseLinuxOSMorphingTools, '_exec_cmd_chroot')
     def test__apply_grub2_config_no_update_grub(self, mock_exec_cmd_chroot,
     def test__apply_grub2_config_no_update_grub(self, mock_exec_cmd_chroot,
-                                                mock_execute_update_grub):
+                                                mock_schedule_grub2_update):
         config_obj = {
         config_obj = {
             'location': mock.sentinel.location,
             'location': mock.sentinel.location,
             'source': mock.sentinel.source,
             'source': mock.sentinel.source,
@@ -1103,7 +1143,7 @@ class BaseLinuxOSMorphingToolsTestBase(test_base.CoriolisBaseTestCase):
         mock_exec_cmd_chroot.assert_called_once_with(
         mock_exec_cmd_chroot.assert_called_once_with(
             'mv -f %s %s' % (config_obj['location'], config_obj['source'])
             'mv -f %s %s' % (config_obj['location'], config_obj['source'])
         )
         )
-        mock_execute_update_grub.assert_not_called()
+        mock_schedule_grub2_update.assert_not_called()
 
 
     def test__set_grub2_console_settings_invalid_parity(self):
     def test__set_grub2_console_settings_invalid_parity(self):
         self.assertRaises(
         self.assertRaises(

+ 10 - 7
coriolis/tests/osmorphing/test_debian.py

@@ -48,15 +48,16 @@ class BaseDebianMorphingToolsTestCase(test_base.CoriolisBaseTestCase):
 
 
         self.assertFalse(result)
         self.assertFalse(result)
 
 
+    @mock.patch.object(
+        debian.BaseDebianMorphingTools, '_schedule_grub2_update')
     @mock.patch('coriolis.utils.Grub2ConfigEditor')
     @mock.patch('coriolis.utils.Grub2ConfigEditor')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_test_path_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_test_path_chroot')
-    @mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_write_file_sudo')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_write_file_sudo')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_read_file_sudo')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_read_file_sudo')
     def test_disable_predictable_nic_names(
     def test_disable_predictable_nic_names(
             self, mock_read_file_sudo, mock_write_file_sudo,
             self, mock_read_file_sudo, mock_write_file_sudo,
-            mock_exec_cmd_chroot, mock_test_path_chroot,
-            mock_grub2_cfg_editor):
+            mock_test_path_chroot, mock_grub2_cfg_editor,
+            mock_schedule_grub2_update):
         mock_test_path_chroot.return_value = True
         mock_test_path_chroot.return_value = True
 
 
         self.morpher.disable_predictable_nic_names()
         self.morpher.disable_predictable_nic_names()
@@ -87,7 +88,7 @@ class BaseDebianMorphingToolsTestCase(test_base.CoriolisBaseTestCase):
         mock_read_file_sudo.assert_called_once_with('etc/default/grub')
         mock_read_file_sudo.assert_called_once_with('etc/default/grub')
         mock_write_file_sudo.assert_called_once_with(
         mock_write_file_sudo.assert_called_once_with(
             "etc/default/grub", mock_grub2_cfg_editor.return_value.dump())
             "etc/default/grub", mock_grub2_cfg_editor.return_value.dump())
-        mock_exec_cmd_chroot.assert_called_once_with("/usr/sbin/update-grub")
+        mock_schedule_grub2_update.assert_called_once_with()
 
 
     @mock.patch('coriolis.utils.Grub2ConfigEditor')
     @mock.patch('coriolis.utils.Grub2ConfigEditor')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
@@ -430,20 +431,21 @@ deb http://archive.debian.org/debian wheezy-updates main non-free-firmware
         mock_write_file_sudo.assert_not_called()
         mock_write_file_sudo.assert_not_called()
         mock_exec_cmd_chroot.assert_not_called()
         mock_exec_cmd_chroot.assert_not_called()
 
 
+    @mock.patch.object(
+        debian.BaseDebianMorphingTools, '_schedule_grub2_update')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_test_path_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_test_path_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
     @mock.patch.object(debian.BaseDebianMorphingTools, '_exec_cmd_chroot')
     def test_install_uefi_fallback_bootloader(
     def test_install_uefi_fallback_bootloader(
         self,
         self,
         mock_exec_cmd_chroot,
         mock_exec_cmd_chroot,
         mock_test_path_chroot,
         mock_test_path_chroot,
+        mock_schedule_grub2_update,
     ):
     ):
         mock_exec_cmd_chroot.side_effect = [
         mock_exec_cmd_chroot.side_effect = [
             # uname -m
             # uname -m
             "x86_64",
             "x86_64",
             # grub-install
             # grub-install
             "",
             "",
-            # update-grub
-            "",
         ]
         ]
         mock_test_path_chroot.return_value = False
         mock_test_path_chroot.return_value = False
 
 
@@ -454,8 +456,9 @@ deb http://archive.debian.org/debian wheezy-updates main non-free-firmware
             mock.call(
             mock.call(
                 "grub-install --removable --target=x86_64-efi "
                 "grub-install --removable --target=x86_64-efi "
                 "--efi-directory=/boot/efi --uefi-secure-boot"),
                 "--efi-directory=/boot/efi --uefi-secure-boot"),
-            mock.call("update-grub"),
         ])
         ])
+        # The grub update must be deferred (scheduled) rather than run now.
+        mock_schedule_grub2_update.assert_called_once_with()
         mock_test_path_chroot.assert_called_once_with(
         mock_test_path_chroot.assert_called_once_with(
             "/boot/efi/EFI/BOOT/BOOTX64.efi")
             "/boot/efi/EFI/BOOT/BOOTX64.efi")