From f74923202311e853b677e52cd83bae2be9605c26 Mon Sep 17 00:00:00 2001
From: zsdc <taras@vyos.io>
Date: Wed, 13 Mar 2024 00:40:09 +0200
Subject: grub: T4548: Fixed configuration files order

To iterate files on ext* file systems GRUB reads their inodes one by one,
ignoring names. This breaks our configuration logic that relies on proper
loading order.

This commit adds a helper `sort_inodes()` that needs to be used whenever GRUB
configuration files are created. It recreates files, changing their inodes in a
way where inodes order matches alphabetical order.
---
 python/vyos/system/grub.py     | 36 ++++++++++++++++++++++++++++++++++++
 src/op_mode/image_installer.py |  4 ++++
 src/system/grub_update.py      |  4 ++++
 3 files changed, 44 insertions(+)

diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py
index 2e8b20972..864ed65aa 100644
--- a/python/vyos/system/grub.py
+++ b/python/vyos/system/grub.py
@@ -17,6 +17,7 @@ import platform
 
 from pathlib import Path
 from re import MULTILINE, compile as re_compile
+from shutil import copy2
 from typing import Union
 from uuid import uuid5, NAMESPACE_URL, UUID
 
@@ -422,3 +423,38 @@ def set_kernel_cmdline_options(cmdline_options: str, version_name: str,
 
     version_add(version_name=version_name, root_dir=root_dir,
                 boot_opts_config=cmdline_options)
+
+
+def sort_inodes(dir_path: str) -> None:
+    """Sort inodes for files inside a folder
+    Regenerate inodes for each file to get the same order for both inodes
+    and file names
+
+    GRUB iterates files by inodes, not alphabetically. Therefore, if we
+    want to read them in proper order, we need to sort inodes for all
+    config files in a folder.
+
+    Args:
+        dir_path (str): a path to directory
+    """
+    dir_content: list[Path] = sorted(Path(dir_path).iterdir())
+    temp_list_old: list[Path] = []
+    temp_list_new: list[Path] = []
+
+    # create a copy of all files, to get new inodes
+    for item in dir_content:
+        # skip directories
+        if item.is_dir():
+            continue
+        # create a new copy of file with a temporary name
+        copy_path = Path(f'{item.as_posix()}_tmp')
+        copy2(item, Path(copy_path))
+        temp_list_old.append(item)
+        temp_list_new.append(copy_path)
+
+    # delete old files and rename new ones
+    for item in temp_list_old:
+        item.unlink()
+    for item in temp_list_new:
+        new_name = Path(f'{item.as_posix()[0:-4]}')
+        item.rename(new_name)
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index 85ebd19ba..b0567305a 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -786,6 +786,10 @@ def install_image() -> None:
             grub.install(install_target.name, f'{DIR_DST_ROOT}/boot/',
                          f'{DIR_DST_ROOT}/boot/efi')
 
+        # sort inodes (to make GRUB read config files in alphabetical order)
+        grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS}')
+        grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS_VERS}')
+
         # umount filesystems and remove temporary files
         if is_raid_install(install_target):
             cleanup([install_target.name],
diff --git a/src/system/grub_update.py b/src/system/grub_update.py
index 5a7d8eb72..5a0534195 100644
--- a/src/system/grub_update.py
+++ b/src/system/grub_update.py
@@ -105,4 +105,8 @@ if __name__ == '__main__':
     else:
         render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {})
 
+    # sort inodes (to make GRUB read config files in alphabetical order)
+    grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS}')
+    grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}')
+
     exit(0)
-- 
cgit v1.2.3