summaryrefslogtreecommitdiff
path: root/src/op_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/op_mode')
-rwxr-xr-xsrc/op_mode/image_installer.py137
-rwxr-xr-xsrc/op_mode/image_manager.py17
2 files changed, 136 insertions, 18 deletions
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index aa4cf301b..b3e6e518c 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -20,6 +20,7 @@
from argparse import ArgumentParser, Namespace
from pathlib import Path
from shutil import copy, chown, rmtree, copytree
+from glob import glob
from sys import exit
from time import sleep
from typing import Union
@@ -59,6 +60,8 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user'
MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?'
MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?'
MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?'
+MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?'
+MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?'
MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?'
MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?'
MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.'
@@ -183,6 +186,83 @@ def create_partitions(target_disk: str, target_size: int,
return disk_details
+def search_format_selection(image: tuple[str, str]) -> str:
+ """Format a string for selection of image
+
+ Args:
+ image (tuple[str, str]): a tuple of image name and drive
+
+ Returns:
+ str: formatted string
+ """
+ return f'{image[0]} on {image[1]}'
+
+
+def search_previous_installation(disks: list[str]) -> None:
+ """Search disks for previous installation config and SSH keys
+
+ Args:
+ disks (list[str]): a list of available disks
+ """
+ mnt_config = '/mnt/config'
+ mnt_ssh = '/mnt/ssh'
+ mnt_tmp = '/mnt/tmp'
+ rmtree(Path(mnt_config), ignore_errors=True)
+ rmtree(Path(mnt_ssh), ignore_errors=True)
+ Path(mnt_tmp).mkdir(exist_ok=True)
+
+ print('Searching for data from previous installations')
+ image_data = []
+ for disk_name in disks:
+ for partition in disk.partition_list(disk_name):
+ if disk.partition_mount(partition, mnt_tmp):
+ if Path(mnt_tmp + '/boot').exists():
+ for path in Path(mnt_tmp + '/boot').iterdir():
+ if path.joinpath('rw/config/.vyatta_config').exists():
+ image_data.append((path.name, partition))
+
+ disk.partition_umount(partition)
+
+ if len(image_data) == 1:
+ image_name, image_drive = image_data[0]
+ print('Found data from previous installation:')
+ print(f'\t{image_name} on {image_drive}')
+ if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ return
+
+ elif len(image_data) > 1:
+ print('Found data from previous installations')
+ if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True):
+ return
+
+ image_name, image_drive = select_entry(image_data,
+ 'Available versions:',
+ MSG_INPUT_CHOOSE_COPY_DATA,
+ search_format_selection)
+ else:
+ print('No previous installation found')
+ return
+
+ disk.partition_mount(image_drive, mnt_tmp)
+
+ copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config)
+ Path(mnt_ssh).mkdir()
+ host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*')
+ for host_key in host_keys:
+ copy(host_key, mnt_ssh)
+
+ disk.partition_umount(image_drive)
+
+
+def copy_previous_installation_data(target_dir: str) -> None:
+ if Path('/mnt/config').exists():
+ copytree('/mnt/config', f'{target_dir}/opt/vyatta/etc/config',
+ dirs_exist_ok=True)
+ if Path('/mnt/ssh').exists():
+ copytree('/mnt/ssh', f'{target_dir}/etc/ssh',
+ dirs_exist_ok=True)
+
+
def ask_single_disk(disks_available: dict[str, int]) -> str:
"""Ask user to select a disk for installation
@@ -203,6 +283,8 @@ def ask_single_disk(disks_available: dict[str, int]) -> str:
print(MSG_INFO_INSTALL_EXIT)
exit()
+ search_previous_installation(list(disks_available))
+
disk_details: disk.DiskDetails = create_partitions(disk_selected,
disks_available[disk_selected])
@@ -259,6 +341,8 @@ def check_raid_install(disks_available: dict[str, int]) -> Union[str, None]:
print(MSG_INFO_INSTALL_EXIT)
exit()
+ search_previous_installation(list(disks_available))
+
disks: list[disk.DiskDetails] = []
for disk_selected in list(disks_selected):
print(f'Creating partitions on {disk_selected}')
@@ -376,7 +460,7 @@ def validate_signature(file_path: str, sign_type: str) -> None:
print('Signature is valid')
-def image_fetch(image_path: str) -> Path:
+def image_fetch(image_path: str, no_prompt: bool = False) -> Path:
"""Fetch an ISO image
Args:
@@ -389,13 +473,14 @@ def image_fetch(image_path: str) -> Path:
# check a type of path
if urlparse(image_path).scheme:
# download an image
- download(ISO_DOWNLOAD_PATH, image_path, True, True)
+ download(ISO_DOWNLOAD_PATH, image_path, True, True,
+ raise_error=True)
# download a signature
sign_file = (False, '')
for sign_type in ['minisig', 'asc']:
try:
download(f'{ISO_DOWNLOAD_PATH}.{sign_type}',
- f'{image_path}.{sign_type}')
+ f'{image_path}.{sign_type}', raise_error=True)
sign_file = (True, sign_type)
break
except Exception:
@@ -404,7 +489,8 @@ def image_fetch(image_path: str) -> Path:
if sign_file[0]:
validate_signature(ISO_DOWNLOAD_PATH, sign_file[1])
else:
- if not ask_yes_no(MSG_WARN_ISO_SIGN_UNAVAL, default=False):
+ if (not no_prompt and
+ not ask_yes_no(MSG_WARN_ISO_SIGN_UNAVAL, default=False)):
cleanup()
exit(MSG_INFO_INSTALL_EXIT)
@@ -433,6 +519,17 @@ def migrate_config() -> bool:
return False
+def copy_ssh_host_keys() -> bool:
+ """Ask user to copy SSH host keys
+
+ Returns:
+ bool: user's decision
+ """
+ if ask_yes_no('Would you like to copy SSH host keys?', default=True):
+ return True
+ return False
+
+
def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:
"""Clean up after installation
@@ -567,6 +664,10 @@ def install_image() -> None:
copy(FILE_ROOTFS_SRC,
f'{DIR_DST_ROOT}/boot/{image_name}/{image_name}.squashfs')
+ # copy saved config data and SSH keys
+ # owner restored on copy of config data by chmod_2775, above
+ copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw')
+
if is_raid_install(install_target):
write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw'
raid.update_default(write_dir)
@@ -629,7 +730,7 @@ def install_image() -> None:
@compat.grub_cfg_update
-def add_image(image_path: str) -> None:
+def add_image(image_path: str, no_prompt: bool = False) -> None:
"""Add a new image
Args:
@@ -639,7 +740,7 @@ def add_image(image_path: str) -> None:
exit(MSG_ERR_LIVE)
# fetch an image
- iso_path: Path = image_fetch(image_path)
+ iso_path: Path = image_fetch(image_path, no_prompt)
try:
# mount an ISO
Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True)
@@ -668,8 +769,12 @@ def add_image(image_path: str) -> None:
raise compat.DowngradingImageTools(
f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed')
- image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
- set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True)
+ if not no_prompt:
+ image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
+ set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True)
+ else:
+ image_name: str = version_name
+ set_as_default: bool = True
# find target directory
root_dir: str = disk.find_persistence()
@@ -678,7 +783,7 @@ def add_image(image_path: str) -> None:
# create all the rest in a single step
target_config_dir: str = f'{root_dir}/boot/{image_name}/rw/opt/vyatta/etc/config/'
# copy config
- if migrate_config():
+ if no_prompt or migrate_config():
print('Copying configuration directory')
# copytree preserves perms but not ownership:
Path(target_config_dir).mkdir(parents=True)
@@ -692,6 +797,14 @@ def add_image(image_path: str) -> None:
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/'
+ if no_prompt or copy_ssh_host_keys():
+ print('Copying SSH host keys')
+ 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)
+
# copy system image and kernel files
print('Copying system image files')
for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir():
@@ -727,8 +840,10 @@ def parse_arguments() -> Namespace:
choices=['install', 'add'],
required=True,
help='action to perform with an image')
+ parser.add_argument('--no-prompt', action='store_true',
+ help='perform action non-interactively')
parser.add_argument(
- '--image_path',
+ '--image-path',
help='a path (HTTP or local file) to an image that needs to be installed'
)
# parser.add_argument('--image_new_name', help='a new name for image')
@@ -746,7 +861,7 @@ if __name__ == '__main__':
if args.action == 'install':
install_image()
if args.action == 'add':
- add_image(args.image_path)
+ add_image(args.image_path, args.no_prompt)
exit()
diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py
index e4b2f4833..e75485f9f 100755
--- a/src/op_mode/image_manager.py
+++ b/src/op_mode/image_manager.py
@@ -36,7 +36,7 @@ MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another im
@compat.grub_cfg_update
def delete_image(image_name: Optional[str] = None,
- prompt: bool = True) -> None:
+ no_prompt: bool = False) -> None:
"""Remove installed image files and boot entry
Args:
@@ -44,7 +44,7 @@ def delete_image(image_name: Optional[str] = None,
"""
available_images: list[str] = grub.version_list()
if image_name is None:
- if not prompt:
+ if no_prompt:
exit('An image name is required for delete action')
else:
image_name = select_entry(available_images,
@@ -60,8 +60,9 @@ def delete_image(image_name: Optional[str] = None,
if not persistence_storage:
exit('Persistence storage cannot be found')
- if not ask_yes_no(f'Do you really want to delete the image {image_name}?',
- default=False):
+ if (not no_prompt and
+ not ask_yes_no(f'Do you really want to delete the image {image_name}?',
+ default=False)):
exit()
# remove files and menu entry
@@ -171,11 +172,13 @@ def parse_arguments() -> Namespace:
choices=['delete', 'set', 'rename', 'list'],
required=True,
help='action to perform with an image')
+ parser.add_argument('--no-prompt', action='store_true',
+ help='perform action non-interactively')
parser.add_argument(
- '--image_name',
+ '--image-name',
help=
'a name of an image to add, delete, install, rename, or set as default')
- parser.add_argument('--image_new_name', help='a new name for image')
+ parser.add_argument('--image-new-name', help='a new name for image')
args: Namespace = parser.parse_args()
# Validate arguments
if args.action == 'rename' and (not args.image_name or
@@ -189,7 +192,7 @@ if __name__ == '__main__':
try:
args: Namespace = parse_arguments()
if args.action == 'delete':
- delete_image(args.image_name)
+ delete_image(args.image_name, args.no_prompt)
if args.action == 'set':
set_image(args.image_name)
if args.action == 'rename':