From 6a125d5bd2e5e0c27dc1feb3598e2e60a0526dd8 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 18 Sep 2021 11:22:18 +0200 Subject: Adding in options for BTRFS subvolumes --- archinstall/lib/disk.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 872d9bfc..79a8d3c8 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -110,6 +110,19 @@ def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None): return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) +def disk_layout_filesystem_checks(layout): + # This can probably be compressed into a any() + options = {} + for block_device in layout: + for partition in block_device.get('partitions', []): + if partition.get('filesystem', {}).get('format', False) == 'btrfs': + if not partition['filesystem'].get('subvolume', None): + if not options.get('btrfs-subvolumes', None) is None: + options['btrfs-subvolumes'] = input('Would you like to use BTRFS subvolumes? (Y/n)').strip().lower() in ('', 'y', 'yes') + + if options['btrfs-subvolumes']: + partition['filesystem']['subvolume'] = '@' # Detect /home etc, and set up sane defaults? + def suggest_single_disk_layout(block_device, default_filesystem=None): if not default_filesystem: from .user_interaction import ask_for_main_filesystem_format -- cgit v1.2.3-54-g00ecf From 68212dd32cf10e263456ae69cead762731667775 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 18 Sep 2021 14:54:42 +0200 Subject: Preparing to split up ./lib/disk.py And added some sane(?) defaults, which I'll massage into code some how. --- archinstall/lib/disk.py | 3 ++- archinstall/lib/disk2/btrfs.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 archinstall/lib/disk2/btrfs.py (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 79a8d3c8..be5b9edb 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -7,6 +7,7 @@ from typing import Optional from .general import * from .hardware import has_uefi from .output import log +from .disk2 import btrfs # TODO: rename disk2 to disk, once we migrate all the code here into a structure ROOT_DIR_PATTERN = re.compile('^.*?/devices') GPT = 0b00000001 @@ -121,7 +122,7 @@ def disk_layout_filesystem_checks(layout): options['btrfs-subvolumes'] = input('Would you like to use BTRFS subvolumes? (Y/n)').strip().lower() in ('', 'y', 'yes') if options['btrfs-subvolumes']: - partition['filesystem']['subvolume'] = '@' # Detect /home etc, and set up sane defaults? + btrfs.create_subvolume(partition) def suggest_single_disk_layout(block_device, default_filesystem=None): if not default_filesystem: diff --git a/archinstall/lib/disk2/btrfs.py b/archinstall/lib/disk2/btrfs.py new file mode 100644 index 00000000..549d23c1 --- /dev/null +++ b/archinstall/lib/disk2/btrfs.py @@ -0,0 +1,9 @@ +def create_subvolume(partition): + if partition['mountpoint'] == '/': + partition['filesystem']['subvolume'] = '@' + elif partition['mountpoint'] == '/home': + partition['filesystem']['subvolume'] = '@home' + + # @.snapshots /.snapshots + # @log /var/log + # @pkg /var/cache/pacman/pkg \ No newline at end of file -- cgit v1.2.3-54-g00ecf From ed823be3bae2151a73ba9817a32c226ac5a3c1c0 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 18 Sep 2021 15:52:29 +0200 Subject: Refactoring and cleaning up a bit I'm making sure that the JSON structure of the user config can get a say in how the subvolumes should be used later on. As well as splitting up where the logic should be to make it easier to maintain. --- archinstall/lib/disk.py | 35 +++++++++++++++++++++-------------- archinstall/lib/disk2/btrfs.py | 11 +++-------- examples/guided.py | 1 - 3 files changed, 24 insertions(+), 23 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index be5b9edb..740fd4ce 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -111,19 +111,6 @@ def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None): return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) -def disk_layout_filesystem_checks(layout): - # This can probably be compressed into a any() - options = {} - for block_device in layout: - for partition in block_device.get('partitions', []): - if partition.get('filesystem', {}).get('format', False) == 'btrfs': - if not partition['filesystem'].get('subvolume', None): - if not options.get('btrfs-subvolumes', None) is None: - options['btrfs-subvolumes'] = input('Would you like to use BTRFS subvolumes? (Y/n)').strip().lower() in ('', 'y', 'yes') - - if options['btrfs-subvolumes']: - btrfs.create_subvolume(partition) - def suggest_single_disk_layout(block_device, default_filesystem=None): if not default_filesystem: from .user_interaction import ask_for_main_filesystem_format @@ -164,7 +151,23 @@ def suggest_single_disk_layout(block_device, default_filesystem=None): } }) - if block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: + 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' + } + } + + elif block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it layout[block_device.path]['partitions'].append({ # Home "type" : "primary", @@ -186,6 +189,10 @@ def suggest_multi_disk_layout(block_devices, default_filesystem=None): 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: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb ARCH_LINUX_INSTALLED_SIZE = 20 # Gb, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? diff --git a/archinstall/lib/disk2/btrfs.py b/archinstall/lib/disk2/btrfs.py index 549d23c1..d6758b3f 100644 --- a/archinstall/lib/disk2/btrfs.py +++ b/archinstall/lib/disk2/btrfs.py @@ -1,9 +1,4 @@ -def create_subvolume(partition): - if partition['mountpoint'] == '/': - partition['filesystem']['subvolume'] = '@' - elif partition['mountpoint'] == '/home': - partition['filesystem']['subvolume'] = '@home' +from ..general import SysCommand - # @.snapshots /.snapshots - # @log /var/log - # @pkg /var/cache/pacman/pkg \ No newline at end of file +def create_subvolume(installation): + SysCommand(f"btrfs subvolume create {installation.target}/@") \ No newline at end of file diff --git a/examples/guided.py b/examples/guided.py index afe648e7..b7c75b30 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -116,7 +116,6 @@ def ask_user_questions(): if archinstall.arguments.get('harddrives', None) is not None and archinstall.storage.get('disk_layouts', None) is None: archinstall.storage['disk_layouts'] = archinstall.select_disk_layout(archinstall.arguments['harddrives']) - archinstall.disk_layout_filesystem_checks(archinstall.storage['disk_layouts']) # Get disk encryption password (or skip if blank) if archinstall.arguments['harddrives'] and archinstall.arguments.get('!encryption-password', None) is None: -- cgit v1.2.3-54-g00ecf From d2d80113b3c6a337097b407674f67b84cc14c82a Mon Sep 17 00:00:00 2001 From: Hugo Ankarloo Date: Mon, 20 Sep 2021 19:30:07 +0200 Subject: Fix Bug: Timezone is ignored, always same as host Bug affects normal interactive usage (example/guided.py). The timezone configured in the installer is not the timezone that ends up in the new installed system. Instead, the timezone used in the host system (from where the installer is run) is the one that finally ends up being used. Reason: systemd-nspawn by default copies the host timezone into the target. And systemd-nspawn is run when keyboard-layout is changed (which is done after changing the timezone). Solution: Add option `--timezone=off` to systemd-nspawn, which hinders affecting the timezone in the target. --- archinstall/lib/systemd.py | 1 + 1 file changed, 1 insertion(+) (limited to 'archinstall/lib') diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py index 383f1f17..d297c507 100644 --- a/archinstall/lib/systemd.py +++ b/archinstall/lib/systemd.py @@ -64,6 +64,7 @@ class Boot: self.session = SysCommandWorker([ '/usr/bin/systemd-nspawn', '-D', self.instance.target, + '--timezone=off', '-b', '--machine', self.container_name ]) -- cgit v1.2.3-54-g00ecf From ffbfafb35428168366de3ced572f648c6d49dc03 Mon Sep 17 00:00:00 2001 From: Hugo Ankarloo Date: Mon, 20 Sep 2021 19:58:56 +0200 Subject: Fix Bug: config b0rked by Suggest partition layout File: lib/user_interaction.py When function manage_new_and_existing_partitions() is used, and 'Suggest partition layout' is selected, the partition info is not correctly stored in the config. Instead of: {"partitions": [{...}, {...}]} You get: {"partitions": {"partitions": [{...}, {...}], "wipe":True}} --- archinstall/lib/user_interaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index b017e41a..66dd3350 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -674,7 +674,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: if input(f"{block_device} contains queued partitions, this will remove those, are you sure? y/N: ").strip().lower() in ('', 'n'): continue - block_device_struct["partitions"] = suggest_single_disk_layout(block_device)[block_device] + block_device_struct.update( suggest_single_disk_layout(block_device)[block_device] ) elif task is None: return block_device_struct else: -- cgit v1.2.3-54-g00ecf From 915ae88947a008a21006d2572e498ed0c134350c Mon Sep 17 00:00:00 2001 From: Hugo Ankarloo Date: Mon, 20 Sep 2021 20:17:39 +0200 Subject: Fix Bug: Set filesystem crashes if no partitions File: lib/user_interaction.py When function manage_new_and_existing_partitions() is used, and no partitions are configured, and 'Set desired filesystem for a partition' is selected, the installer crashes. --- archinstall/lib/user_interaction.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 66dd3350..ca8fb6f6 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -730,7 +730,10 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['boot'] = not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('boot', False) elif task == "Set desired filesystem for a partition": - if (partition := generic_select(block_device_struct["partitions"], 'Select which partition to set a filesystem on: ', options_output=False)): + if not block_device_struct["partitions"]: + log("No partitions found. Create some partitions first", level=logging.WARNING, fg='yellow') + continue + elif (partition := generic_select(block_device_struct["partitions"], 'Select which partition to set a filesystem on: ', options_output=False)): if not block_device_struct["partitions"][block_device_struct["partitions"].index(partition)].get('filesystem', None): block_device_struct["partitions"][block_device_struct["partitions"].index(partition)]['filesystem'] = {} -- cgit v1.2.3-54-g00ecf From 26244212cfe2d2ecbf7c791c811deb499e7a3bcf Mon Sep 17 00:00:00 2001 From: Hugo Ankarloo Date: Mon, 20 Sep 2021 21:46:56 +0200 Subject: Fix Bug: Cannot get partuuid from loop device File: lib/disk.py When installing on a loopback device (a.k.a loop device), function Filesystem.partuuid_to_index() crashes with a JSON parsing error. REASON 1) For loop devices, the property BlockDevice.device returns the actual image file (back-file) of the loop device instead of the /dev/X device. 2) Function Filesystem.partuuid_to_index() executes `lsblk --json` against BlockDevice.device . 3) `lsblk` fails and prints the error "not a block device" to stderr. This causes the output to not be valid JSON. 4) Code crashes when JSON parser tries to parse the output. SOLUTION - Make sure property BlockDevice.device only returns a valid block device. - Create new function BlockDevice.device_or_backfile that mimics the present behaviour of BlockDevice.device. - Use BlockDevice.device_or_backfile in function BlockDevice.__repr__(). SOLUTION REASONING I can only see one reason behind BlockDevice.device returning the back-file of a loop device, and that is to show the back-file to the user (instead of /dev/X) when printing the string representation of a BlockDevice. All other parts of the code can use the /dev/X file just fine. And IMO it makes more sense that a property named `device` only returns devices, and not normal files. --- archinstall/lib/disk.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index c86bf7bc..33f598bf 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -244,7 +244,7 @@ class BlockDevice: # I'm placing the encryption password on a BlockDevice level. def __repr__(self, *args, **kwargs): - return f"BlockDevice({self.device}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" + return f"BlockDevice({self.device_or_backfile}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" def __iter__(self): for partition in self.partitions: @@ -285,23 +285,33 @@ class BlockDevice: return device['pttype'] @property - def device(self): + def device_or_backfile(self): """ Returns the actual device-endpoint of the BlockDevice. If it's a loop-back-device it returns the back-file, - If it's a ATA-drive it returns the /dev/X device - And if it's a crypto-device it returns the parent device + For other types it return self.device """ - if "type" not in self.info: - raise DiskError(f'Could not locate backplane info for "{self.path}"') - if self.info['type'] == 'loop': for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: if not drive['name'] == self.path: continue return drive['back-file'] - elif self.info['type'] == 'disk': + else: + return self.device + + @property + def device(self): + """ + Returns the device file of the BlockDevice. + If it's a loop-back-device it returns the /dev/X device, + If it's a ATA-drive it returns the /dev/X device + And if it's a crypto-device it returns the parent device + """ + if "type" not in self.info: + raise DiskError(f'Could not locate backplane info for "{self.path}"') + + if self.info['type'] in ['disk','loop']: return self.path elif self.info['type'][:4] == 'raid': # This should catch /dev/md## raid devices -- cgit v1.2.3-54-g00ecf From 5bcbb50936690deff12a085634215c2d4f42f38c Mon Sep 17 00:00:00 2001 From: Hugo Ankarloo Date: Tue, 21 Sep 2021 00:38:20 +0200 Subject: Fix Bug: 'Suggest partition layout' crashes File: lib/user_interaction.py When function manage_new_and_existing_partitions() is used, and 'Suggest partition layout' is selected, the installer crashes. REASON Bug was introduced in commit 9e67ce3, when partition layout was changed to use device.path as keys (instead of device). It seems all necessary changes were made for this, except this one. --- archinstall/lib/user_interaction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index be74f9b9..ba6259b1 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -674,7 +674,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: if input(f"{block_device} contains queued partitions, this will remove those, are you sure? y/N: ").strip().lower() in ('', 'n'): continue - block_device_struct.update( suggest_single_disk_layout(block_device)[block_device] ) + block_device_struct.update( suggest_single_disk_layout(block_device)[block_device.path] ) elif task is None: return block_device_struct else: -- cgit v1.2.3-54-g00ecf From 427492d7c9a386dbeb08561f163b9a68870f6e71 Mon Sep 17 00:00:00 2001 From: Oleksandr Zinkevych Date: Fri, 15 Oct 2021 18:03:08 +0300 Subject: Fix re_rank_mirrors --- archinstall/lib/mirrors.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 1b62a61b..2325282f 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -123,10 +123,17 @@ def use_mirrors(regions: dict, destination='/etc/pacman.d/mirrorlist'): return True -def re_rank_mirrors(top=10, *positionals, **kwargs): - if SysCommand(f'/usr/bin/rankmirrors -n {top} /etc/pacman.d/mirrorlist > /etc/pacman.d/mirrorlist').exit_code == 0: - return True - return False +def re_rank_mirrors( + top: int = 10, + src: str = '/etc/pacman.d/mirrorlist', + dst: str = '/etc/pacman.d/mirrorlist', +) -> bool: + cmd = SysCommand(f"/usr/bin/rankmirrors -n {top} {src}") + if cmd.exit_code != 0: + return False + with open(dst, 'w') as f: + f.write(str(cmd)) + return True def list_mirrors(sort_order=["https", "http"]): -- cgit v1.2.3-54-g00ecf From 165d47f4bb3d79f405691a7cfb6ece55b33cd81c Mon Sep 17 00:00:00 2001 From: Oleksandr Zinkevych Date: Fri, 15 Oct 2021 18:21:35 +0300 Subject: Fix use_mirrors --- archinstall/lib/mirrors.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 1b62a61b..12fa7450 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,6 +1,6 @@ import urllib.error import urllib.request -from typing import Union +from typing import Union, Mapping, Iterable from .general import * from .output import log @@ -113,10 +113,13 @@ def insert_mirrors(mirrors, *args, **kwargs): return True -def use_mirrors(regions: dict, destination='/etc/pacman.d/mirrorlist'): +def use_mirrors( + regions: Mapping[str, Iterable[str]], + destination: str ='/etc/pacman.d/mirrorlist' +) -> bool: log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) - for region, mirrors in regions.items(): - with open(destination, 'w') as mirrorlist: + with open(destination, 'w') as mirrorlist: + for region, mirrors in regions.items(): for mirror in mirrors: mirrorlist.write(f'## {region}\n') mirrorlist.write(f'Server = {mirror}\n') -- cgit v1.2.3-54-g00ecf From ca25c356b687ffe9a2b63494de97607e8adc68ca Mon Sep 17 00:00:00 2001 From: Richard Neumann Date: Thu, 21 Oct 2021 22:48:52 +0200 Subject: Update type hints meminfo() returns ints. --- archinstall/lib/hardware.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index a8f87b80..e4308638 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -152,15 +152,15 @@ def product_name() -> Optional[str]: return product.read().strip() -def mem_available() -> Optional[str]: +def mem_available() -> Optional[int]: return meminfo('MemAvailable') -def mem_free() -> Optional[str]: +def mem_free() -> Optional[int]: return meminfo('MemFree') -def mem_total() -> Optional[str]: +def mem_total() -> Optional[int]: return meminfo('MemTotal') -- cgit v1.2.3-54-g00ecf From 4f6cec5069023198b047cb61e3e65fb37a93d577 Mon Sep 17 00:00:00 2001 From: Richard Neumann Date: Thu, 21 Oct 2021 22:50:49 +0200 Subject: Remove useless initialization of mem_info = {} --- archinstall/lib/hardware.py | 2 -- 1 file changed, 2 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index e4308638..4f8192e4 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -79,8 +79,6 @@ def meminfo(key: Optional[str] = None) -> Union[dict[str, int], int]: """Returns a dict with memory info if called with no args or the value of the given key of said dict. """ - mem_info = {} - with MEMINFO.open() as file: mem_info = { (columns := line.strip().split())[0].rstrip(':'): int(columns[1]) -- cgit v1.2.3-54-g00ecf From 63c6f39f98efcaa644d2d64c2e9468cb240a32dd Mon Sep 17 00:00:00 2001 From: Richard Neumann Date: Thu, 21 Oct 2021 22:54:00 +0200 Subject: Generalize CPU vendor detection Implement has_amd_cpu() and has_intel_cpu() as partials. --- archinstall/lib/hardware.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 4f8192e4..7172628b 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -1,4 +1,5 @@ import os +from functools import partial from pathlib import Path from typing import Iterator, Optional, Union @@ -95,11 +96,11 @@ def has_wifi() -> bool: return 'WIRELESS' in enrich_iface_types(list_interfaces().values()).values() -def has_amd_cpu() -> bool: - return any(cpu.get("vendor_id") == "AuthenticAMD" for cpu in cpuinfo()) +def has_cpu_vendor(vendor_id: str) -> bool: + return any(cpu.get("vendor_id") == vendor_id for cpu in cpuinfo()) -def has_intel_cpu() -> bool: - return any(cpu.get("vendor_id") == "GenuineIntel" for cpu in cpuinfo()) +has_amd_cpu = partial(has_cpu_vendor, "AuthenticAMD") +has_intel_cpu = partial(has_cpu_vendor, "GenuineIntel") def has_uefi() -> bool: return os.path.isdir('/sys/firmware/efi') -- cgit v1.2.3-54-g00ecf From 8eea3259245814e269e32ce69a08b81fcb2fca53 Mon Sep 17 00:00:00 2001 From: Richard Neumann Date: Thu, 21 Oct 2021 22:56:46 +0200 Subject: Improve type hint --- archinstall/lib/hardware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/hardware.py b/archinstall/lib/hardware.py index 7172628b..bbfb06a9 100644 --- a/archinstall/lib/hardware.py +++ b/archinstall/lib/hardware.py @@ -76,7 +76,7 @@ def cpuinfo() -> Iterator[dict[str, str]]: cpu[key.strip()] = value.strip() -def meminfo(key: Optional[str] = None) -> Union[dict[str, int], int]: +def meminfo(key: Optional[str] = None) -> Union[dict[str, int], Optional[int]]: """Returns a dict with memory info if called with no args or the value of the given key of said dict. """ -- cgit v1.2.3-54-g00ecf From a822b8edae2c80edc86e786e27d876b705b5c985 Mon Sep 17 00:00:00 2001 From: Oleksandr Zinkevych Date: Fri, 22 Oct 2021 14:53:18 +0300 Subject: use_mirrors: return None instead of True --- archinstall/lib/mirrors.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 12fa7450..739bf1a8 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -116,14 +116,13 @@ def insert_mirrors(mirrors, *args, **kwargs): def use_mirrors( regions: Mapping[str, Iterable[str]], destination: str ='/etc/pacman.d/mirrorlist' -) -> bool: +) -> None: log(f'A new package mirror-list has been created: {destination}', level=logging.INFO) with open(destination, 'w') as mirrorlist: for region, mirrors in regions.items(): for mirror in mirrors: mirrorlist.write(f'## {region}\n') mirrorlist.write(f'Server = {mirror}\n') - return True def re_rank_mirrors(top=10, *positionals, **kwargs): -- cgit v1.2.3-54-g00ecf 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.py | 1093 ----------------------------------- archinstall/lib/disk/__init__.py | 7 + archinstall/lib/disk/blockdevice.py | 207 +++++++ archinstall/lib/disk/btrfs.py | 4 + archinstall/lib/disk/filesystem.py | 188 ++++++ archinstall/lib/disk/helpers.py | 178 ++++++ archinstall/lib/disk/partition.py | 332 +++++++++++ archinstall/lib/disk/user_guides.py | 145 +++++ archinstall/lib/disk/validators.py | 42 ++ archinstall/lib/disk2/btrfs.py | 4 - 10 files changed, 1103 insertions(+), 1097 deletions(-) create mode 100644 archinstall/lib/disk/__init__.py create mode 100644 archinstall/lib/disk/blockdevice.py create mode 100644 archinstall/lib/disk/btrfs.py create mode 100644 archinstall/lib/disk/filesystem.py create mode 100644 archinstall/lib/disk/helpers.py create mode 100644 archinstall/lib/disk/partition.py create mode 100644 archinstall/lib/disk/user_guides.py create mode 100644 archinstall/lib/disk/validators.py delete mode 100644 archinstall/lib/disk2/btrfs.py (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 740fd4ce..e69de29b 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -1,1093 +0,0 @@ -import glob -import pathlib -import re -import time -from typing import Optional - -from .general import * -from .hardware import has_uefi -from .output import log -from .disk2 import btrfs # TODO: rename disk2 to disk, once we migrate all the code here into a structure - -ROOT_DIR_PATTERN = re.compile('^.*?/devices') -GPT = 0b00000001 -MBR = 0b00000010 - - -def valid_parted_position(pos :str): - if not len(pos): - return False - - if pos.isdigit(): - return True - - if pos[-1] == '%' and pos[:-1].isdigit(): - return True - - if pos[-3:].lower() in ['mib', 'kib', 'b', 'tib'] and pos[:-3].replace(".", "", 1).isdigit(): - return True - - if pos[-2:].lower() in ['kb', 'mb', 'gb', 'tb'] and pos[:-2].replace(".", "", 1).isdigit(): - return True - - return False - -def valid_fs_type(fstype :str) -> bool: - # https://www.gnu.org/software/parted/manual/html_node/mkpart.html - # Above link doesn't agree with `man parted` /mkpart documentation: - """ - fs-type can - be one of "btrfs", "ext2", - "ext3", "ext4", "fat16", - "fat32", "hfs", "hfs+", - "linux-swap", "ntfs", "reis‐ - erfs", "udf", or "xfs". - """ - - return fstype.lower() in [ - "btrfs", - "ext2", - "ext3", "ext4", # `man parted` allows these - "fat16", "fat32", - "hfs", "hfs+", # "hfsx", not included in `man parted` - "linux-swap", - "ntfs", - "reiserfs", - "udf", # "ufs", not included in `man parted` - "xfs", # `man parted` allows this - ] - - -def sort_block_devices_based_on_performance(block_devices): - result = {device: 0 for device in block_devices} - - for device, weight in result.items(): - if device.spinning: - weight -= 10 - else: - weight += 5 - - if device.bus_type == 'nvme': - weight += 20 - elif device.bus_type == 'sata': - weight += 10 - - result[device] = weight - - return result - -def filter_disks_below_size_in_gb(devices, gigabytes): - for disk in devices: - if disk.size >= gigabytes: - yield disk - -def select_largest_device(devices, gigabytes, filter_out=None): - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - copy_devices = list(filter_disks_below_size_in_gb(copy_devices, gigabytes)) - - if not len(copy_devices): - return None - - return max(copy_devices, key=(lambda device : device.size)) - -def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None): - if not filter_out: - filter_out = [] - - copy_devices = [*devices] - for filter_device in filter_out: - if filter_device in copy_devices: - copy_devices.pop(copy_devices.index(filter_device)) - - if not len(copy_devices): - return None - - return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) - -def suggest_single_disk_layout(block_device, default_filesystem=None): - if not default_filesystem: - from .user_interaction import ask_for_main_filesystem_format - default_filesystem = ask_for_main_filesystem_format() - - MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb - - layout = { - block_device.path : { - "wipe" : True, - "partitions" : [] - } - } - - layout[block_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "1MiB", - "size" : "513MiB", - "boot" : True, - "encrypted" : False, - "format" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - layout[block_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "513MiB", - "encrypted" : False, - "format" : True, - "size" : "100%" if block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART else f"{min(block_device.size, 20)*1024}MiB", - "mountpoint" : "/", - "filesystem" : { - "format" : default_filesystem - } - }) - - 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' - } - } - - elif block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: - # If we don't want to use subvolumes, - # But we want to be able to re-use data between re-installs.. - # A second partition for /home would be nice if we have the space for it - layout[block_device.path]['partitions'].append({ - # Home - "type" : "primary", - "encrypted" : False, - "format" : True, - "start" : f"{min(block_device.size*0.2, 20)*1024}MiB", - "size" : "100%", - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem - } - }) - - return layout - - -def suggest_multi_disk_layout(block_devices, default_filesystem=None): - if not default_filesystem: - 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: - # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ - # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ - - MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb - ARCH_LINUX_INSTALLED_SIZE = 20 # Gb, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? - - block_devices = sort_block_devices_based_on_performance(block_devices).keys() - - home_device = select_largest_device(block_devices, gigabytes=MIN_SIZE_TO_ALLOW_HOME_PART) - root_device = select_disk_larger_than_or_close_to(block_devices, gigabytes=ARCH_LINUX_INSTALLED_SIZE, filter_out=[home_device]) - - log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG) - - layout = { - root_device.path : { - "wipe" : True, - "partitions" : [] - }, - home_device.path : { - "wipe" : True, - "partitions" : [] - }, - } - - layout[root_device.path]['partitions'].append({ - # Boot - "type" : "primary", - "start" : "1MiB", - "size" : "513MiB", - "boot" : True, - "encrypted" : False, - "format" : True, - "mountpoint" : "/boot", - "filesystem" : { - "format" : "fat32" - } - }) - layout[root_device.path]['partitions'].append({ - # Root - "type" : "primary", - "start" : "513MiB", - "encrypted" : False, - "format" : True, - "size" : "100%", - "mountpoint" : "/", - "filesystem" : { - "format" : default_filesystem - } - }) - - layout[home_device.path]['partitions'].append({ - # Home - "type" : "primary", - "encrypted" : False, - "format" : True, - "start" : "4MiB", - "size" : "100%", - "mountpoint" : "/home", - "filesystem" : { - "format" : default_filesystem - } - }) - - return layout - - -class BlockDevice: - def __init__(self, path, info=None): - if not info: - # If we don't give any information, we need to auto-fill it. - # Otherwise any subsequent usage will break. - info = all_disks()[path].info - - self.path = path - self.info = info - self.keep_partitions = True - self.part_cache = {} - - # TODO: Currently disk encryption is a BIT misleading. - # It's actually partition-encryption, but for future-proofing this - # I'm placing the encryption password on a BlockDevice level. - - def __repr__(self, *args, **kwargs): - return f"BlockDevice({self.device}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" - - def __iter__(self): - for partition in self.partitions: - yield self.partitions[partition] - - def __getitem__(self, key, *args, **kwargs): - if key not in self.info: - raise KeyError(f'{self} does not contain information: "{key}"') - return self.info[key] - - def __len__(self): - return len(self.partitions) - - def __lt__(self, left_comparitor): - return self.path < left_comparitor.path - - def json(self): - """ - json() has precedence over __dump__, so this is a way - to give less/partial information for user readability. - """ - return self.path - - def __dump__(self): - return { - self.path : { - 'partuuid' : self.uuid, - 'wipe' : self.info.get('wipe', None), - 'partitions' : [part.__dump__() for part in self.partitions.values()] - } - } - - @property - def partition_type(self): - output = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8')) - - for device in output['blockdevices']: - return device['pttype'] - - @property - def device(self): - """ - Returns the actual device-endpoint of the BlockDevice. - If it's a loop-back-device it returns the back-file, - If it's a ATA-drive it returns the /dev/X device - And if it's a crypto-device it returns the parent device - """ - if "type" not in self.info: - raise DiskError(f'Could not locate backplane info for "{self.path}"') - - if self.info['type'] == 'loop': - for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: - if not drive['name'] == self.path: - continue - - return drive['back-file'] - elif self.info['type'] == 'disk': - return self.path - elif self.info['type'][:4] == 'raid': - # This should catch /dev/md## raid devices - return self.path - elif self.info['type'] == 'crypt': - if 'pkname' not in self.info: - raise DiskError(f'A crypt device ({self.path}) without a parent kernel device name.') - return f"/dev/{self.info['pkname']}" - else: - log(f"Unknown blockdevice type for {self.path}: {self.info['type']}", level=logging.DEBUG) - - # if not stat.S_ISBLK(os.stat(full_path).st_mode): - # raise DiskError(f'Selected disk "{full_path}" is not a block device.') - - @property - def partitions(self): - SysCommand(['partprobe', self.path]) - - result = SysCommand(['/usr/bin/lsblk', '-J', self.path]) - - if b'not a block device' in result: - raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}') - - if not result[:1] == b'{': - raise DiskError('Error getting JSON output from:', f'/usr/bin/lsblk -J {self.path}') - - r = json.loads(result.decode('UTF-8')) - if len(r['blockdevices']) and 'children' in r['blockdevices'][0]: - root_path = f"/dev/{r['blockdevices'][0]['name']}" - for part in r['blockdevices'][0]['children']: - part_id = part['name'][len(os.path.basename(self.path)):] - if part_id not in self.part_cache: - # TODO: Force over-write even if in cache? - if part_id not in self.part_cache or self.part_cache[part_id].size != part['size']: - self.part_cache[part_id] = Partition(root_path + part_id, self, part_id=part_id, size=part['size']) - - return {k: self.part_cache[k] for k in sorted(self.part_cache)} - - @property - def partition(self): - all_partitions = self.partitions - return [all_partitions[k] for k in all_partitions] - - @property - def partition_table_type(self): - return GPT - - @property - def uuid(self): - log('BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow') - """ - Returns the disk UUID as returned by lsblk. - This is more reliable than relying on /dev/disk/by-partuuid as - it doesn't seam to be able to detect md raid partitions. - """ - for partition in json.loads(SysCommand(f'lsblk -J -o+UUID {self.path}').decode('UTF-8'))['blockdevices']: - return partition.get('uuid', None) - - def convert_size_to_gb(self, size): - units = { - 'P' : lambda s : float(s) * 2048, - 'T' : lambda s : float(s) * 1024, - 'G' : lambda s : float(s), - 'M' : lambda s : float(s) / 1024, - 'K' : lambda s : float(s) / 2048, - 'B' : lambda s : float(s) / 3072, - } - unit = size[-1] - return float(units.get(unit, lambda s : None)(size[:-1])) - - @property - def size(self): - output = json.loads(SysCommand(f"lsblk --json -o+SIZE {self.path}").decode('UTF-8')) - - for device in output['blockdevices']: - return self.convert_size_to_gb(device['size']) - - @property - def bus_type(self): - output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) - - for device in output['blockdevices']: - return device['tran'] - - @property - def spinning(self): - output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) - - for device in output['blockdevices']: - return device['rota'] is True - - @property - def free_space(self): - # NOTE: parted -s will default to `cancel` on prompt, skipping any partition - # that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso, - # so the free will ignore the ESP partition and just give the "free" space. - # Doesn't harm us, but worth noting in case something weird happens. - for line in SysCommand(f"parted -s --machine {self.path} print free"): - if 'free' in (free_space := line.decode('UTF-8')): - _, start, end, size, *_ = free_space.strip('\r\n;').split(':') - yield (start, end, size) - - @property - def largest_free_space(self): - info = None - for space_info in self.free_space: - if not info: - info = space_info - else: - # [-1] = size - if space_info[-1] > info[-1]: - info = space_info - return info - - def has_partitions(self): - return len(self.partitions) - - def has_mount_point(self, mountpoint): - for partition in self.partitions: - if self.partitions[partition].mountpoint == mountpoint: - return True - return False - - def flush_cache(self): - self.part_cache = {} - - def get_partition(self, uuid): - for partition in self: - if partition.uuid == uuid: - return partition - - -class Partition: - def __init__(self, path: str, block_device: BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True): - if not part_id: - part_id = os.path.basename(path) - - self.block_device = block_device - self.path = path - self.part_id = part_id - self.mountpoint = mountpoint - self.target_mountpoint = mountpoint - self.filesystem = filesystem - self.size = size # TODO: Refresh? - self._encrypted = None - self.encrypted = encrypted - self.allow_formatting = False - - if mountpoint: - self.mount(mountpoint) - - mount_information = get_mount_info(self.path) - - if self.mountpoint != mount_information.get('target', None) and mountpoint: - raise DiskError(f"{self} was given a mountpoint but the actual mountpoint differs: {mount_information.get('target', None)}") - - if target := mount_information.get('target', None): - self.mountpoint = target - - if not self.filesystem and autodetect_filesystem: - if fstype := mount_information.get('fstype', get_filesystem_type(path)): - self.filesystem = fstype - - if self.filesystem == 'crypto_LUKS': - self.encrypted = True - - def __lt__(self, left_comparitor): - if type(left_comparitor) == Partition: - left_comparitor = left_comparitor.path - else: - left_comparitor = str(left_comparitor) - return self.path < left_comparitor # Not quite sure the order here is correct. But /dev/nvme0n1p1 comes before /dev/nvme0n1p5 so seems correct. - - def __repr__(self, *args, **kwargs): - mount_repr = '' - if self.mountpoint: - mount_repr = f", mounted={self.mountpoint}" - elif self.target_mountpoint: - mount_repr = f", rel_mountpoint={self.target_mountpoint}" - - if self._encrypted: - return f'Partition(path={self.path}, size={self.size}, PARTUUID={self.uuid}, parent={self.real_device}, fs={self.filesystem}{mount_repr})' - else: - return f'Partition(path={self.path}, size={self.size}, PARTUUID={self.uuid}, fs={self.filesystem}{mount_repr})' - - def __dump__(self): - return { - 'type' : 'primary', - 'PARTUUID' : self.uuid, - 'wipe' : self.allow_formatting, - 'boot' : self.boot, - 'ESP' : self.boot, - 'mountpoint' : self.target_mountpoint, - 'encrypted' : self._encrypted, - 'start' : self.start, - 'size' : self.end, - 'filesystem' : { - 'format' : get_filesystem_type(self.path) - } - } - - @property - def sector_size(self): - output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.path}").decode('UTF-8')) - - for device in output['blockdevices']: - return device.get('log-sec', None) - - @property - def start(self): - output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) - - for partition in output.get('partitiontable', {}).get('partitions', []): - if partition['node'] == self.path: - return partition['start']# * self.sector_size - - @property - def end(self): - # TODO: Verify that the logic holds up, that 'size' is the size without 'start' added to it. - output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) - - for partition in output.get('partitiontable', {}).get('partitions', []): - if partition['node'] == self.path: - return partition['size']# * self.sector_size - - @property - def boot(self): - output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) - - # Get the bootable flag from the sfdisk output: - # { - # "partitiontable": { - # "label":"dos", - # "id":"0xd202c10a", - # "device":"/dev/loop0", - # "unit":"sectors", - # "sectorsize":512, - # "partitions": [ - # {"node":"/dev/loop0p1", "start":2048, "size":10483712, "type":"83", "bootable":true} - # ] - # } - # } - - for partition in output.get('partitiontable', {}).get('partitions', []): - if partition['node'] == self.path: - return partition.get('bootable', False) - - return False - - @property - def partition_type(self): - lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8')) - - for device in lsblk['blockdevices']: - return device['pttype'] - - @property - def uuid(self) -> Optional[str]: - """ - Returns the PARTUUID as returned by lsblk. - This is more reliable than relying on /dev/disk/by-partuuid as - it doesn't seam to be able to detect md raid partitions. - """ - - lsblk = json.loads(SysCommand(f'lsblk -J -o+PARTUUID {self.path}').decode('UTF-8')) - for partition in lsblk['blockdevices']: - return partition.get('partuuid', None) - return None - - @property - def encrypted(self): - return self._encrypted - - @encrypted.setter - def encrypted(self, value: bool): - - self._encrypted = value - - @property - def parent(self): - return self.real_device - - @property - def real_device(self): - for blockdevice in json.loads(SysCommand('lsblk -J').decode('UTF-8'))['blockdevices']: - if parent := self.find_parent_of(blockdevice, os.path.basename(self.path)): - return f"/dev/{parent}" - # raise DiskError(f'Could not find appropriate parent for encrypted partition {self}') - return self.path - - def detect_inner_filesystem(self, password): - log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) - from .luks import luks2 - - try: - with luks2(self, storage.get('ENC_IDENTIFIER', 'ai')+'loop', password, auto_unmount=True) as unlocked_device: - return unlocked_device.filesystem - except SysCallError: - return None - - def has_content(self): - fs_type = get_filesystem_type(self.path) - if not fs_type or "swap" in fs_type: - return False - - temporary_mountpoint = '/tmp/' + hashlib.md5(bytes(f"{time.time()}", 'UTF-8') + os.urandom(12)).hexdigest() - temporary_path = pathlib.Path(temporary_mountpoint) - - temporary_path.mkdir(parents=True, exist_ok=True) - if (handle := SysCommand(f'/usr/bin/mount {self.path} {temporary_mountpoint}')).exit_code != 0: - raise DiskError(f'Could not mount and check for content on {self.path} because: {b"".join(handle)}') - - files = len(glob.glob(f"{temporary_mountpoint}/*")) - iterations = 0 - while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations + 1) < 10: - time.sleep(1) - - temporary_path.rmdir() - - return True if files > 0 else False - - def encrypt(self, *args, **kwargs): - """ - A wrapper function for luks2() instances and the .encrypt() method of that instance. - """ - from .luks import luks2 - - handle = luks2(self, None, None) - return handle.encrypt(self, *args, **kwargs) - - def format(self, filesystem=None, path=None, log_formatting=True): - """ - 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. - """ - if filesystem is None: - filesystem = self.filesystem - - if path is None: - path = self.path - - # To avoid "unable to open /dev/x: No such file or directory" - start_wait = time.time() - while pathlib.Path(path).exists() is False and time.time() - start_wait < 10: - time.sleep(0.025) - - if log_formatting: - 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')): - 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') - 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: - 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: - 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: - 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: - raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") - self.filesystem = filesystem - - elif filesystem == 'crypto_LUKS': - # from .luks import luks2 - # encrypted_partition = luks2(self, None, None) - # encrypted_partition.format(path) - self.filesystem = filesystem - - else: - raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.") - - if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS': - self.encrypted = True - else: - self.encrypted = False - - return True - - def find_parent_of(self, data, name, parent=None): - if data['name'] == name: - return parent - elif 'children' in data: - for child in data['children']: - if parent := self.find_parent_of(child, name, parent=data['name']): - return parent - - def mount(self, target, fs=None, options=''): - if not self.mountpoint: - log(f'Mounting {self} to {target}', level=logging.INFO) - if not fs: - if not self.filesystem: - raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.') - fs = self.filesystem - - pathlib.Path(target).mkdir(parents=True, exist_ok=True) - - try: - if options: - SysCommand(f"/usr/bin/mount -o {options} {self.path} {target}") - else: - SysCommand(f"/usr/bin/mount {self.path} {target}") - except SysCallError as err: - raise err - - self.mountpoint = target - return True - - def unmount(self): - try: - SysCommand(f"/usr/bin/umount {self.path}") - except SysCallError as err: - exit_code = err.exit_code - - # Without to much research, it seams that low error codes are errors. - # And above 8k is indicators such as "/dev/x not mounted.". - # So anything in between 0 and 8k are errors (?). - if 0 < exit_code < 8000: - raise err - - self.mountpoint = None - return True - - def umount(self): - return self.unmount() - - def filesystem_supported(self): - """ - The support for a filesystem (this partition) is tested by calling - partition.format() with a path set to '/dev/null' which returns two exceptions: - 1. SysCallError saying that /dev/null is not formattable - but the filesystem is supported - 2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type - """ - try: - self.format(self.filesystem, '/dev/null', log_formatting=False, allow_formatting=True) - except (SysCallError, DiskError): - pass # We supported it, but /dev/null is not formatable as expected so the mkfs call exited with an error code - except UnknownFilesystemFormat as err: - raise err - return True - - -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 - - -def device_state(name, *args, **kwargs): - # Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709 - if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)): - with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f: - if f.read(1) == '1': - return - - path = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/block/{}'.format(name))) - hotplug_buses = ("usb", "ieee1394", "mmc", "pcmcia", "firewire") - for bus in hotplug_buses: - if os.path.exists('/sys/bus/{}'.format(bus)): - for device_bus in os.listdir('/sys/bus/{}/devices'.format(bus)): - device_link = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/bus/{}/devices/{}'.format(bus, device_bus))) - if re.search(device_link, path): - return - return True - - -# lsblk --json -l -n -o path -def all_disks(*args, **kwargs): - kwargs.setdefault("partitions", False) - drives = {} - - lsblk = json.loads(SysCommand('lsblk --json -l -n -o path,size,type,mountpoint,label,pkname,model').decode('UTF_8')) - for drive in lsblk['blockdevices']: - if not kwargs['partitions'] and drive['type'] == 'part': - continue - - drives[drive['path']] = BlockDevice(drive['path'], drive) - - return drives - - -def convert_to_gigabytes(string): - unit = string.strip()[-1] - size = float(string.strip()[:-1]) - - if unit == 'M': - size = size / 1024 - elif unit == 'T': - size = size * 1024 - - return size - - -def harddrive(size=None, model=None, fuzzy=False): - collection = all_disks() - for drive in collection: - if size and convert_to_gigabytes(collection[drive]['size']) != size: - continue - if model and (collection[drive]['model'] is None or collection[drive]['model'].lower() != model.lower()): - continue - - return collection[drive] - - -def get_mount_info(path) -> dict: - try: - output = SysCommand(f'/usr/bin/findmnt --json {path}').decode('UTF-8') - except SysCallError: - return {} - - if not output: - return {} - - output = json.loads(output) - if 'filesystems' in output: - if len(output['filesystems']) > 1: - raise DiskError(f"Path '{path}' contains multiple mountpoints: {output['filesystems']}") - - return output['filesystems'][0] - - -def get_partitions_in_use(mountpoint) -> list: - try: - output = SysCommand(f"/usr/bin/findmnt --json -R {mountpoint}").decode('UTF-8') - except SysCallError: - return [] - - mounts = [] - - if not output: - return [] - - output = json.loads(output) - for target in output.get('filesystems', []): - mounts.append(Partition(target['source'], None, filesystem=target.get('fstype', None), mountpoint=target['target'])) - - for child in target.get('children', []): - mounts.append(Partition(child['source'], None, filesystem=child.get('fstype', None), mountpoint=child['target'])) - - return mounts - - -def get_filesystem_type(path): - try: - return SysCommand(f"blkid -o value -s TYPE {path}").decode('UTF-8').strip() - except SysCallError: - return None - - -def disk_layouts(): - try: - return json.loads(SysCommand("lsblk -f -o+TYPE,SIZE -J").decode('UTF-8')) - except SysCallError as err: - log(f"Could not return disk layouts: {err}") - return None - - -def encrypted_partitions(blockdevices :dict) -> bool: - for partition in blockdevices.values(): - if partition.get('encrypted', False): - yield partition - -def find_partition_by_mountpoint(block_devices, relative_mountpoint :str): - for device in block_devices: - for partition in block_devices[device]['partitions']: - if partition.get('mountpoint', None) == relative_mountpoint: - return partition \ No newline at end of file diff --git a/archinstall/lib/disk/__init__.py b/archinstall/lib/disk/__init__.py new file mode 100644 index 00000000..8237f774 --- /dev/null +++ b/archinstall/lib/disk/__init__.py @@ -0,0 +1,7 @@ +from .btrfs import * +from .helpers import * +from .blockdevice import BlockDevice +from .filesystem import Filesystem +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 new file mode 100644 index 00000000..e2fcdae5 --- /dev/null +++ b/archinstall/lib/disk/blockdevice.py @@ -0,0 +1,207 @@ +from ..output import log + +class BlockDevice: + def __init__(self, path, info=None): + if not info: + # If we don't give any information, we need to auto-fill it. + # Otherwise any subsequent usage will break. + info = all_disks()[path].info + + self.path = path + self.info = info + self.keep_partitions = True + self.part_cache = {} + + # TODO: Currently disk encryption is a BIT misleading. + # It's actually partition-encryption, but for future-proofing this + # I'm placing the encryption password on a BlockDevice level. + + def __repr__(self, *args, **kwargs): + return f"BlockDevice({self.device}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" + + def __iter__(self): + for partition in self.partitions: + yield self.partitions[partition] + + def __getitem__(self, key, *args, **kwargs): + if key not in self.info: + raise KeyError(f'{self} does not contain information: "{key}"') + return self.info[key] + + def __len__(self): + return len(self.partitions) + + def __lt__(self, left_comparitor): + return self.path < left_comparitor.path + + def json(self): + """ + json() has precedence over __dump__, so this is a way + to give less/partial information for user readability. + """ + return self.path + + def __dump__(self): + return { + self.path : { + 'partuuid' : self.uuid, + 'wipe' : self.info.get('wipe', None), + 'partitions' : [part.__dump__() for part in self.partitions.values()] + } + } + + @property + def partition_type(self): + output = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8')) + + for device in output['blockdevices']: + return device['pttype'] + + @property + def device(self): + """ + Returns the actual device-endpoint of the BlockDevice. + If it's a loop-back-device it returns the back-file, + If it's a ATA-drive it returns the /dev/X device + And if it's a crypto-device it returns the parent device + """ + if "type" not in self.info: + raise DiskError(f'Could not locate backplane info for "{self.path}"') + + if self.info['type'] == 'loop': + for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: + if not drive['name'] == self.path: + continue + + return drive['back-file'] + elif self.info['type'] == 'disk': + return self.path + elif self.info['type'][:4] == 'raid': + # This should catch /dev/md## raid devices + return self.path + elif self.info['type'] == 'crypt': + if 'pkname' not in self.info: + raise DiskError(f'A crypt device ({self.path}) without a parent kernel device name.') + return f"/dev/{self.info['pkname']}" + else: + log(f"Unknown blockdevice type for {self.path}: {self.info['type']}", level=logging.DEBUG) + + # if not stat.S_ISBLK(os.stat(full_path).st_mode): + # raise DiskError(f'Selected disk "{full_path}" is not a block device.') + + @property + def partitions(self): + SysCommand(['partprobe', self.path]) + + result = SysCommand(['/usr/bin/lsblk', '-J', self.path]) + + if b'not a block device' in result: + raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}') + + if not result[:1] == b'{': + raise DiskError('Error getting JSON output from:', f'/usr/bin/lsblk -J {self.path}') + + r = json.loads(result.decode('UTF-8')) + if len(r['blockdevices']) and 'children' in r['blockdevices'][0]: + root_path = f"/dev/{r['blockdevices'][0]['name']}" + for part in r['blockdevices'][0]['children']: + part_id = part['name'][len(os.path.basename(self.path)):] + if part_id not in self.part_cache: + # TODO: Force over-write even if in cache? + if part_id not in self.part_cache or self.part_cache[part_id].size != part['size']: + self.part_cache[part_id] = Partition(root_path + part_id, self, part_id=part_id, size=part['size']) + + return {k: self.part_cache[k] for k in sorted(self.part_cache)} + + @property + def partition(self): + all_partitions = self.partitions + return [all_partitions[k] for k in all_partitions] + + @property + def partition_table_type(self): + return GPT + + @property + def uuid(self): + log('BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow') + """ + Returns the disk UUID as returned by lsblk. + This is more reliable than relying on /dev/disk/by-partuuid as + it doesn't seam to be able to detect md raid partitions. + """ + for partition in json.loads(SysCommand(f'lsblk -J -o+UUID {self.path}').decode('UTF-8'))['blockdevices']: + return partition.get('uuid', None) + + def convert_size_to_gb(self, size): + units = { + 'P' : lambda s : float(s) * 2048, + 'T' : lambda s : float(s) * 1024, + 'G' : lambda s : float(s), + 'M' : lambda s : float(s) / 1024, + 'K' : lambda s : float(s) / 2048, + 'B' : lambda s : float(s) / 3072, + } + unit = size[-1] + return float(units.get(unit, lambda s : None)(size[:-1])) + + @property + def size(self): + output = json.loads(SysCommand(f"lsblk --json -o+SIZE {self.path}").decode('UTF-8')) + + for device in output['blockdevices']: + return self.convert_size_to_gb(device['size']) + + @property + def bus_type(self): + output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) + + for device in output['blockdevices']: + return device['tran'] + + @property + def spinning(self): + output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) + + for device in output['blockdevices']: + return device['rota'] is True + + @property + def free_space(self): + # NOTE: parted -s will default to `cancel` on prompt, skipping any partition + # that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso, + # so the free will ignore the ESP partition and just give the "free" space. + # Doesn't harm us, but worth noting in case something weird happens. + for line in SysCommand(f"parted -s --machine {self.path} print free"): + if 'free' in (free_space := line.decode('UTF-8')): + _, start, end, size, *_ = free_space.strip('\r\n;').split(':') + yield (start, end, size) + + @property + def largest_free_space(self): + info = None + for space_info in self.free_space: + if not info: + info = space_info + else: + # [-1] = size + if space_info[-1] > info[-1]: + info = space_info + return info + + def has_partitions(self): + return len(self.partitions) + + def has_mount_point(self, mountpoint): + for partition in self.partitions: + if self.partitions[partition].mountpoint == mountpoint: + return True + return False + + def flush_cache(self): + self.part_cache = {} + + def get_partition(self, uuid): + for partition in self: + if partition.uuid == uuid: + return partition \ No newline at end of file diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py new file mode 100644 index 00000000..d6758b3f --- /dev/null +++ b/archinstall/lib/disk/btrfs.py @@ -0,0 +1,4 @@ +from ..general import SysCommand + +def create_subvolume(installation): + SysCommand(f"btrfs subvolume create {installation.target}/@") \ No newline at end of file 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 diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py new file mode 100644 index 00000000..d37dfd9b --- /dev/null +++ b/archinstall/lib/disk/helpers.py @@ -0,0 +1,178 @@ +import re +import json +from ..exceptions import SysCallError +from ..general import SysCommand +from ..output import log + +ROOT_DIR_PATTERN = re.compile('^.*?/devices') + +def sort_block_devices_based_on_performance(block_devices): + result = {device: 0 for device in block_devices} + + for device, weight in result.items(): + if device.spinning: + weight -= 10 + else: + weight += 5 + + if device.bus_type == 'nvme': + weight += 20 + elif device.bus_type == 'sata': + weight += 10 + + result[device] = weight + + return result + +def filter_disks_below_size_in_gb(devices, gigabytes): + for disk in devices: + if disk.size >= gigabytes: + yield disk + +def select_largest_device(devices, gigabytes, filter_out=None): + if not filter_out: + filter_out = [] + + copy_devices = [*devices] + for filter_device in filter_out: + if filter_device in copy_devices: + copy_devices.pop(copy_devices.index(filter_device)) + + copy_devices = list(filter_disks_below_size_in_gb(copy_devices, gigabytes)) + + if not len(copy_devices): + return None + + return max(copy_devices, key=(lambda device : device.size)) + +def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None): + if not filter_out: + filter_out = [] + + copy_devices = [*devices] + for filter_device in filter_out: + if filter_device in copy_devices: + copy_devices.pop(copy_devices.index(filter_device)) + + if not len(copy_devices): + return None + + return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) + +def convert_to_gigabytes(string): + unit = string.strip()[-1] + size = float(string.strip()[:-1]) + + if unit == 'M': + size = size / 1024 + elif unit == 'T': + size = size * 1024 + + return size + +def device_state(name, *args, **kwargs): + # Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709 + if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)): + with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f: + if f.read(1) == '1': + return + + path = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/block/{}'.format(name))) + hotplug_buses = ("usb", "ieee1394", "mmc", "pcmcia", "firewire") + for bus in hotplug_buses: + if os.path.exists('/sys/bus/{}'.format(bus)): + for device_bus in os.listdir('/sys/bus/{}/devices'.format(bus)): + device_link = ROOT_DIR_PATTERN.sub('', os.readlink('/sys/bus/{}/devices/{}'.format(bus, device_bus))) + if re.search(device_link, path): + return + return True + +# lsblk --json -l -n -o path +def all_disks(*args, **kwargs): + kwargs.setdefault("partitions", False) + drives = {} + + lsblk = json.loads(SysCommand('lsblk --json -l -n -o path,size,type,mountpoint,label,pkname,model').decode('UTF_8')) + for drive in lsblk['blockdevices']: + if not kwargs['partitions'] and drive['type'] == 'part': + continue + + drives[drive['path']] = BlockDevice(drive['path'], drive) + + return drives + + +def harddrive(size=None, model=None, fuzzy=False): + collection = all_disks() + for drive in collection: + if size and convert_to_gigabytes(collection[drive]['size']) != size: + continue + if model and (collection[drive]['model'] is None or collection[drive]['model'].lower() != model.lower()): + continue + + return collection[drive] + + +def get_mount_info(path) -> dict: + try: + output = SysCommand(f'/usr/bin/findmnt --json {path}').decode('UTF-8') + except SysCallError: + return {} + + if not output: + return {} + + output = json.loads(output) + if 'filesystems' in output: + if len(output['filesystems']) > 1: + raise DiskError(f"Path '{path}' contains multiple mountpoints: {output['filesystems']}") + + return output['filesystems'][0] + + +def get_partitions_in_use(mountpoint) -> list: + try: + output = SysCommand(f"/usr/bin/findmnt --json -R {mountpoint}").decode('UTF-8') + except SysCallError: + return [] + + mounts = [] + + if not output: + return [] + + output = json.loads(output) + for target in output.get('filesystems', []): + mounts.append(Partition(target['source'], None, filesystem=target.get('fstype', None), mountpoint=target['target'])) + + for child in target.get('children', []): + mounts.append(Partition(child['source'], None, filesystem=child.get('fstype', None), mountpoint=child['target'])) + + return mounts + + +def get_filesystem_type(path): + try: + return SysCommand(f"blkid -o value -s TYPE {path}").decode('UTF-8').strip() + except SysCallError: + return None + + +def disk_layouts(): + try: + return json.loads(SysCommand("lsblk -f -o+TYPE,SIZE -J").decode('UTF-8')) + except SysCallError as err: + log(f"Could not return disk layouts: {err}") + return None + + +def encrypted_partitions(blockdevices :dict) -> bool: + for partition in blockdevices.values(): + if partition.get('encrypted', False): + yield partition + +def find_partition_by_mountpoint(block_devices, relative_mountpoint :str): + for device in block_devices: + for partition in block_devices[device]['partitions']: + if partition.get('mountpoint', None) == relative_mountpoint: + return partition \ No newline at end of file diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py new file mode 100644 index 00000000..6b60347f --- /dev/null +++ b/archinstall/lib/disk/partition.py @@ -0,0 +1,332 @@ +import glob +import pathlib +import time +from typing import Optional +from .blockdevice import BlockDevice +from ..output import log + +class Partition: + def __init__(self, path: str, block_device: BlockDevice, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True): + if not part_id: + part_id = os.path.basename(path) + + self.block_device = block_device + self.path = path + self.part_id = part_id + self.mountpoint = mountpoint + self.target_mountpoint = mountpoint + self.filesystem = filesystem + self.size = size # TODO: Refresh? + self._encrypted = None + self.encrypted = encrypted + self.allow_formatting = False + + if mountpoint: + self.mount(mountpoint) + + mount_information = get_mount_info(self.path) + + if self.mountpoint != mount_information.get('target', None) and mountpoint: + raise DiskError(f"{self} was given a mountpoint but the actual mountpoint differs: {mount_information.get('target', None)}") + + if target := mount_information.get('target', None): + self.mountpoint = target + + if not self.filesystem and autodetect_filesystem: + if fstype := mount_information.get('fstype', get_filesystem_type(path)): + self.filesystem = fstype + + if self.filesystem == 'crypto_LUKS': + self.encrypted = True + + def __lt__(self, left_comparitor): + if type(left_comparitor) == Partition: + left_comparitor = left_comparitor.path + else: + left_comparitor = str(left_comparitor) + return self.path < left_comparitor # Not quite sure the order here is correct. But /dev/nvme0n1p1 comes before /dev/nvme0n1p5 so seems correct. + + def __repr__(self, *args, **kwargs): + mount_repr = '' + if self.mountpoint: + mount_repr = f", mounted={self.mountpoint}" + elif self.target_mountpoint: + mount_repr = f", rel_mountpoint={self.target_mountpoint}" + + if self._encrypted: + return f'Partition(path={self.path}, size={self.size}, PARTUUID={self.uuid}, parent={self.real_device}, fs={self.filesystem}{mount_repr})' + else: + return f'Partition(path={self.path}, size={self.size}, PARTUUID={self.uuid}, fs={self.filesystem}{mount_repr})' + + def __dump__(self): + return { + 'type' : 'primary', + 'PARTUUID' : self.uuid, + 'wipe' : self.allow_formatting, + 'boot' : self.boot, + 'ESP' : self.boot, + 'mountpoint' : self.target_mountpoint, + 'encrypted' : self._encrypted, + 'start' : self.start, + 'size' : self.end, + 'filesystem' : { + 'format' : get_filesystem_type(self.path) + } + } + + @property + def sector_size(self): + output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.path}").decode('UTF-8')) + + for device in output['blockdevices']: + return device.get('log-sec', None) + + @property + def start(self): + output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) + + for partition in output.get('partitiontable', {}).get('partitions', []): + if partition['node'] == self.path: + return partition['start']# * self.sector_size + + @property + def end(self): + # TODO: Verify that the logic holds up, that 'size' is the size without 'start' added to it. + output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) + + for partition in output.get('partitiontable', {}).get('partitions', []): + if partition['node'] == self.path: + return partition['size']# * self.sector_size + + @property + def boot(self): + output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) + + # Get the bootable flag from the sfdisk output: + # { + # "partitiontable": { + # "label":"dos", + # "id":"0xd202c10a", + # "device":"/dev/loop0", + # "unit":"sectors", + # "sectorsize":512, + # "partitions": [ + # {"node":"/dev/loop0p1", "start":2048, "size":10483712, "type":"83", "bootable":true} + # ] + # } + # } + + for partition in output.get('partitiontable', {}).get('partitions', []): + if partition['node'] == self.path: + return partition.get('bootable', False) + + return False + + @property + def partition_type(self): + lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8')) + + for device in lsblk['blockdevices']: + return device['pttype'] + + @property + def uuid(self) -> Optional[str]: + """ + Returns the PARTUUID as returned by lsblk. + This is more reliable than relying on /dev/disk/by-partuuid as + it doesn't seam to be able to detect md raid partitions. + """ + + lsblk = json.loads(SysCommand(f'lsblk -J -o+PARTUUID {self.path}').decode('UTF-8')) + for partition in lsblk['blockdevices']: + return partition.get('partuuid', None) + return None + + @property + def encrypted(self): + return self._encrypted + + @encrypted.setter + def encrypted(self, value: bool): + + self._encrypted = value + + @property + def parent(self): + return self.real_device + + @property + def real_device(self): + for blockdevice in json.loads(SysCommand('lsblk -J').decode('UTF-8'))['blockdevices']: + if parent := self.find_parent_of(blockdevice, os.path.basename(self.path)): + return f"/dev/{parent}" + # raise DiskError(f'Could not find appropriate parent for encrypted partition {self}') + return self.path + + def detect_inner_filesystem(self, password): + log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) + from .luks import luks2 + + try: + with luks2(self, storage.get('ENC_IDENTIFIER', 'ai')+'loop', password, auto_unmount=True) as unlocked_device: + return unlocked_device.filesystem + except SysCallError: + return None + + def has_content(self): + fs_type = get_filesystem_type(self.path) + if not fs_type or "swap" in fs_type: + return False + + temporary_mountpoint = '/tmp/' + hashlib.md5(bytes(f"{time.time()}", 'UTF-8') + os.urandom(12)).hexdigest() + temporary_path = pathlib.Path(temporary_mountpoint) + + temporary_path.mkdir(parents=True, exist_ok=True) + if (handle := SysCommand(f'/usr/bin/mount {self.path} {temporary_mountpoint}')).exit_code != 0: + raise DiskError(f'Could not mount and check for content on {self.path} because: {b"".join(handle)}') + + files = len(glob.glob(f"{temporary_mountpoint}/*")) + iterations = 0 + while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations + 1) < 10: + time.sleep(1) + + temporary_path.rmdir() + + return True if files > 0 else False + + def encrypt(self, *args, **kwargs): + """ + A wrapper function for luks2() instances and the .encrypt() method of that instance. + """ + from .luks import luks2 + + handle = luks2(self, None, None) + return handle.encrypt(self, *args, **kwargs) + + def format(self, filesystem=None, path=None, log_formatting=True): + """ + 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. + """ + if filesystem is None: + filesystem = self.filesystem + + if path is None: + path = self.path + + # To avoid "unable to open /dev/x: No such file or directory" + start_wait = time.time() + while pathlib.Path(path).exists() is False and time.time() - start_wait < 10: + time.sleep(0.025) + + if log_formatting: + 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')): + 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') + 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: + 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: + 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: + 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: + raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}") + self.filesystem = filesystem + + elif filesystem == 'crypto_LUKS': + # from .luks import luks2 + # encrypted_partition = luks2(self, None, None) + # encrypted_partition.format(path) + self.filesystem = filesystem + + else: + raise UnknownFilesystemFormat(f"Fileformat '{filesystem}' is not yet implemented.") + + if get_filesystem_type(path) == 'crypto_LUKS' or get_filesystem_type(self.real_device) == 'crypto_LUKS': + self.encrypted = True + else: + self.encrypted = False + + return True + + def find_parent_of(self, data, name, parent=None): + if data['name'] == name: + return parent + elif 'children' in data: + for child in data['children']: + if parent := self.find_parent_of(child, name, parent=data['name']): + return parent + + def mount(self, target, fs=None, options=''): + if not self.mountpoint: + log(f'Mounting {self} to {target}', level=logging.INFO) + if not fs: + if not self.filesystem: + raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.') + fs = self.filesystem + + pathlib.Path(target).mkdir(parents=True, exist_ok=True) + + try: + if options: + SysCommand(f"/usr/bin/mount -o {options} {self.path} {target}") + else: + SysCommand(f"/usr/bin/mount {self.path} {target}") + except SysCallError as err: + raise err + + self.mountpoint = target + return True + + def unmount(self): + try: + SysCommand(f"/usr/bin/umount {self.path}") + except SysCallError as err: + exit_code = err.exit_code + + # Without to much research, it seams that low error codes are errors. + # And above 8k is indicators such as "/dev/x not mounted.". + # So anything in between 0 and 8k are errors (?). + if 0 < exit_code < 8000: + raise err + + self.mountpoint = None + return True + + def umount(self): + return self.unmount() + + def filesystem_supported(self): + """ + The support for a filesystem (this partition) is tested by calling + partition.format() with a path set to '/dev/null' which returns two exceptions: + 1. SysCallError saying that /dev/null is not formattable - but the filesystem is supported + 2. UnknownFilesystemFormat that indicates that we don't support the given filesystem type + """ + try: + self.format(self.filesystem, '/dev/null', log_formatting=False, allow_formatting=True) + except (SysCallError, DiskError): + pass # We supported it, but /dev/null is not formatable as expected so the mkfs call exited with an error code + except UnknownFilesystemFormat as err: + raise err + return True \ No newline at end of file diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py new file mode 100644 index 00000000..f6466268 --- /dev/null +++ b/archinstall/lib/disk/user_guides.py @@ -0,0 +1,145 @@ +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 + default_filesystem = ask_for_main_filesystem_format() + + MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb + + layout = { + block_device.path : { + "wipe" : True, + "partitions" : [] + } + } + + layout[block_device.path]['partitions'].append({ + # Boot + "type" : "primary", + "start" : "1MiB", + "size" : "513MiB", + "boot" : True, + "encrypted" : False, + "format" : True, + "mountpoint" : "/boot", + "filesystem" : { + "format" : "fat32" + } + }) + layout[block_device.path]['partitions'].append({ + # Root + "type" : "primary", + "start" : "513MiB", + "encrypted" : False, + "format" : True, + "size" : "100%" if block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART else f"{min(block_device.size, 20)*1024}MiB", + "mountpoint" : "/", + "filesystem" : { + "format" : default_filesystem + } + }) + + 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' + } + } + + elif block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: + # If we don't want to use subvolumes, + # But we want to be able to re-use data between re-installs.. + # A second partition for /home would be nice if we have the space for it + layout[block_device.path]['partitions'].append({ + # Home + "type" : "primary", + "encrypted" : False, + "format" : True, + "start" : f"{min(block_device.size*0.2, 20)*1024}MiB", + "size" : "100%", + "mountpoint" : "/home", + "filesystem" : { + "format" : default_filesystem + } + }) + + return layout + + +def suggest_multi_disk_layout(block_devices, default_filesystem=None): + if not default_filesystem: + 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: + # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ + # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ + + MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb + ARCH_LINUX_INSTALLED_SIZE = 20 # Gb, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? + + block_devices = sort_block_devices_based_on_performance(block_devices).keys() + + home_device = select_largest_device(block_devices, gigabytes=MIN_SIZE_TO_ALLOW_HOME_PART) + root_device = select_disk_larger_than_or_close_to(block_devices, gigabytes=ARCH_LINUX_INSTALLED_SIZE, filter_out=[home_device]) + + log(f"Suggesting multi-disk-layout using {len(block_devices)} disks, where {root_device} will be /root and {home_device} will be /home", level=logging.DEBUG) + + layout = { + root_device.path : { + "wipe" : True, + "partitions" : [] + }, + home_device.path : { + "wipe" : True, + "partitions" : [] + }, + } + + layout[root_device.path]['partitions'].append({ + # Boot + "type" : "primary", + "start" : "1MiB", + "size" : "513MiB", + "boot" : True, + "encrypted" : False, + "format" : True, + "mountpoint" : "/boot", + "filesystem" : { + "format" : "fat32" + } + }) + layout[root_device.path]['partitions'].append({ + # Root + "type" : "primary", + "start" : "513MiB", + "encrypted" : False, + "format" : True, + "size" : "100%", + "mountpoint" : "/", + "filesystem" : { + "format" : default_filesystem + } + }) + + layout[home_device.path]['partitions'].append({ + # Home + "type" : "primary", + "encrypted" : False, + "format" : True, + "start" : "4MiB", + "size" : "100%", + "mountpoint" : "/home", + "filesystem" : { + "format" : default_filesystem + } + }) + + return layout \ No newline at end of file diff --git a/archinstall/lib/disk/validators.py b/archinstall/lib/disk/validators.py new file mode 100644 index 00000000..0c9f5a74 --- /dev/null +++ b/archinstall/lib/disk/validators.py @@ -0,0 +1,42 @@ +def valid_parted_position(pos :str): + if not len(pos): + return False + + if pos.isdigit(): + return True + + if pos[-1] == '%' and pos[:-1].isdigit(): + return True + + if pos[-3:].lower() in ['mib', 'kib', 'b', 'tib'] and pos[:-3].replace(".", "", 1).isdigit(): + return True + + if pos[-2:].lower() in ['kb', 'mb', 'gb', 'tb'] and pos[:-2].replace(".", "", 1).isdigit(): + return True + + return False + +def valid_fs_type(fstype :str) -> bool: + # https://www.gnu.org/software/parted/manual/html_node/mkpart.html + # Above link doesn't agree with `man parted` /mkpart documentation: + """ + fs-type can + be one of "btrfs", "ext2", + "ext3", "ext4", "fat16", + "fat32", "hfs", "hfs+", + "linux-swap", "ntfs", "reis‐ + erfs", "udf", or "xfs". + """ + + return fstype.lower() in [ + "btrfs", + "ext2", + "ext3", "ext4", # `man parted` allows these + "fat16", "fat32", + "hfs", "hfs+", # "hfsx", not included in `man parted` + "linux-swap", + "ntfs", + "reiserfs", + "udf", # "ufs", not included in `man parted` + "xfs", # `man parted` allows this + ] \ No newline at end of file diff --git a/archinstall/lib/disk2/btrfs.py b/archinstall/lib/disk2/btrfs.py deleted file mode 100644 index d6758b3f..00000000 --- a/archinstall/lib/disk2/btrfs.py +++ /dev/null @@ -1,4 +0,0 @@ -from ..general import SysCommand - -def create_subvolume(installation): - SysCommand(f"btrfs subvolume create {installation.target}/@") \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 3ec8df97533c6cf185e66d0839876527cfafec01 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Oct 2021 20:45:44 +0200 Subject: Removed the old disk.py --- archinstall/lib/disk.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 archinstall/lib/disk.py (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py deleted file mode 100644 index e69de29b..00000000 -- cgit v1.2.3-54-g00ecf From 2ef793b76af3173c3bbe7fd9942263df6d84d464 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Oct 2021 20:55:00 +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/blockdevice.py | 2 ++ archinstall/lib/disk/helpers.py | 1 + 2 files changed, 3 insertions(+) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py index e2fcdae5..422f35aa 100644 --- a/archinstall/lib/disk/blockdevice.py +++ b/archinstall/lib/disk/blockdevice.py @@ -1,4 +1,6 @@ +import json from ..output import log +from ..general import SysCommand class BlockDevice: def __init__(self, path, info=None): diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index d37dfd9b..8b372f73 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -1,5 +1,6 @@ import re import json +from .blockdevice import BlockDevice from ..exceptions import SysCallError from ..general import SysCommand from ..output import log -- cgit v1.2.3-54-g00ecf From 8b96080ec81939f3a69c28585c21c43e496d087b Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Fri, 22 Oct 2021 21:02:39 +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/blockdevice.py | 1 + archinstall/lib/disk/partition.py | 1 + archinstall/lib/disk/user_guides.py | 1 + 3 files changed, 3 insertions(+) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py index 422f35aa..daa65323 100644 --- a/archinstall/lib/disk/blockdevice.py +++ b/archinstall/lib/disk/blockdevice.py @@ -1,4 +1,5 @@ import json +import logging from ..output import log from ..general import SysCommand diff --git a/archinstall/lib/disk/partition.py b/archinstall/lib/disk/partition.py index 6b60347f..30151583 100644 --- a/archinstall/lib/disk/partition.py +++ b/archinstall/lib/disk/partition.py @@ -1,6 +1,7 @@ import glob import pathlib import time +import logging from typing import Optional from .blockdevice import BlockDevice from ..output import log diff --git a/archinstall/lib/disk/user_guides.py b/archinstall/lib/disk/user_guides.py index f6466268..0a975149 100644 --- a/archinstall/lib/disk/user_guides.py +++ b/archinstall/lib/disk/user_guides.py @@ -1,3 +1,4 @@ +import logging from ..output import log def suggest_single_disk_layout(block_device, default_filesystem=None): -- cgit v1.2.3-54-g00ecf 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') 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-54-g00ecf From 68b891837c6174d1f75babf42ee6657d4726576b Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Wed, 27 Oct 2021 13:13:10 +0000 Subject: Finalized the create_subvolume and mount_subvolume functions. Remaining is to call these functions during the disk setup process to create the subvolumes and mount them in place, rather than doing the normal steps. --- archinstall/lib/disk/btrfs.py | 36 ++++++++++++++++++++++++++++++++++-- archinstall/lib/disk/helpers.py | 4 +++- 2 files changed, 37 insertions(+), 3 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index d6758b3f..558a249e 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -1,4 +1,36 @@ +import pathlib, glob +from typing import Union +from .helpers import get_mount_info +from ..exceptions import DiskError from ..general import SysCommand -def create_subvolume(installation): - SysCommand(f"btrfs subvolume create {installation.target}/@") \ No newline at end of file +def mount_subvolume(installation, location :Union[pathlib.Path, str], force=False) -> bool: + """ + This function uses mount to mount a subvolume on a given device, at a given location with a given subvolume name. + + @installation: archinstall.Installer instance + @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot + @force: overrides the check for weither or not the subvolume mountpoint is empty or not + """ + # Set up the required physical structure + if type(location) == str: + location = pathlib.Path(location) + + if not location.exists(): + location.mkdir(parents=True) + + if glob.glob(str(installation.target/location/'*')) and force is False: + 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 + +def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: + """ + This function uses btrfs to create a subvolume. + + @installation: archinstall.Installer instance + @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot + """ + + SysCommand(f"btrfs subvolume create {installation.target}/{str(location)}") \ No newline at end of file diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 65abdea2..2e84a657 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -1,5 +1,7 @@ import re import json +import pathlib +from typing import Union from .blockdevice import BlockDevice from ..exceptions import SysCallError from ..general import SysCommand @@ -114,7 +116,7 @@ def harddrive(size=None, model=None, fuzzy=False): return collection[drive] -def get_mount_info(path) -> dict: +def get_mount_info(path :Union[pathlib.Path, str]) -> dict: try: output = SysCommand(f'/usr/bin/findmnt --json {path}').decode('UTF-8') except SysCallError: -- cgit v1.2.3-54-g00ecf 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') 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-54-g00ecf From 5d124f810699580663b865009cc2c5f366acd9b9 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 13:51:39 +0200 Subject: Added debugging to the btrfs functions. --- archinstall/lib/disk/btrfs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index e9ffec66..f26a18f5 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -3,6 +3,7 @@ from typing import Union from .helpers import get_mount_info from ..exceptions import DiskError from ..general import SysCommand +from ..output import log def mount_subvolume(installation, location :Union[pathlib.Path, str], force=False) -> bool: """ @@ -22,6 +23,8 @@ def mount_subvolume(installation, location :Union[pathlib.Path, str], force=Fals if glob.glob(str(installation.target/location/'*')) and force is False: raise DiskError(f"Cannot mount subvolume to {installation.target/location} because it contains data (non-empty folder target)") + log(f"Mounting {location} as a subvolume", level=logging.INFO) + print(get_mount_info(installation.target/location)) # Mount the logical volume to the physical structure mount_location = get_mount_info(installation.target/location)['source'] SysCommand(f"umount {mount_location}") @@ -34,5 +37,5 @@ def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: @installation: archinstall.Installer instance @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot """ - + log(f"Creating a subvolume on {installation.target}/{str(location)}", level=logging.INFO) SysCommand(f"btrfs subvolume create {installation.target}/{str(location)}") \ No newline at end of file -- cgit v1.2.3-54-g00ecf From d3aec52d986718f8d4f15063aa656dc4627dae81 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 13:54:18 +0200 Subject: Forgot an import --- archinstall/lib/disk/btrfs.py | 1 + 1 file changed, 1 insertion(+) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index f26a18f5..b56430b8 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -1,4 +1,5 @@ import pathlib, glob +import logging from typing import Union from .helpers import get_mount_info from ..exceptions import DiskError -- cgit v1.2.3-54-g00ecf From 6be071b6e0d43a2fded8a3fd773525fb1216aa91 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 16:16:43 +0200 Subject: Enhanced get_mount_info() to recursively get the information --- archinstall/lib/disk/helpers.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 2e84a657..0111d327 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -117,10 +117,13 @@ def harddrive(size=None, model=None, fuzzy=False): def get_mount_info(path :Union[pathlib.Path, str]) -> dict: - try: - output = SysCommand(f'/usr/bin/findmnt --json {path}').decode('UTF-8') - except SysCallError: - return {} + for traversal in list(map(str, [str(path)] + list(pathlib.Path(str(path)).parents))): + try: + output = SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8') + if output: + break + except SysCallError: + pass if not output: return {} @@ -132,6 +135,8 @@ def get_mount_info(path :Union[pathlib.Path, str]) -> dict: return output['filesystems'][0] + return {} + def get_partitions_in_use(mountpoint) -> list: from .partition import Partition -- cgit v1.2.3-54-g00ecf From c4f688ce4c9ce89748e11bd0565c9b3720e66b74 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 16:28:55 +0200 Subject: Added some error handling to mount points not getting mounted properly. --- archinstall/lib/disk/btrfs.py | 2 +- archinstall/lib/disk/helpers.py | 5 ++++- archinstall/lib/installer.py | 7 +++++++ 3 files changed, 12 insertions(+), 2 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index b56430b8..cf60095f 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -25,7 +25,7 @@ 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)") log(f"Mounting {location} as a subvolume", level=logging.INFO) - print(get_mount_info(installation.target/location)) + print(get_mount_info(installation.target/location, traverse=True)) # Mount the logical volume to the physical structure mount_location = get_mount_info(installation.target/location)['source'] SysCommand(f"umount {mount_location}") diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 0111d327..8a6d5a48 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -116,7 +116,7 @@ def harddrive(size=None, model=None, fuzzy=False): return collection[drive] -def get_mount_info(path :Union[pathlib.Path, str]) -> dict: +def get_mount_info(path :Union[pathlib.Path, str], traverse=False) -> dict: for traversal in list(map(str, [str(path)] + list(pathlib.Path(str(path)).parents))): try: output = SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8') @@ -125,6 +125,9 @@ def get_mount_info(path :Union[pathlib.Path, str]) -> dict: except SysCallError: pass + if not traverse: + break + if not output: return {} diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 41d918a1..b0a7c5f1 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,11 +1,14 @@ +import time from .disk import * from .hardware import * from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout +from .helpers import get_mount_info from .mirrors import * from .plugins import plugins from .storage import storage from .user_interaction import * from .disk.btrfs import create_subvolume, mount_subvolume +from .exceptions import DiskError, ServiceException # 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"] @@ -144,6 +147,10 @@ class Installer: else: mountpoints[mountpoint]['device_instance'].mount(f"{self.target}{mountpoint}") + time.sleep(1) + if not get_mount_info(f"{self.target}{mountpoint}", traverse=False): + raise DiskError(f"Target {self.target}{mountpoint} never got mounted properly.") + if (subvolumes := mountpoints[mountpoint].get('btrfs', {}).get('subvolumes', {})): for name, location in subvolumes.items(): create_subvolume(self, location) -- cgit v1.2.3-54-g00ecf From e6de28287dd149b37a41b8ddbcb35e28435536f7 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 16:29:52 +0200 Subject: Wrong lib import path --- archinstall/lib/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index b0a7c5f1..0bdddb2e 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -2,7 +2,7 @@ import time from .disk import * from .hardware import * from .locale_helpers import verify_keyboard_layout, verify_x11_keyboard_layout -from .helpers import get_mount_info +from .disk.helpers import get_mount_info from .mirrors import * from .plugins import plugins from .storage import storage -- cgit v1.2.3-54-g00ecf From c2596be1f10cb766b9bd06a0ab637468b90b07e4 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 16:32:55 +0200 Subject: Added more logging (will change INFO to DEBUG later) --- archinstall/lib/disk/btrfs.py | 3 +-- archinstall/lib/disk/helpers.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index cf60095f..bf24f88b 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -25,9 +25,8 @@ 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)") log(f"Mounting {location} as a subvolume", level=logging.INFO) - print(get_mount_info(installation.target/location, traverse=True)) # Mount the logical volume to the physical structure - mount_location = get_mount_info(installation.target/location)['source'] + mount_location = get_mount_info(installation.target/location, traverse=True)['source'] SysCommand(f"umount {mount_location}") return SysCommand(f"mount {mount_location} {installation.target}/{str(location)} -o subvol=@/{str(location)}").exit_code == 0 diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 8a6d5a48..78bf08ed 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -1,5 +1,6 @@ import re import json +import logging import pathlib from typing import Union from .blockdevice import BlockDevice @@ -119,6 +120,7 @@ def harddrive(size=None, model=None, fuzzy=False): def get_mount_info(path :Union[pathlib.Path, str], traverse=False) -> dict: for traversal in list(map(str, [str(path)] + list(pathlib.Path(str(path)).parents))): try: + log(f"Getting mount information at location {traversal}", level=logging.INFO) output = SysCommand(f'/usr/bin/findmnt --json {traversal}').decode('UTF-8') if output: break -- cgit v1.2.3-54-g00ecf From 4bc164cccc4c95d1f1e12e721aa61bb7299c1cef Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 16:57:51 +0200 Subject: Forgot to include {installation.target} in subvolume mounting --- archinstall/lib/disk/btrfs.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index bf24f88b..f682cd7c 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -18,13 +18,13 @@ def mount_subvolume(installation, location :Union[pathlib.Path, str], force=Fals if type(location) == str: location = pathlib.Path(location) - if not location.exists(): - location.mkdir(parents=True) + if not (installation.target/location).exists(): + (installation.target/location).mkdir(parents=True) if glob.glob(str(installation.target/location/'*')) and force is False: raise DiskError(f"Cannot mount subvolume to {installation.target/location} because it contains data (non-empty folder target)") - log(f"Mounting {location} as a subvolume", level=logging.INFO) + log(f"Mounting {installation.target/location} as a subvolume", level=logging.INFO) # Mount the logical volume to the physical structure mount_location = get_mount_info(installation.target/location, traverse=True)['source'] SysCommand(f"umount {mount_location}") -- cgit v1.2.3-54-g00ecf From 3ee1a5c18e4bf248b54e8da9be6a88cfe042a2b9 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 17:15:33 +0200 Subject: Tweaked get_mount_info() and mount_subvolume(). mount info now returns the path it found after traversal. mount_subvolume will no longer assume installation.target is of pathlib.Path and converts it if it isn't. --- archinstall/lib/disk/btrfs.py | 33 +++++++++++++++++++++------------ archinstall/lib/disk/helpers.py | 17 +++++++++++++---- 2 files changed, 34 insertions(+), 16 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index f682cd7c..a71fb1f4 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -6,29 +6,38 @@ from ..exceptions import DiskError from ..general import SysCommand from ..output import log -def mount_subvolume(installation, location :Union[pathlib.Path, str], force=False) -> bool: +def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], force=False) -> bool: """ This function uses mount to mount a subvolume on a given device, at a given location with a given subvolume name. @installation: archinstall.Installer instance - @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot + @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot @force: overrides the check for weither or not the subvolume mountpoint is empty or not """ + + installation_mountpoint = installation.target + if type(installation_mountpoint) == str: + installation_mountpoint = pathlib.Path(installation_mountpoint) # Set up the required physical structure - if type(location) == str: - location = pathlib.Path(location) + if type(subvolume_location) == str: + subvolume_location = pathlib.Path(subvolume_location) - if not (installation.target/location).exists(): - (installation.target/location).mkdir(parents=True) + target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) - if glob.glob(str(installation.target/location/'*')) and force is False: - raise DiskError(f"Cannot mount subvolume to {installation.target/location} because it contains data (non-empty folder target)") + if not (target).exists(): + (target).mkdir(parents=True) + + if glob.glob(str(target/'*')) and force is False: + raise DiskError(f"Cannot mount subvolume to {target} because it contains data (non-empty folder target)") - log(f"Mounting {installation.target/location} as a subvolume", level=logging.INFO) + log(f"Mounting {target} as a subvolume", level=logging.INFO) # Mount the logical volume to the physical structure - mount_location = get_mount_info(installation.target/location, traverse=True)['source'] - SysCommand(f"umount {mount_location}") - return SysCommand(f"mount {mount_location} {installation.target}/{str(location)} -o subvol=@/{str(location)}").exit_code == 0 + mountpoint_device, mountpoint_device_real_path = get_mount_info(target, traverse=True, return_real_path=True)['source'] + if mountpoint_device_real_path == str(target): + log(f"Unmounting non-subvolume {mountpoint_device} previously mounted at {target}") + SysCommand(f"umount {mountpoint_device}") + + return SysCommand(f"mount {mountpoint_device} {target} -o subvol=@{subvolume_location}").exit_code == 0 def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: """ diff --git a/archinstall/lib/disk/helpers.py b/archinstall/lib/disk/helpers.py index 78bf08ed..341b732f 100644 --- a/archinstall/lib/disk/helpers.py +++ b/archinstall/lib/disk/helpers.py @@ -117,7 +117,7 @@ def harddrive(size=None, model=None, fuzzy=False): return collection[drive] -def get_mount_info(path :Union[pathlib.Path, str], traverse=False) -> dict: +def get_mount_info(path :Union[pathlib.Path, str], traverse=False, return_real_path=False) -> dict: for traversal in list(map(str, [str(path)] + list(pathlib.Path(str(path)).parents))): try: log(f"Getting mount information at location {traversal}", level=logging.INFO) @@ -131,16 +131,25 @@ def get_mount_info(path :Union[pathlib.Path, str], traverse=False) -> dict: break if not output: - return {} + if return_real_path: + return {}, None + else: + return {} output = json.loads(output) if 'filesystems' in output: if len(output['filesystems']) > 1: raise DiskError(f"Path '{path}' contains multiple mountpoints: {output['filesystems']}") - return output['filesystems'][0] + if return_real_path: + return output['filesystems'][0], traversal + else: + return output['filesystems'][0] - return {} + if return_real_path: + return {}, traversal + else: + return {} def get_partitions_in_use(mountpoint) -> list: -- cgit v1.2.3-54-g00ecf From 9f9c637bfd2e9c8aff3e8fb0777db4e56d5ceedd Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 17:22:21 +0200 Subject: Fixes tuple issue --- archinstall/lib/disk/btrfs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index a71fb1f4..4fe890e5 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -32,12 +32,12 @@ def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], log(f"Mounting {target} as a subvolume", level=logging.INFO) # Mount the logical volume to the physical structure - mountpoint_device, mountpoint_device_real_path = get_mount_info(target, traverse=True, return_real_path=True)['source'] + mount_information, mountpoint_device_real_path = get_mount_info(target, traverse=True, return_real_path=True) if mountpoint_device_real_path == str(target): - log(f"Unmounting non-subvolume {mountpoint_device} previously mounted at {target}") - SysCommand(f"umount {mountpoint_device}") + log(f"Unmounting non-subvolume {mount_information['source']} previously mounted at {target}") + SysCommand(f"umount {mount_information['source']}") - return SysCommand(f"mount {mountpoint_device} {target} -o subvol=@{subvolume_location}").exit_code == 0 + return SysCommand(f"mount {mount_information['source']} {target} -o subvol=@{subvolume_location}").exit_code == 0 def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: """ -- cgit v1.2.3-54-g00ecf From dbebe8cf37c24e7c3bcb51f056c72b033706c033 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 17:46:25 +0200 Subject: Raising DiskError if subvolumes cannot be created. --- archinstall/lib/disk/btrfs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index 4fe890e5..eaa73af8 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -47,4 +47,5 @@ def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot """ log(f"Creating a subvolume on {installation.target}/{str(location)}", level=logging.INFO) - SysCommand(f"btrfs subvolume create {installation.target}/{str(location)}") \ No newline at end of file + if (cmd := SysCommand(f"btrfs subvolume create {installation.target}/{str(location)}")).exit_code != 0: + raise DiskError(f"Could not create a subvolume at {installation.target}/{str(location)}: {cmd}") \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 29a9fbddb27b00c98eae24782c5bec231e862f5c Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 17:50:24 +0200 Subject: Failed to create directory structure on subvolume create. Only on subvolume mount. This fixes that. --- archinstall/lib/disk/btrfs.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index eaa73af8..ff0b7b58 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -24,8 +24,8 @@ def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) - if not (target).exists(): - (target).mkdir(parents=True) + if not target.exists(): + target.mkdir(parents=True) if glob.glob(str(target/'*')) and force is False: raise DiskError(f"Cannot mount subvolume to {target} because it contains data (non-empty folder target)") @@ -39,13 +39,26 @@ def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], return SysCommand(f"mount {mount_information['source']} {target} -o subvol=@{subvolume_location}").exit_code == 0 -def create_subvolume(installation, location :Union[pathlib.Path, str]) -> bool: +def create_subvolume(installation, subvolume_location :Union[pathlib.Path, str]) -> bool: """ This function uses btrfs to create a subvolume. @installation: archinstall.Installer instance - @location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot + @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot """ - log(f"Creating a subvolume on {installation.target}/{str(location)}", level=logging.INFO) - if (cmd := SysCommand(f"btrfs subvolume create {installation.target}/{str(location)}")).exit_code != 0: - raise DiskError(f"Could not create a subvolume at {installation.target}/{str(location)}: {cmd}") \ No newline at end of file + + installation_mountpoint = installation.target + if type(installation_mountpoint) == str: + installation_mountpoint = pathlib.Path(installation_mountpoint) + # Set up the required physical structure + if type(subvolume_location) == str: + subvolume_location = pathlib.Path(subvolume_location) + + target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) + + if not target.exists(): + target.mkdir(parents=True) + + log(f"Creating a subvolume on {target}", level=logging.INFO) + if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: + raise DiskError(f"Could not create a subvolume at {target}: {cmd}") \ No newline at end of file -- cgit v1.2.3-54-g00ecf From 522ca2e41c566b73b9dde291252b047842c718a1 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 17:55:16 +0200 Subject: Adding error handling for paths and btrfs subvolume creation. --- archinstall/lib/disk/btrfs.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/btrfs.py b/archinstall/lib/disk/btrfs.py index ff0b7b58..6fafab34 100644 --- a/archinstall/lib/disk/btrfs.py +++ b/archinstall/lib/disk/btrfs.py @@ -56,8 +56,17 @@ def create_subvolume(installation, subvolume_location :Union[pathlib.Path, str]) target = installation_mountpoint / subvolume_location.relative_to(subvolume_location.anchor) - if not target.exists(): - target.mkdir(parents=True) + # Difference from mount_subvolume: + # We only check if the parent exists, since we'll run in to "target path already exists" otherwise + if not target.parent.exists(): + target.parent.mkdir(parents=True) + + if glob.glob(str(target/'*')) and force is False: + raise DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)") + + # Remove the target if it exists + if target.exists(): + target.rmdir() log(f"Creating a subvolume on {target}", level=logging.INFO) if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: -- cgit v1.2.3-54-g00ecf From 62f5bf4c8387f9f7591f1e1100067545b10f9b24 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Sat, 30 Oct 2021 20:26:54 +0200 Subject: Merging in parts of Master related to disk.py --- archinstall/lib/disk/blockdevice.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) (limited to 'archinstall/lib') diff --git a/archinstall/lib/disk/blockdevice.py b/archinstall/lib/disk/blockdevice.py index 57cbcfa6..d278fa2e 100644 --- a/archinstall/lib/disk/blockdevice.py +++ b/archinstall/lib/disk/blockdevice.py @@ -21,7 +21,7 @@ class BlockDevice: # I'm placing the encryption password on a BlockDevice level. def __repr__(self, *args, **kwargs): - return f"BlockDevice({self.device}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" + return f"BlockDevice({self.device_or_backfile}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" def __iter__(self): for partition in self.partitions: @@ -62,23 +62,33 @@ class BlockDevice: return device['pttype'] @property - def device(self): + def device_or_backfile(self): """ Returns the actual device-endpoint of the BlockDevice. If it's a loop-back-device it returns the back-file, - If it's a ATA-drive it returns the /dev/X device - And if it's a crypto-device it returns the parent device + For other types it return self.device """ - if "type" not in self.info: - raise DiskError(f'Could not locate backplane info for "{self.path}"') - if self.info['type'] == 'loop': for drive in json.loads(SysCommand(['losetup', '--json']).decode('UTF_8'))['loopdevices']: if not drive['name'] == self.path: continue return drive['back-file'] - elif self.info['type'] == 'disk': + else: + return self.device + + @property + def device(self): + """ + Returns the device file of the BlockDevice. + If it's a loop-back-device it returns the /dev/X device, + If it's a ATA-drive it returns the /dev/X device + And if it's a crypto-device it returns the parent device + """ + if "type" not in self.info: + raise DiskError(f'Could not locate backplane info for "{self.path}"') + + if self.info['type'] in ['disk','loop']: return self.path elif self.info['type'][:4] == 'raid': # This should catch /dev/md## raid devices -- cgit v1.2.3-54-g00ecf 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') 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-54-g00ecf