From 7ac06d75d0785da07d270dc38a7581d74c285cc5 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Oct 2021 20:43:01 +0200 Subject: Restructured disk.py into lib/disk/.py instead. Shouldn't be any broken links as we expose all the functions through __init__.py - but you never know so I'll keep an eye for issues with this. --- archinstall/lib/disk/filesystem.py | 188 +++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 archinstall/lib/disk/filesystem.py (limited to 'archinstall/lib/disk/filesystem.py') diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py new file mode 100644 index 00000000..b53d8451 --- /dev/null +++ b/archinstall/lib/disk/filesystem.py @@ -0,0 +1,188 @@ +import time +from .partition import Partition +from .blockdevice import BlockDevice +from ..output import log + +GPT = 0b00000001 +MBR = 0b00000010 + +class Filesystem: + # TODO: + # When instance of a HDD is selected, check all usages and gracefully unmount them + # as well as close any crypto handles. + def __init__(self, blockdevice, mode): + self.blockdevice = blockdevice + self.mode = mode + + def __enter__(self, *args, **kwargs): + if self.blockdevice.keep_partitions is False: + log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=logging.DEBUG) + if self.mode == GPT: + if self.parted_mklabel(self.blockdevice.device, "gpt"): + self.blockdevice.flush_cache() + return self + else: + raise DiskError('Problem setting the disk label type to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') + elif self.mode == MBR: + if self.parted_mklabel(self.blockdevice.device, "msdos"): + return self + else: + raise DiskError('Problem setting the disk label type to msdos:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos') + else: + raise DiskError(f'Unknown mode selected to format in: {self.mode}') + + # TODO: partition_table_type is hardcoded to GPT at the moment. This has to be changed. + elif self.mode == self.blockdevice.partition_table_type: + log(f'Kept partition format {self.mode} for {self.blockdevice}', level=logging.DEBUG) + else: + raise DiskError(f'The selected partition table format {self.mode} does not match that of {self.blockdevice}.') + + return self + + def __repr__(self): + return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})" + + def __exit__(self, *args, **kwargs): + # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager + if len(args) >= 2 and args[1]: + raise args[1] + SysCommand('sync') + return True + + def partuuid_to_index(self, uuid): + output = json.loads(SysCommand(f"lsblk --json -o+PARTUUID {self.blockdevice.device}").decode('UTF-8')) + + for device in output['blockdevices']: + for index, partition in enumerate(device['children']): + if partition['partuuid'].lower() == uuid: + return index + + def load_layout(self, layout :dict): + from .luks import luks2 + + # If the layout tells us to wipe the drive, we do so + if layout.get('wipe', False): + if self.mode == GPT: + if not self.parted_mklabel(self.blockdevice.device, "gpt"): + raise KeyError(f"Could not create a GPT label on {self}") + elif self.mode == MBR: + if not self.parted_mklabel(self.blockdevice.device, "msdos"): + raise KeyError(f"Could not create a MSDOS label on {self}") + + # We then iterate the partitions in order + for partition in layout.get('partitions', []): + # We don't want to re-add an existing partition (those containing a UUID already) + if partition.get('format', False) and not partition.get('PARTUUID', None): + print("Adding partition....") + partition['device_instance'] = self.add_partition(partition.get('type', 'primary'), + start=partition.get('start', '1MiB'), # TODO: Revisit sane block starts (4MB for memorycards for instance) + end=partition.get('size', '100%'), + partition_format=partition.get('filesystem', {}).get('format', 'btrfs')) + # TODO: device_instance some times become None + # print('Device instance:', partition['device_instance']) + + elif (partition_uuid := partition.get('PARTUUID')) and (partition_instance := self.blockdevice.get_partition(uuid=partition_uuid)): + print("Re-using partition_instance:", partition_instance) + partition['device_instance'] = partition_instance + else: + raise ValueError(f"{self}.load_layout() doesn't know how to continue without a new partition definition or a UUID ({partition.get('PARTUUID')}) on the device ({self.blockdevice.get_partition(uuid=partition_uuid)}).") + + if partition.get('filesystem', {}).get('format', False): + if partition.get('encrypted', False): + if not partition.get('password'): + if storage['arguments'] == 'silent': + raise ValueError(f"Missing encryption password for {partition['device_instance']}") + else: + from .user_interaction import get_password + partition['password'] = get_password(f"Enter a encryption password for {partition['device_instance']}") + + partition['device_instance'].encrypt(password=partition['password']) + with luks2(partition['device_instance'], storage.get('ENC_IDENTIFIER', 'ai')+'loop', partition['password']) as unlocked_device: + if not partition.get('format'): + if storage['arguments'] == 'silent': + raise ValueError(f"Missing fs-type to format on newly created encrypted partition {partition['device_instance']}") + else: + if not partition.get('filesystem'): + partition['filesystem'] = {} + + if not partition['filesystem'].get('format', False): + while True: + partition['filesystem']['format'] = input(f"Enter a valid fs-type for newly encrypted partition {partition['filesystem']['format']}: ").strip() + if not partition['filesystem']['format'] or valid_fs_type(partition['filesystem']['format']) is False: + pint("You need to enter a valid fs-type in order to continue. See `man parted` for valid fs-type's.") + continue + break + + unlocked_device.format(partition['filesystem']['format']) + elif partition.get('format', False): + partition['device_instance'].format(partition['filesystem']['format']) + + if partition.get('boot', False): + self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on') + + def find_partition(self, mountpoint): + for partition in self.blockdevice: + if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint: + return partition + + def raw_parted(self, string: str): + if (cmd_handle := SysCommand(f'/usr/bin/parted -s {string}')).exit_code != 0: + log(f"Parted ended with a bad exit code: {cmd_handle}", level=logging.ERROR, fg="red") + return cmd_handle + + def parted(self, string: str): + """ + Performs a parted execution of the given string + + :param string: A raw string passed to /usr/bin/parted -s + :type string: str + """ + return self.raw_parted(string).exit_code == 0 + + def use_entire_disk(self, root_filesystem_type='ext4') -> Partition: + # TODO: Implement this with declarative profiles instead. + raise ValueError("Installation().use_entire_disk() has to be re-worked.") + + def add_partition(self, partition_type, start, end, partition_format=None): + log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO) + + previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()} + + if self.mode == MBR: + if len(self.blockdevice.partitions) > 3: + DiskError("Too many partitions on disk, MBR disks can only have 3 parimary partitions") + + if partition_format: + parted_string = f'{self.blockdevice.device} mkpart {partition_type} {partition_format} {start} {end}' + else: + parted_string = f'{self.blockdevice.device} mkpart {partition_type} {start} {end}' + + if self.parted(parted_string): + start_wait = time.time() + + while previous_partition_uuids == {partition.uuid for partition in self.blockdevice.partitions.values()}: + if time.time() - start_wait > 10: + raise DiskError(f"New partition never showed up after adding new partition on {self} (timeout 10 seconds).") + time.sleep(0.025) + + + # Todo: Find a better way to detect if the new UUID of the partition has showed up. + # But this will address (among other issues) + time.sleep(float(storage['arguments'].get('disk-sleep', 2.0))) # Let the kernel catch up with quick block devices (nvme for instance) + return self.blockdevice.get_partition(uuid=(previous_partition_uuids ^ {partition.uuid for partition in self.blockdevice.partitions.values()}).pop()) + + def set_name(self, partition: int, name: str): + return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0 + + def set(self, partition: int, string: str): + log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO) + return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0 + + def parted_mklabel(self, device: str, disk_label: str): + log(f"Creating a new partition labling on {device}", level=logging.INFO, fg="yellow") + # Try to unmount devices before attempting to run mklabel + try: + SysCommand(f'bash -c "umount {device}?"') + except: + pass + return self.raw_parted(f'{device} mklabel {disk_label}').exit_code == 0 \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 7149b76f3bd3163938fe7413546e5f678f98851f Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Oct 2021 21:54:16 +0200 Subject: Forgot some imports that didn't show up on a static run without going through a few of the menu's --- archinstall/lib/disk/__init__.py | 2 +- archinstall/lib/disk/blockdevice.py | 3 +++ archinstall/lib/disk/filesystem.py | 6 +++++- archinstall/lib/disk/helpers.py | 2 ++ archinstall/lib/disk/partition.py | 4 ++++ archinstall/lib/disk/user_guides.py | 4 ++-- examples/guided.py | 6 +++--- 7 files changed, 20 insertions(+), 7 deletions(-) (limited to 'archinstall/lib/disk/filesystem.py') diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py index 8237f774..352d04b9 100644 --- a/archinstall/lib/disk/__init__.py +++ b/archinstall/lib/disk/__init__.py @@ -1,7 +1,7 @@ from .btrfs import * from .helpers import * from .blockdevice import BlockDevice -from .filesystem import Filesystem +from .filesystem import Filesystem, MBR, GPT from .partition import * from .user_guides import * from .validators import * \ No newline at end of file diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py index daa65323..57cbcfa6 100644 --- a/archinstall/lib/disk/blockdevice.py +++ b/archinstall/lib/disk/blockdevice.py @@ -1,3 +1,4 @@ +import os import json import logging from ..output import log @@ -94,6 +95,7 @@ class BlockDevice: @property def partitions(self): + from .filesystem import Partition SysCommand(['partprobe', self.path]) result = SysCommand(['/usr/bin/lsblk', '-J', self.path]) @@ -123,6 +125,7 @@ class BlockDevice: @property def partition_table_type(self): + from .filesystem import GPT return GPT @property diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index b53d8451..28846764 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -1,7 +1,11 @@ import time +import logging +import json from .partition import Partition from .blockdevice import BlockDevice +from ..general import SysCommand from ..output import log +from ..storage import storage GPT = 0b00000001 MBR = 0b00000010 @@ -58,7 +62,7 @@ class Filesystem: return index def load_layout(self, layout :dict): - from .luks import luks2 + from ..luks import luks2 # If the layout tells us to wipe the drive, we do so if layout.get('wipe', False): diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 8b372f73..65abdea2 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -132,6 +132,8 @@ def get_mount_info(path) -> dict: def get_partitions_in_use(mountpoint) -> list: + from .partition import Partition + try: output = SysCommand(f"/usr/bin/findmnt --json -R {mountpoint}").decode('UTF-8') except SysCallError: diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 30151583..3bb2982b 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -2,9 +2,13 @@ import glob import pathlib import time import logging +import json +import os from typing import Optional from .blockdevice import BlockDevice +from .helpers import get_mount_info, get_filesystem_type from ..output import log +from ..general import SysCommand class Partition: def __init__(self, path: str, block_device: BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True): diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py index 0a975149..79b9d48f 100644 --- a/archinstall/lib/disk/user_guides.py +++ b/archinstall/lib/disk/user_guides.py @@ -3,7 +3,7 @@ from ..output import log def suggest_single_disk_layout(block_device, default_filesystem=None): if not default_filesystem: - from .user_interaction import ask_for_main_filesystem_format + from ..user_interaction import ask_for_main_filesystem_format default_filesystem = ask_for_main_filesystem_format() MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb @@ -76,7 +76,7 @@ def suggest_single_disk_layout(block_device, default_filesystem=None): def suggest_multi_disk_layout(block_devices, default_filesystem=None): if not default_filesystem: - from .user_interaction import ask_for_main_filesystem_format + from ..user_interaction import ask_for_main_filesystem_format default_filesystem = ask_for_main_filesystem_format() # Not really a rock solid foundation of information to stand on, but it's a start: diff --git a/examples/guided.py b/examples/guided.py index b7c75b30..2efb4972 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -140,7 +140,7 @@ def ask_user_questions(): # Ask for a root password (optional, but triggers requirement for super-user if skipped) if not archinstall.arguments.get('!root-password', None): - archinstall.arguments['!root-password'] = archinstall.get_password(prompt='Enter root password (Recommendation: leave blank to leave root disabled): ') + archinstall.arguments['!root-password'] = archinstall.get_password(prompt='Enter root password (leave blank to disable disabled & create superuser): ') # Ask for additional users (super-user if root pw was not set) @@ -245,9 +245,9 @@ def perform_filesystem_operations(): Setup the blockdevice, filesystem (and optionally encryption). Once that's done, we'll hand over to perform_installation() """ - mode = archinstall.GPT + mode = archinstall.disk.GPT if has_uefi() is False: - mode = archinstall.MBR + mode = archinstall.disk.MBR for drive in archinstall.arguments['harddrives']: with archinstall.Filesystem(drive, mode) as fs: -- cgit v1.2.3-70-g09d2 From 76dc426a0fb69fa2d8cbc5c76934dc736d2839a6 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 11:00:22 +0200 Subject: Added creating and mounting of subvolume structure for BTRFS. --- archinstall/lib/disk/btrfs.py | 4 +++- archinstall/lib/disk/filesystem.py | 1 + archinstall/lib/disk/user_guides.py | 25 ++++++++++++++----------- archinstall/lib/installer.py | 7 +++++++ 4 files changed, 25 insertions(+), 12 deletions(-) (limited to 'archinstall/lib/disk/filesystem.py') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index 558a249e..e9ffec66 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -23,7 +23,9 @@ def mount_subvolume(installation, location :Union[pathlib.Path, str], force=Fals raise DiskError(f"Cannot mount subvolume to {installation.target/location} because it contains data (non-empty folder target)") # Mount the logical volume to the physical structure - return SysCommand(f"mount {get_mount_info(installation.target/location)['source']} {installation.target}/{str(location)} -o subvol=@/{str(location)}").exit_code == 0 + mount_location = get_mount_info(installation.target/location)['source'] + SysCommand(f"umount {mount_location}") + return SysCommand(f"mount {mount_location} {installation.target}/{str(location)} -o subvol=@/{str(location)}").exit_code == 0 def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: """ diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 28846764..0328cd83 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -132,6 +132,7 @@ class Filesystem: def raw_parted(self, string: str): if (cmd_handle := SysCommand(f'/usr/bin/parted -s {string}')).exit_code != 0: log(f"Parted ended with a bad exit code: {cmd_handle}", level=logging.ERROR, fg="red") + time.sleep(0.5) return cmd_handle def parted(self, string: str): diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py index 79b9d48f..6f8a1edb 100644 --- a/archinstall/lib/disk/user_guides.py +++ b/archinstall/lib/disk/user_guides.py @@ -41,18 +41,21 @@ def suggest_single_disk_layout(block_device, default_filesystem=None): } }) - if default_filesystem == 'btrfs' and input('Would you like to use BTRFS subvolumes? (Y/n)').strip().lower() in ('', 'y', 'yes'): - # https://btrfs.wiki.kernel.org/index.php/FAQ - # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash - # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh - layout[block_device.path]['partitions'][1]['btrfs'] = { - "subvolumes" : { - '@home' : '/home', - '@log' : '/var/log', - '@pkgs' : '/var/cache/pacman/pkg', - '@.snapshots' : '/.snapshots' + if default_filesystem == 'btrfs' and input('Would you like to use BTRFS subvolumes? (Y/n): ').strip().lower() in ('', 'y', 'yes'): + if input('Do you want to use a recommended structure? (Y/n): ').strip().lower() in ('', 'y', 'yes'): + # https://btrfs.wiki.kernel.org/index.php/FAQ + # https://unix.stackexchange.com/questions/246976/btrfs-subvolume-uuid-clash + # https://github.com/classy-giraffe/easy-arch/blob/main/easy-arch.sh + layout[block_device.path]['partitions'][1]['btrfs'] = { + "subvolumes" : { + '@home' : '/home', + '@log' : '/var/log', + '@pkgs' : '/var/cache/pacman/pkg', + '@.snapshots' : '/.snapshots' + } } - } + else: + pass #... implement a guided setup elif block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: # If we don't want to use subvolumes, diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 2aac8510..41d918a1 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -5,6 +5,7 @@ from .mirrors import * from .plugins import plugins from .storage import storage from .user_interaction import * +from .disk.btrfs import create_subvolume, mount_subvolume # Any package that the Installer() is responsible for (optional and the default ones) __packages__ = ["base", "base-devel", "linux-firmware", "linux", "linux-lts", "linux-zen", "linux-hardened"] @@ -139,9 +140,15 @@ class Installer: password = mountpoints[mountpoint]['password'] with luks2(mountpoints[mountpoint]['device_instance'], loopdev, password, auto_unmount=False) as unlocked_device: unlocked_device.mount(f"{self.target}{mountpoint}") + else: mountpoints[mountpoint]['device_instance'].mount(f"{self.target}{mountpoint}") + if (subvolumes := mountpoints[mountpoint].get('btrfs', {}).get('subvolumes', {})): + for name, location in subvolumes.items(): + create_subvolume(self, location) + mount_subvolume(self, location) + def mount(self, partition, mountpoint, create_mountpoint=True): if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'): os.makedirs(f'{self.target}{mountpoint}') -- cgit v1.2.3-70-g09d2 From 32aa91bdded15b4bd97fd6a0c0af918fbf3affc2 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 21:04:31 +0200 Subject: Adding support for passing arguments to .format() This should enable people to use custom option arguments in their config files when scripting installations or using the API. --- archinstall/lib/disk/filesystem.py | 4 ++-- archinstall/lib/disk/partition.py | 26 +++++++++++++++++++------- 2 files changed, 21 insertions(+), 9 deletions(-) (limited to 'archinstall/lib/disk/filesystem.py') diff --git a/archinstall/lib/disk/filesystem.py b/archinstall/lib/disk/filesystem.py index 0328cd83..d8cc85f9 100644 --- a/archinstall/lib/disk/filesystem.py +++ b/archinstall/lib/disk/filesystem.py @@ -117,9 +117,9 @@ class Filesystem: continue break - unlocked_device.format(partition['filesystem']['format']) + unlocked_device.format(partition['filesystem']['format'], options=partition.get('options', [])) elif partition.get('format', False): - partition['device_instance'].format(partition['filesystem']['format']) + partition['device_instance'].format(partition['filesystem']['format'], options=partition.get('options', [])) if partition.get('boot', False): self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on') diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 3bb2982b..40a83d6b 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -208,7 +208,7 @@ class Partition: handle = luks2(self, None, None) return handle.encrypt(self, *args, **kwargs) - def format(self, filesystem=None, path=None, log_formatting=True): + def format(self, filesystem=None, path=None, log_formatting=True, options=[]): """ Format can be given an overriding path, for instance /dev/null to test the formatting functionality and in essence the support for the given filesystem. @@ -228,33 +228,45 @@ class Partition: log(f'Formatting {path} -> {filesystem}', level=logging.INFO) if filesystem == 'btrfs': - if 'UUID:' not in (mkfs := SysCommand(f'/usr/bin/mkfs.btrfs -f {path}').decode('UTF-8')): + options = ['-f'] + options + + if 'UUID:' not in (mkfs := SysCommand(f"/usr/bin/mkfs.btrfs {' '.join(options)} {path}").decode('UTF-8')): raise DiskError(f'Could not format {path} with {filesystem} because: {mkfs}') self.filesystem = filesystem elif filesystem == 'fat32': - mkfs = SysCommand(f'/usr/bin/mkfs.vfat -F32 {path}').decode('UTF-8') + options = ['-F32'] + options + + mkfs = SysCommand(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}").decode('UTF-8') if ('mkfs.fat' not in mkfs and 'mkfs.vfat' not in mkfs) or 'command not found' in mkfs: raise DiskError(f"Could not format {path} with {filesystem} because: {mkfs}") self.filesystem = filesystem elif filesystem == 'ext4': - if (handle := SysCommand(f'/usr/bin/mkfs.ext4 -F {path}')).exit_code != 0: + options = ['-F'] + options + + if (handle := SysCommand(f"/usr/bin/mkfs.ext4 {' '.join(options)} {path}")).exit_code != 0: raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") self.filesystem = filesystem elif filesystem == 'ext2': - if (handle := SysCommand(f'/usr/bin/mkfs.ext2 -F {path}')).exit_code != 0: + options = ['-F'] + options + + if (handle := SysCommand(f"/usr/bin/mkfs.ext2 {' '.join(options)} {path}")).exit_code != 0: raise DiskError(f'Could not format {path} with {filesystem} because: {b"".join(handle)}') self.filesystem = 'ext2' elif filesystem == 'xfs': - if (handle := SysCommand(f'/usr/bin/mkfs.xfs -f {path}')).exit_code != 0: + options = ['-f'] + options + + if (handle := SysCommand(f"/usr/bin/mkfs.xfs {' '.join(options)} {path}")).exit_code != 0: raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") self.filesystem = filesystem elif filesystem == 'f2fs': - if (handle := SysCommand(f'/usr/bin/mkfs.f2fs -f {path}')).exit_code != 0: + options = ['-f'] + options + + if (handle := SysCommand(f"/usr/bin/mkfs.f2fs {' '.join(options)} {path}")).exit_code != 0: raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") self.filesystem = filesystem -- cgit v1.2.3-70-g09d2