From b12a8c5aef72c0adaca197c98f5dfa9cbb8a73d2 Mon Sep 17 00:00:00 2001 From: Oleksandr Kuchmystyi Date: Wed, 22 Jul 2026 16:40:57 +0300 Subject: image: T9021: Save bash history during image upgrade Previously, bash history was silently lost on every image upgrade, forcing operators to start with an empty history each time. Extend the image upgrade script to detect and migrate bash history files from the currently running image to the newly installed one. --- src/op_mode/image_installer.py | 109 +++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index ea5aed088..4e7a70893 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -812,6 +812,18 @@ def copy_ssh_known_hosts() -> bool: return known_hosts_files and ask_yes_no(msg, default=True) +def copy_bash_history() -> bool: + """Ask user to copy bash history + + Returns: + bool: user's decision + """ + + history_files = get_bash_history_files() + msg = 'Would you like to copy bash history?' + return bool(history_files) and ask_yes_no(msg, default=False) + + def console_hint() -> str: pid = getppid() if 'SUDO_USER' in environ else getpid() try: @@ -1116,36 +1128,62 @@ def install_image() -> None: exit(1) -def get_known_hosts_files(for_root=True, for_users=True) -> list: - """Collect all existing `known_hosts` files for root and/or users under /home""" +def _get_users_relative_paths( + relative_paths: list, for_root=True, for_users=True +) -> list: + """ + Retrieve a list of file paths based on given relative paths, optionally + including files from the root directory and user home directories. + """ files = [] if for_root: - base_files = ('/root/.ssh/known_hosts', '/etc/ssh/ssh_known_hosts') - for file_path in base_files: - root_known_hosts = Path(file_path) - if root_known_hosts.exists(): - files.append(root_known_hosts) + for relative_path in relative_paths: + file_path = Path('/root') / relative_path + if file_path.exists(): + files.append(file_path) if for_users: # for each non-system user for user in get_local_users(): home_dir = Path(get_user_home_dir(user)) if home_dir.exists(): - known_hosts = home_dir / '.ssh' / 'known_hosts' - if known_hosts.exists(): - files.append(known_hosts) + for relative_path in relative_paths: + file_path = home_dir / relative_path + if file_path.exists(): + files.append(file_path) + + return [file_path.absolute() for file_path in files] + + +def get_known_hosts_files(for_root=True, for_users=True) -> list: + """Collect all existing `known_hosts` files for root and/or users under /home""" + + files = _get_users_relative_paths( + ['.ssh/known_hosts'], + for_root=for_root, + for_users=for_users, + ) + + if for_root: + # Add also additional global 'ssh_known_hosts' file + root_known_hosts = Path('/etc/ssh/ssh_known_hosts') + if root_known_hosts.exists(): + files.append(root_known_hosts) return files +def _mkdir_and_copy_file(source_path: Path, target_path: Path): + """Create the target directory if it doesn't exist and copy a file from source to target""" + + target_path.parent.mkdir(parents=True, exist_ok=True) + copy(source_path, target_path) + + def migrate_known_hosts(target_dir: str): """Copy `known_hosts` for root and all users to the new image directory""" - def _mkdir_and_copy_file(known_hosts_file, target_known_hosts): - target_known_hosts.parent.mkdir(parents=True, exist_ok=True) - copy(known_hosts_file, target_known_hosts) - # Copy root only files using default path known_hosts_files = get_known_hosts_files(for_root=True, for_users=False) for known_hosts_file in known_hosts_files: @@ -1165,6 +1203,36 @@ def migrate_known_hosts(target_dir: str): _mkdir_and_copy_file(known_hosts_file, target_known_hosts) +def get_bash_history_files(for_root=True, for_users=True) -> list: + """Collect all existing `.bash_history` files for root and/or users under /home""" + + return _get_users_relative_paths( + ['.bash_history'], + for_root=for_root, + for_users=for_users, + ) + + +def migrate_bash_history(target_dir: str): + """Copy `.bash_history` for root and all users to the new image directory""" + + # Copy root's history using its default path + history_files = get_bash_history_files(for_root=True, for_users=False) + for history_file in history_files: + target_history = Path(f'{target_dir}{history_file}') + _mkdir_and_copy_file(history_file, target_history) + + # Preserve non-root users' bash history the same way known_hosts + # is backed up: under /var/.users_backups/{user}, since regular + # user home directories are not otherwise migrated. + history_files = get_bash_history_files(for_root=False, for_users=True) + for history_file in history_files: + username = history_file.parent.name + base_dir = Path(f'{target_dir}/var/.users_backups/{username}') + target_history = base_dir / '.bash_history' + _mkdir_and_copy_file(history_file, target_history) + + @compat.grub_cfg_update def add_image(image_path: str, vrf: str = None, username: str = '', password: str = '', no_prompt: bool = False, force: bool = False) -> None: @@ -1290,19 +1358,28 @@ def add_image(image_path: str, vrf: str = None, username: str = '', chmod_2775(target_config_dir) Path(f'{target_config_dir}/.vyatta_config').touch() - target_ssh_dir: str = f'{root_dir}/boot/{image_name}/rw/etc/ssh/' + # Default persistence directory + target_persist_dir: str = f'{root_dir}/boot/{image_name}/rw' + if no_prompt or copy_ssh_host_keys(): print('Copying SSH host keys') + target_ssh_dir: str = f'{target_persist_dir}/etc/ssh/' Path(target_ssh_dir).mkdir(parents=True) host_keys: list[str] = glob('/etc/ssh/ssh_host*') for host_key in host_keys: copy(host_key, target_ssh_dir) - target_ssh_known_hosts_dir: str = f'{root_dir}/boot/{image_name}/rw' if no_prompt or copy_ssh_known_hosts(): print('Copying SSH known_hosts files') + target_ssh_known_hosts_dir: str = target_persist_dir migrate_known_hosts(target_ssh_known_hosts_dir) + # By default we should disable auto-copy under '--no-prompt' mode + if no_prompt is False and copy_bash_history(): + print('Copying bash history') + target_history_dir: str = target_persist_dir + migrate_bash_history(target_history_dir) + # copy system image and kernel files print('Copying system image files') for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir(): -- cgit v1.2.3