diff options
| -rw-r--r-- | python/vyos/system/disk.py | 83 | ||||
| -rw-r--r-- | python/vyos/system/grub.py | 15 | ||||
| -rw-r--r-- | python/vyos/system/raid.py | 115 | ||||
| -rw-r--r-- | python/vyos/utils/io.py | 10 | ||||
| -rwxr-xr-x | src/op_mode/image_installer.py | 295 | 
5 files changed, 426 insertions, 92 deletions
| diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py index 882b4eb39..49e6b5c5e 100644 --- a/python/vyos/system/disk.py +++ b/python/vyos/system/disk.py @@ -15,12 +15,20 @@  from json import loads as json_loads  from os import sync +from dataclasses import dataclass  from psutil import disk_partitions  from vyos.utils.process import run, cmd +@dataclass +class DiskDetails: +    """Disk details""" +    name: str +    partition: dict[str, str] + +  def disk_cleanup(drive_path: str) -> None:      """Clean up disk partition table (MBR and GPT)      Zeroize primary and secondary headers - first and last 17408 bytes @@ -67,6 +75,62 @@ def parttable_create(drive_path: str, root_size: int) -> None:      sync()      run(f'partprobe {drive_path}') +    partitions: list[str] = partition_list(drive_path) + +    disk: DiskDetails = DiskDetails( +        name = drive_path, +        partition = { +            'efi': next(x for x in partitions if x.endswith('2')), +            'root': next(x for x in partitions if x.endswith('3')) +        } +    ) + +    return disk + + +def partition_list(drive_path: str) -> list[str]: +    """Get a list of partitions on a drive + +    Args: +        drive_path (str): path to a drive + +    Returns: +        list[str]: a list of partition paths +    """ +    lsblk: str = cmd(f'lsblk -Jp {drive_path}') +    drive_info: dict = json_loads(lsblk) +    device: list = drive_info.get('blockdevices') +    children: list[str] = device[0].get('children', []) if device else [] +    partitions: list[str] = [child.get('name') for child in children] +    return partitions + + +def partition_parent(partition_path: str) -> str: +    """Get a parent device for a partition + +    Args: +        partition (str): path to a partition + +    Returns: +        str: path to a parent device +    """ +    parent: str = cmd(f'lsblk -ndpo pkname {partition_path}') +    return parent + + +def from_partition(partition_path: str) -> DiskDetails: +    drive_path: str = partition_parent(partition_path) +    partitions: list[str] = partition_list(drive_path) + +    disk: DiskDetails = DiskDetails( +        name = drive_path, +        partition = { +            'efi': next(x for x in partitions if x.endswith('2')), +            'root': next(x for x in partitions if x.endswith('3')) +        } +    ) + +    return disk  def filesystem_create(partition: str, fstype: str) -> None:      """Create a filesystem on a partition @@ -138,25 +202,6 @@ def find_device(mountpoint: str) -> str:      return '' -def raid_create(raid_name: str, -                raid_members: list[str], -                raid_level: str = 'raid1') -> None: -    """Create a RAID array - -    Args: -        raid_name (str): a name of array (data, backup, test, etc.) -        raid_members (list[str]): a list of array members -        raid_level (str, optional): an array level. Defaults to 'raid1'. -    """ -    raid_devices_num: int = len(raid_members) -    raid_members_str: str = ' '.join(raid_members) -    command: str = f'mdadm --create /dev/md/{raid_name} --metadata=1.2 \ -        --raid-devices={raid_devices_num} --level={raid_level} \ -        {raid_members_str}' - -    run(command) - -  def disks_size() -> dict[str, int]:      """Get a dictionary with physical disks and their sizes diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index 9ac205c03..0ac16af9a 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -19,7 +19,7 @@ from typing import Union  from uuid import uuid5, NAMESPACE_URL, UUID  from vyos.template import render -from vyos.utils.process import run, cmd +from vyos.utils.process import cmd  from vyos.system import disk  # Define variables @@ -49,7 +49,7 @@ REGEX_GRUB_MODULES: str = r'^insmod (?P<module_name>.+)$'  REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_version>.+)/)?vmlinuz.*$' -def install(drive_path: str, boot_dir: str, efi_dir: str) -> None: +def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> None:      """Install GRUB for both BIOS and EFI modes (hybrid boot)      Args: @@ -62,11 +62,11 @@ def install(drive_path: str, boot_dir: str, efi_dir: str) -> None:              {drive_path} --force',          f'grub-install --no-floppy --recheck --target=x86_64-efi \              --force-extra-removable --boot-directory={boot_dir} \ -            --efi-directory={efi_dir} --bootloader-id="VyOS" \ +            --efi-directory={efi_dir} --bootloader-id="{id}" \              --no-uefi-secure-boot'      ]      for command in commands: -        run(command) +        cmd(command)  def gen_version_uuid(version_name: str) -> str: @@ -294,7 +294,7 @@ def set_default(version_name: str, root_dir: str = '') -> None:      vars_write(vars_file, vars_current) -def common_write(root_dir: str = '') -> None: +def common_write(root_dir: str = '', grub_common: dict[str, str] = {}) -> None:      """Write common GRUB configuration file (overwrite everything)      Args: @@ -304,7 +304,7 @@ def common_write(root_dir: str = '') -> None:      if not root_dir:          root_dir = disk.find_persistence()      common_config = f'{root_dir}/{CFG_VYOS_COMMON}' -    render(common_config, TMPL_GRUB_COMMON, {}) +    render(common_config, TMPL_GRUB_COMMON, grub_common)  def create_structure(root_dir: str = '') -> None: @@ -335,3 +335,6 @@ def set_console_type(console_type: str, root_dir: str = '') -> None:      vars_current: dict[str, str] = vars_read(vars_file)      vars_current['console_type'] = str(console_type)      vars_write(vars_file, vars_current) + +def set_raid(root_dir: str = '') -> None: +    pass diff --git a/python/vyos/system/raid.py b/python/vyos/system/raid.py new file mode 100644 index 000000000..13b99fa69 --- /dev/null +++ b/python/vyos/system/raid.py @@ -0,0 +1,115 @@ +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library.  If not, see <http://www.gnu.org/licenses/>. + +"""RAID related functions""" + +from pathlib import Path +from shutil import copy +from dataclasses import dataclass + +from vyos.utils.process import cmd +from vyos.system import disk + + +@dataclass +class RaidDetails: +    """RAID type""" +    name: str +    level: str +    members: list[str] +    disks: list[disk.DiskDetails] + + +def raid_create(raid_members: list[str], +                raid_name: str = 'md0', +                raid_level: str = 'raid1') -> None: +    """Create a RAID array + +    Args: +        raid_name (str): a name of array (data, backup, test, etc.) +        raid_members (list[str]): a list of array members +        raid_level (str, optional): an array level. Defaults to 'raid1'. +    """ +    raid_devices_num: int = len(raid_members) +    raid_members_str: str = ' '.join(raid_members) +    if Path('/sys/firmware/efi').exists(): +        for part in raid_members: +            drive: str = disk.partition_parent(part) +            command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}' +            cmd(command) +    else: +        for part in raid_members: +            drive: str = disk.partition_parent(part) +            command: str = f'sgdisk --typecode=3:A19D880F-05FC-4D3B-A006-743F0F84911E {drive}' +            cmd(command) +    for part in raid_members: +        command: str = f'mdadm --zero-superblock {part}' +        cmd(command) +    command: str = f'mdadm --create /dev/{raid_name} -R --metadata=1.0 \ +        --raid-devices={raid_devices_num} --level={raid_level} \ +        {raid_members_str}' + +    cmd(command) + +    raid = RaidDetails( +        name = f'/dev/{raid_name}', +        level = raid_level, +        members = raid_members, +        disks = [disk.from_partition(m) for m in raid_members] +    ) + +    return raid + +def update_initramfs() -> None: +    """Update initramfs""" +    mdadm_script = '/etc/initramfs-tools/scripts/local-top/mdadm' +    copy('/usr/share/initramfs-tools/scripts/local-block/mdadm', mdadm_script) +    p = Path(mdadm_script) +    p.write_text(p.read_text().replace('$((COUNT + 1))', '20')) +    command: str = 'update-initramfs -u' +    cmd(command) + +def update_default(target_dir: str) -> None: +    """Update /etc/default/mdadm to start MD monitoring daemon at boot +    """ +    source_mdadm_config = '/etc/default/mdadm' +    target_mdadm_config = Path(target_dir).joinpath('/etc/default/mdadm') +    target_mdadm_config_dir = Path(target_mdadm_config).parent +    Path.mkdir(target_mdadm_config_dir, parents=True, exist_ok=True) +    s = Path(source_mdadm_config).read_text().replace('START_DAEMON=false', +                                                      'START_DAEMON=true') +    Path(target_mdadm_config).write_text(s) + +def get_uuid(device: str) -> str: +    """Get UUID of a device""" +    command: str = f'tune2fs -l {device}' +    l = cmd(command).splitlines() +    uuid = next((x for x in l if x.startswith('Filesystem UUID')), '') +    return uuid.split(':')[1].strip() if uuid else '' + +def get_uuids(raid_details: RaidDetails) -> tuple[str]: +    """Get UUIDs of RAID members + +    Args: +        raid_name (str): a name of array (data, backup, test, etc.) + +    Returns: +        tuple[str]: root_disk uuid, root_md uuid +    """ +    raid_name: str = raid_details.name +    root_partition: str = raid_details.members[0] +    uuid_root_disk: str = get_uuid(root_partition) +    uuid_root_md: str = get_uuid(raid_name) +    return uuid_root_disk, uuid_root_md diff --git a/python/vyos/utils/io.py b/python/vyos/utils/io.py index e34a1ba32..74099b502 100644 --- a/python/vyos/utils/io.py +++ b/python/vyos/utils/io.py @@ -13,6 +13,8 @@  # You should have received a copy of the GNU Lesser General Public  # License along with this library.  If not, see <http://www.gnu.org/licenses/>. +from typing import Callable +  def print_error(str='', end='\n'):      """      Print `str` to stderr, terminated with `end`. @@ -73,7 +75,8 @@ def is_dumb_terminal():      import os      return os.getenv('TERM') in ['vt100', 'dumb'] -def select_entry(l: list, list_msg: str = '', prompt_msg: str = '') -> str: +def select_entry(l: list, list_msg: str = '', prompt_msg: str = '', +                 list_format: Callable = None,) -> str:      """Select an entry from a list      Args: @@ -87,7 +90,10 @@ def select_entry(l: list, list_msg: str = '', prompt_msg: str = '') -> str:      en = list(enumerate(l, 1))      print(list_msg)      for i, e in en: -        print(f'\t{i}: {e}') +        if list_format: +            print(f'\t{i}: {list_format(e)}') +        else: +            print(f'\t{i}: {e}')      select = ask_input(prompt_msg, numeric_only=True,                         valid_responses=range(1, len(l)+1))      return next(filter(lambda x: x[0] == select, en))[1] diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 88d5eeb48..aa4cf301b 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -21,18 +21,20 @@ from argparse import ArgumentParser, Namespace  from pathlib import Path  from shutil import copy, chown, rmtree, copytree  from sys import exit -from passlib.hosts import linux_context +from time import sleep +from typing import Union  from urllib.parse import urlparse +from passlib.hosts import linux_context  from psutil import disk_partitions  from vyos.configtree import ConfigTree  from vyos.remote import download -from vyos.system import disk, grub, image, compat, SYSTEM_CFG_VER +from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER  from vyos.template import render -from vyos.utils.io import ask_input, ask_yes_no +from vyos.utils.io import ask_input, ask_yes_no, select_entry  from vyos.utils.file import chmod_2775 -from vyos.utils.process import run +from vyos.utils.process import cmd, run  # define text messages  MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system image" instead.' @@ -44,11 +46,15 @@ MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation'  MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.'  MSG_INFO_INSTALL_DISKS_LIST: str = 'The following disks were found:'  MSG_INFO_INSTALL_DISK_SELECT: str = 'Which one should be used for installation?' +MSG_INFO_INSTALL_RAID_CONFIGURE: str = 'Would you like to configure RAID-1 mirroring?' +MSG_INFO_INSTALL_RAID_FOUND_DISKS: str = 'Would you like to configure RAID-1 mirroring on them?' +MSG_INFO_INSTALL_RAID_CHOOSE_DISKS: str = 'Would you like to choose two disks for RAID-1 mirroring?'  MSG_INFO_INSTALL_DISK_CONFIRM: str = 'Installation will delete all data on the drive. Continue?' +MSG_INFO_INSTALL_RAID_CONFIRM: str = 'Installation will delete all data on both drives. Continue?'  MSG_INFO_INSTALL_PARTITONING: str = 'Creating partition table...'  MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like to copy it to the new image?'  MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?' -MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set a new image as default one for boot?' +MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?'  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)?' @@ -107,13 +113,14 @@ def gb_to_bytes(size: float) -> int:      return int(size * 1024**3) -def find_disk() -> tuple[str, int]: +def find_disks() -> dict[str, int]:      """Find a target disk for installation      Returns: -        tuple[str, int]: disk name and size in bytes +        dict[str, int]: a list of available disks by name and size      """      # check for available disks +    print('Probing disks')      disks_available: dict[str, int] = disk.disks_size()      for disk_name, disk_size in disks_available.copy().items():          if disk_size < CONST_MIN_DISK_SIZE: @@ -122,17 +129,10 @@ def find_disk() -> tuple[str, int]:          print(MSG_ERR_NO_DISK)          exit(MSG_INFO_INSTALL_EXIT) -    # select one as a target -    print(MSG_INFO_INSTALL_DISKS_LIST) -    default_disk: str = list(disks_available)[0] -    for disk_name, disk_size in disks_available.items(): -        disk_size_human: str = bytes_to_gb(disk_size) -        print(f'Drive: {disk_name} ({disk_size_human} GB)') -    disk_selected: str = ask_input(MSG_INFO_INSTALL_DISK_SELECT, -                                   default=default_disk, -                                   valid_responses=list(disks_available)) +    num_disks: int = len(disks_available) +    print(f'{num_disks} disk(s) found') -    return disk_selected, disks_available[disk_selected] +    return disks_available  def ask_root_size(available_space: int) -> int: @@ -160,6 +160,126 @@ def ask_root_size(available_space: int) -> int:          return root_size_kbytes +def create_partitions(target_disk: str, target_size: int, +                      prompt: bool = True) -> None: +    """Create partitions on a target disk + +    Args: +        target_disk (str): a target disk +        target_size (int): size of disk in bytes +    """ +    # define target rootfs size in KB (smallest unit acceptable by sgdisk) +    available_size: int = (target_size - CONST_RESERVED_SPACE) // 1024 +    if prompt: +        rootfs_size: int = ask_root_size(available_size) +    else: +        rootfs_size: int = available_size + +    print(MSG_INFO_INSTALL_PARTITONING) +    disk.disk_cleanup(target_disk) +    disk_details: disk.DiskDetails = disk.parttable_create(target_disk, +                                                           rootfs_size) + +    return disk_details + + +def ask_single_disk(disks_available: dict[str, int]) -> str: +    """Ask user to select a disk for installation + +    Args: +        disks_available (dict[str, int]): a list of available disks +    """ +    print(MSG_INFO_INSTALL_DISKS_LIST) +    default_disk: str = list(disks_available)[0] +    for disk_name, disk_size in disks_available.items(): +        disk_size_human: str = bytes_to_gb(disk_size) +        print(f'Drive: {disk_name} ({disk_size_human} GB)') +    disk_selected: str = ask_input(MSG_INFO_INSTALL_DISK_SELECT, +                                   default=default_disk, +                                   valid_responses=list(disks_available)) + +    # create partitions +    if not ask_yes_no(MSG_INFO_INSTALL_DISK_CONFIRM): +        print(MSG_INFO_INSTALL_EXIT) +        exit() + +    disk_details: disk.DiskDetails = create_partitions(disk_selected, +                                                       disks_available[disk_selected]) + +    disk.filesystem_create(disk_details.partition['efi'], 'efi') +    disk.filesystem_create(disk_details.partition['root'], 'ext4') + +    return disk_details + + +def check_raid_install(disks_available: dict[str, int]) -> Union[str, None]: +    """Ask user to select disks for RAID installation + +    Args: +        disks_available (dict[str, int]): a list of available disks +    """ +    if len(disks_available) < 2: +        return None + +    if not ask_yes_no(MSG_INFO_INSTALL_RAID_CONFIGURE, default=True): +        return None + +    def format_selection(disk_name: str) -> str: +        return f'{disk_name}\t({bytes_to_gb(disks_available[disk_name])} GB)' + +    disk0, disk1 = list(disks_available)[0], list(disks_available)[1] +    disks_selected: dict[str, int] = { disk0: disks_available[disk0], +                                       disk1: disks_available[disk1] } + +    target_size: int = min(disks_selected[disk0], disks_selected[disk1]) + +    print(MSG_INFO_INSTALL_DISKS_LIST) +    for disk_name, disk_size in disks_selected.items(): +        disk_size_human: str = bytes_to_gb(disk_size) +        print(f'\t{disk_name} ({disk_size_human} GB)') +    if not ask_yes_no(MSG_INFO_INSTALL_RAID_FOUND_DISKS, default=True): +        if not ask_yes_no(MSG_INFO_INSTALL_RAID_CHOOSE_DISKS, default=True): +            return None +        else: +            disks_selected = {} +            disk0 = select_entry(list(disks_available), 'Disks available:', +                                 'Select first disk:', format_selection) + +            disks_selected[disk0] = disks_available[disk0] +            del disks_available[disk0] +            disk1 = select_entry(list(disks_available), 'Remaining disks:', +                                 'Select second disk:', format_selection) +            disks_selected[disk1] = disks_available[disk1] + +            target_size: int = min(disks_selected[disk0], +                                   disks_selected[disk1]) + +    # create partitions +    if not ask_yes_no(MSG_INFO_INSTALL_RAID_CONFIRM): +        print(MSG_INFO_INSTALL_EXIT) +        exit() + +    disks: list[disk.DiskDetails] = [] +    for disk_selected in list(disks_selected): +        print(f'Creating partitions on {disk_selected}') +        disk_details = create_partitions(disk_selected, target_size, +                                         prompt=False) +        disk.filesystem_create(disk_details.partition['efi'], 'efi') + +        disks.append(disk_details) + +    print('Creating RAID array') +    members = [disk.partition['root'] for disk in disks] +    raid_details: raid.RaidDetails = raid.raid_create(members) +    # raid init stuff +    print('Updating initramfs') +    raid.update_initramfs() +    # end init +    print('Creating filesystem on RAID array') +    disk.filesystem_create(raid_details.name, 'ext4') + +    return raid_details +  def prepare_tmp_disr() -> None:      """Create temporary directories for installation @@ -294,7 +414,7 @@ def image_fetch(image_path: str) -> Path:              if local_path.is_file():                  return local_path              else: -                raise +                raise FileNotFoundError      except Exception:          print(f'The image cannot be fetched from: {image_path}')          exit(1) @@ -351,6 +471,27 @@ def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None:                  if Path(remove_item).is_dir():                      rmtree(remove_item) +def cleanup_raid(details: raid.RaidDetails) -> None: +    efiparts = [] +    for raid_disk in details.disks: +        efiparts.append(raid_disk.partition['efi']) +    cleanup([details.name, *efiparts], +            ['/mnt/installation']) + + +def is_raid_install(install_object: Union[disk.DiskDetails, raid.RaidDetails]) -> bool: +    """Check if installation target is a RAID array + +    Args: +        install_object (Union[disk.DiskDetails, raid.RaidDetails]): a target disk + +    Returns: +        bool: True if it is a RAID array +    """ +    if isinstance(install_object, raid.RaidDetails): +        return True +    return False +  def install_image() -> None:      """Install an image to a disk @@ -363,50 +504,44 @@ def install_image() -> None:          print(MSG_INFO_INSTALL_EXIT)          exit() +    # configure image name +    running_image_name: str = image.get_running_image() +    while True: +        image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, +                                    running_image_name) +        if image.validate_name(image_name): +            break +        print(MSG_WARN_IMAGE_NAME_WRONG) + +    # ask for password +    user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos') + +    # ask for default console +    console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, +                                  default='K', +                                  valid_responses=['K', 'S', 'U']) +    console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'} + +    disks: dict[str, int] = find_disks() + +    install_target: Union[disk.DiskDetails, raid.RaidDetails, None] = None      try: -        # configure image name -        running_image_name: str = image.get_running_image() -        while True: -            image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, -                                        running_image_name) -            if image.validate_name(image_name): -                break -            print(MSG_WARN_IMAGE_NAME_WRONG) - -        # define target drive -        install_target, target_size = find_disk() - -        # define target rootfs size in KB (smallest unit acceptable by sgdisk) -        availabe_size: int = (target_size - CONST_RESERVED_SPACE) // 1024 -        rootfs_size: int = ask_root_size(availabe_size) - -        # ask for password -        user_password: str = ask_input(MSG_INPUT_PASSWORD, default='vyos') - -        # ask for default console -        console_type: str = ask_input(MSG_INPUT_CONSOLE_TYPE, -                                      default='K', -                                      valid_responses=['K', 'S', 'U']) -        console_dict: dict[str, str] = {'K': 'tty', 'S': 'ttyS', 'U': 'ttyUSB'} - -        # create partitions -        if not ask_yes_no(MSG_INFO_INSTALL_DISK_CONFIRM): -            print(MSG_INFO_INSTALL_EXIT) -            exit() -        print(MSG_INFO_INSTALL_PARTITONING) -        disk.disk_cleanup(install_target) -        disk.parttable_create(install_target, rootfs_size) -        disk.filesystem_create(f'{install_target}2', 'efi') -        disk.filesystem_create(f'{install_target}3', 'ext4') - -        # create directiroes for installation media +        install_target = check_raid_install(disks) +        if install_target is None: +            install_target = ask_single_disk(disks) + +        # create directories for installation media          prepare_tmp_disr()          # mount target filesystem and create required dirs inside          print('Mounting new partitions') -        disk.partition_mount(f'{install_target}3', DIR_DST_ROOT) -        Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True) -        disk.partition_mount(f'{install_target}2', f'{DIR_DST_ROOT}/boot/efi') +        if is_raid_install(install_target): +            disk.partition_mount(install_target.name, DIR_DST_ROOT) +            Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True) +        else: +            disk.partition_mount(install_target.partition['root'], DIR_DST_ROOT) +            Path(f'{DIR_DST_ROOT}/boot/efi').mkdir(parents=True) +            disk.partition_mount(install_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')          # a config dir. It is the deepest one, so the comand will          # create all the rest in a single step @@ -432,10 +567,10 @@ def install_image() -> None:          copy(FILE_ROOTFS_SRC,               f'{DIR_DST_ROOT}/boot/{image_name}/{image_name}.squashfs') -        # install GRUB -        print('Installing GRUB to the drive') -        grub.install(install_target, f'{DIR_DST_ROOT}/boot/', -                     f'{DIR_DST_ROOT}/boot/efi') +        if is_raid_install(install_target): +            write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw' +            raid.update_default(write_dir) +          setup_grub(DIR_DST_ROOT)          # add information about version          grub.create_structure() @@ -443,9 +578,34 @@ def install_image() -> None:          grub.set_default(image_name, DIR_DST_ROOT)          grub.set_console_type(console_dict[console_type], DIR_DST_ROOT) +        if is_raid_install(install_target): +            # add RAID specific modules +            grub.modules_write(f'{DIR_DST_ROOT}/{grub.CFG_VYOS_MODULES}', +                               ['part_msdos', 'part_gpt', 'diskfilter', +                                'ext2','mdraid1x']) +        # install GRUB +        if is_raid_install(install_target): +            print('Installing GRUB to the drives') +            l = install_target.disks +            for disk_target in l: +                disk.partition_mount(disk_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi') +                grub.install(disk_target.name, f'{DIR_DST_ROOT}/boot/', +                             f'{DIR_DST_ROOT}/boot/efi', +                             id=f'VyOS (RAID disk {l.index(disk_target) + 1})') +                disk.partition_umount(disk_target.partition['efi']) +        else: +            print('Installing GRUB to the drive') +            grub.install(install_target.name, f'{DIR_DST_ROOT}/boot/', +                         f'{DIR_DST_ROOT}/boot/efi') +          # umount filesystems and remove temporary files -        cleanup([f'{install_target}2', f'{install_target}3'], -                ['/mnt/installation']) +        if is_raid_install(install_target): +            cleanup([install_target.name], +                    ['/mnt/installation']) +        else: +            cleanup([install_target.partition['efi'], +                     install_target.partition['root']], +                    ['/mnt/installation'])          # we are done          print(MSG_INFO_INSTALL_SUCCESS) @@ -455,8 +615,13 @@ def install_image() -> None:          print(f'Unable to install VyOS: {err}')          # unmount filesystems and clenup          try: -            cleanup([f'{install_target}2', f'{install_target}3'], -                    ['/mnt/installation']) +            if install_target is not None: +                if is_raid_install(install_target): +                    cleanup_raid(install_target) +                else: +                    cleanup([install_target.partition['efi'], +                             install_target.partition['root']], +                            ['/mnt/installation'])          except Exception as err:              print(f'Cleanup failed: {err}') | 
