index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/lib/disk.py | 105 | ||||
-rw-r--r-- | archinstall/lib/exceptions.py | 5 | ||||
-rw-r--r-- | archinstall/lib/general.py | 11 | ||||
-rw-r--r-- | archinstall/lib/installer.py | 168 | ||||
-rw-r--r-- | archinstall/lib/luks.py | 35 | ||||
-rw-r--r-- | archinstall/lib/output.py | 12 | ||||
-rw-r--r-- | archinstall/lib/profiles.py | 21 | ||||
-rw-r--r-- | archinstall/lib/user_interaction.py | 84 |
diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index d05588a6..fbc11ca3 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -1,5 +1,5 @@ import glob, re, os, json, time, hashlib -import pathlib +import pathlib, traceback from collections import OrderedDict from .exceptions import DiskError from .general import * @@ -108,7 +108,7 @@ class BlockDevice(): 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, part_id=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)} @@ -130,16 +130,22 @@ class BlockDevice(): return True return False + def flush_cache(self): + self.part_cache = OrderedDict() + class Partition(): - def __init__(self, path, part_id=None, size=-1, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True): + 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 # A fail-safe for unconfigured partitions, such as windows NTFS partitions. @@ -175,28 +181,48 @@ class Partition(): elif self.target_mountpoint: mount_repr = f", rel_mountpoint={self.target_mountpoint}" - if self.encrypted: + if self._encrypted: return f'Partition(path={self.path}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})' else: return f'Partition(path={self.path}, fs={self.filesystem}{mount_repr})' @property + def encrypted(self): + return self._encrypted + + @encrypted.setter + def encrypted(self, value :bool): + if value: + log(f'Marking {self} as encrypted: {value}', level=LOG_LEVELS.Debug) + log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=LOG_LEVELS.Debug) + + self._encrypted = value + + @property def real_device(self): - if not self.encrypted: + if not self._encrypted: return self.path else: for blockdevice in json.loads(b''.join(sys_command('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}') + # 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=LOG_LEVELS.Info) from .luks import luks2 - with luks2(self, 'luksloop', password, auto_unmount=True) as unlocked_device: - return unlocked_device.filesystem + + try: + with luks2(self, 'luksloop', password, auto_unmount=True) as unlocked_device: + return unlocked_device.filesystem + except SysCallError: + return None def has_content(self): + if not get_filesystem_type(self.path): + return False + temporary_mountpoint = '/tmp/'+hashlib.md5(bytes(f"{time.time()}", 'UTF-8')+os.urandom(12)).hexdigest() temporary_path = pathlib.Path(temporary_mountpoint) @@ -213,8 +239,10 @@ class Partition(): def safe_to_format(self): if self.allow_formatting is False: + log(f"Partition {self} is not marked for formatting.", level=LOG_LEVELS.Debug) return False elif self.target_mountpoint == '/boot' and self.has_content(): + log(f"Partition {self} is a boot partition and has content inside.", level=LOG_LEVELS.Debug) return False return True @@ -225,10 +253,11 @@ class Partition(): """ from .luks import luks2 - if not self.encrypted: + if not self._encrypted: raise DiskError(f"Attempting to encrypt a partition that was not marked for encryption: {self}") if not self.safe_to_format(): + log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=LOG_LEVELS.Error, fg="red") return False handle = luks2(self, None, None) @@ -247,6 +276,11 @@ class Partition(): if allow_formatting is None: allow_formatting = self.allow_formatting + # 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 not allow_formatting: raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})") @@ -288,6 +322,12 @@ class Partition(): 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): @@ -313,6 +353,24 @@ class Partition(): self.mountpoint = target return True + def unmount(self): + try: + exit_code = sys_command(f'/usr/bin/umount {self.path}').exit_code + 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 exit_code > 0 and 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 @@ -343,7 +401,8 @@ class Filesystem(): if self.blockdevice.keep_partitions is False: log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=LOG_LEVELS.Debug) if self.mode == GPT: - if sys_command(f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt',).exit_code == 0: + if self.raw_parted(f'{self.blockdevice.device} mklabel gpt').exit_code == 0: + self.blockdevice.flush_cache() return self else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') @@ -380,7 +439,7 @@ class Filesystem(): def raw_parted(self, string:str): x = sys_command(f'/usr/bin/parted -s {string}') - o = b''.join(x) + log(f"'parted -s {string}' returned: {b''.join(x)}", level=LOG_LEVELS.Debug) return x def parted(self, string:str): @@ -392,25 +451,33 @@ class Filesystem(): """ return self.raw_parted(string).exit_code - def use_entire_disk(self, root_filesystem_type='ext4', encrypt_root_partition=True): - self.add_partition('primary', start='1MiB', end='513MiB', format='vfat') - #TODO: figure out what do for bios, we don't need a seprate partion for the bootloader + def use_entire_disk(self, root_filesystem_type='ext4'): + log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug) if hasUEFI(): + self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') self.set_name(0, 'EFI') self.set(0, 'boot on') - # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? - # https://www.gnu.org/software/parted/manual/html_node/set.html + # TODO: Probably redundant because in GPT mode 'esp on' is an alias for "boot on"? + # https://www.gnu.org/software/parted/manual/html_node/set.html self.set(0, 'esp on') self.add_partition('primary', start='513MiB', end='100%') self.blockdevice.partition[0].filesystem = 'vfat' self.blockdevice.partition[1].filesystem = root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) self.blockdevice.partition[0].target_mountpoint = '/boot' self.blockdevice.partition[1].target_mountpoint = '/' - if encrypt_root_partition: - self.blockdevice.partition[1].encrypted = True + self.blockdevice.partition[0].allow_formatting = True + self.blockdevice.partition[1].allow_formatting = True + else: + #we don't need a seprate boot partition it would be a waste of space + self.add_partition('primary', start='1MB', end='100%') + self.blockdevice.partition[0].filesystem=root_filesystem_type + log(f"Set the root partition {self.blockdevice.partition[0]} to use filesystem {root_filesystem_type}.", level=LOG_LEVELS.Debug) + self.blockdevice.partition[0].target_mountpoint = '/' + self.blockdevice.partition[0].allow_formatting = True def add_partition(self, type, start, end, format=None): log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info) @@ -507,4 +574,4 @@ def get_filesystem_type(path): handle = sys_command(f"blkid -o value -s TYPE {path}") return b''.join(handle).strip().decode('UTF-8') except SysCallError: - return None
\ No newline at end of file + return None diff --git a/archinstall/lib/exceptions.py b/archinstall/lib/exceptions.py index 5a5d47c6..a320eef6 100644 --- a/archinstall/lib/exceptions.py +++ b/archinstall/lib/exceptions.py @@ -7,7 +7,10 @@ class UnknownFilesystemFormat(BaseException): class ProfileError(BaseException): pass class SysCallError(BaseException): - pass + def __init__(self, message, exit_code): + super(SysCallError, self).__init__(message) + self.message = message + self.exit_code = exit_code class ProfileNotFound(BaseException): pass class HardwareIncompatibilityError(BaseException): diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index e87e4102..f2a714e7 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -105,8 +105,13 @@ class sys_command():#Thread): self.status = 'starting' user_catalogue = os.path.expanduser('~') - self.cwd = f"{user_catalogue}/.cache/archinstall/workers/{kwargs['worker_id']}/" - self.exec_dir = f'{self.cwd}/{os.path.basename(self.cmd[0])}_workingdir' + + if (workdir := kwargs.get('workdir', None)): + self.cwd = workdir + self.exec_dir = workdir + else: + self.cwd = f"{user_catalogue}/.cache/archinstall/workers/{kwargs['worker_id']}/" + self.exec_dir = f'{self.cwd}/{os.path.basename(self.cmd[0])}_workingdir' if not self.cmd[0][0] == '/': # "which" doesn't work as it's a builtin to bash. @@ -251,7 +256,7 @@ class sys_command():#Thread): if self.exit_code != 0 and not self.kwargs['suppress_errors']: #self.log(self.trace_log.decode('UTF-8'), level=LOG_LEVELS.Debug) #self.log(f"'{self.raw_cmd}' did not exit gracefully, exit code {self.exit_code}.", level=LOG_LEVELS.Error) - raise SysCallError(f"{self.trace_log.decode('UTF-8')}\n'{self.raw_cmd}' did not exit gracefully (trace log above), exit code: {self.exit_code}") + raise SysCallError(message=f"{self.trace_log.decode('UTF-8')}\n'{self.raw_cmd}' did not exit gracefully (trace log above), exit code: {self.exit_code}", exit_code=self.exit_code) self.ended = time.time() with open(f'{self.cwd}/trace.log', 'wb') as fh: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index e8829296..d161c3b7 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,4 +1,4 @@ -import os, stat, time, shutil, subprocess +import os, stat, time, shutil, pathlib from .exceptions import * from .disk import * @@ -9,7 +9,6 @@ from .mirrors import * from .systemd import Networkd from .output import log, LOG_LEVELS from .storage import storage -from .hardware import * class Installer(): """ @@ -54,8 +53,6 @@ class Installer(): } self.base_packages = base_packages.split(' ') - if not hasUEFI(): - base_packages.append('grub') # if it isn't uefi is must be bios therefore we need grub as systemd-boot is uefi only self.post_base_install = [] storage['session'] = self @@ -172,10 +169,19 @@ class Installer(): return True if sys_command(f'/usr/bin/arch-chroot {self.mountpoint} locale-gen').exit_code == 0 else False def set_timezone(self, zone, *args, **kwargs): - if not len(zone): return True + if not zone: return True + if not len(zone): return True # Redundant - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')) - return True + if (pathlib.Path("/usr")/"share"/"zoneinfo"/zone).exists(): + (pathlib.Path(self.mountpoint)/"etc"/"localtime").unlink(missing_ok=True) + sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{zone} /etc/localtime') + return True + else: + self.log( + f"Time zone {zone} does not exist, continuing with system default.", + level=LOG_LEVELS.Warning, + fg='red' + ) def activate_ntp(self): self.log(f'Installing and activating NTP.', level=LOG_LEVELS.Info) @@ -292,13 +298,22 @@ class Installer(): # TODO: Use python functions for this sys_command(f'/usr/bin/arch-chroot {self.mountpoint} chmod 700 /root') + # Configure mkinitcpio to handle some specific use cases. + # TODO: Yes, we should not overwrite the entire thing, but for now this should be fine + # since we just installed the base system. if self.partition.filesystem == 'btrfs': with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit: - ## TODO: Don't replace it, in case some update in the future actually adds something. mkinit.write('MODULES=(btrfs)\n') mkinit.write('BINARIES=(/usr/bin/btrfs)\n') mkinit.write('FILES=()\n') - mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keyboard fsck)\n') + mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n') + sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux') + elif self.partition.encrypted: + with open(f'{self.mountpoint}/etc/mkinitcpio.conf', 'w') as mkinit: + mkinit.write('MODULES=()\n') + mkinit.write('BINARIES=()\n') + mkinit.write('FILES=()\n') + mkinit.write('HOOKS=(base udev autodetect modconf block encrypt filesystems keymap keyboard fsck)\n') sys_command(f'/usr/bin/arch-chroot {self.mountpoint} mkinitcpio -p linux') self.helper_flags['base'] = True @@ -314,62 +329,75 @@ class Installer(): self.log(f'Adding bootloader {bootloader} to {self.boot_partition}', level=LOG_LEVELS.Info) if bootloader == 'systemd-bootctl': - if hasUEFI(): - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} bootctl --no-variables --path=/boot install')) - with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader: - loader.write('default arch\n') - loader.write('timeout 5\n') - - ## For some reason, blkid and /dev/disk/by-uuid are not getting along well. - ## And blkid is wrong in terms of LUKS. - #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip() - with open(f'{self.mountpoint}/boot/loader/entries/arch.conf', 'w') as entry: - entry.write('title Arch Linux\n') - entry.write('linux /vmlinuz-linux\n') - entry.write('initrd /initramfs-linux.img\n') - ## blkid doesn't trigger on loopback devices really well, - ## so we'll use the old manual method until we get that sorted out. - - - if self.partition.encrypted: - for root, folders, uids in os.walk('/dev/disk/by-uuid'): - for uid in uids: - real_path = os.path.realpath(os.path.join(root, uid)) - if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue - if hasAMDCPU(): # intel_paste is intel only, it's redudant on AMD systens - entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw\n') - else: - entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n') - - self.helper_flags['bootloader'] = bootloader - return True - break - else: - for root, folders, uids in os.walk('/dev/disk/by-partuuid'): - for uid in uids: - real_path = os.path.realpath(os.path.join(root, uid)) - if not os.path.basename(real_path) == os.path.basename(self.partition.path): continue - if hasAMDCPU(): - entry.write(f'options root=PARTUUID={uid} rw\n') - else: - entry.write(f'options root=PARTUUID={uid} rw intel_pstate=no_hwp\n') - - self.helper_flags['bootloader'] = bootloader - return True - break - raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.mountpoint}/boot/loader/entries/arch.conf will be broken until fixed.") - else: - raise RequirementError("Systemd-boot is UEFI only it can not be installed or used on bios") - elif bootloader == 'grub-install': - if hasUEFI(): - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=x86_64-efi --efi-directory=/boot --bootloader-id=GRUB')) - sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg') + # TODO: Ideally we would want to check if another config + # points towards the same disk and/or partition. + # And in which case we should do some clean up. + + # Install the boot loader + sys_command(f'/usr/bin/arch-chroot {self.mountpoint} bootctl --no-variables --path=/boot install') + + # Modify or create a loader.conf + if os.path.isfile(f'{self.mountpoint}/boot/loader/loader.conf'): + with open(f'{self.mountpoint}/boot/loader/loader.conf', 'r') as loader: + loader_data = loader.read().split('\n') else: - root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{self.partition.path.strip("/dev/")}/..")',shell=True).decode().strip() - if root_device == "block": - root_device = f"{self.partition.path}" - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=--target=i386-pc /dev/{root_device}')) - sys_command('/usr/bin/arch-chroot grub-mkconfig -o /boot/grub/grub.cfg') + loader_data = [ + f"default {self.init_time}", + f"timeout 5" + ] + + with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader: + for line in loader_data: + if line[:8] == 'default ': + loader.write(f'default {self.init_time}\n') + else: + loader.write(f"{line}") + + ## For some reason, blkid and /dev/disk/by-uuid are not getting along well. + ## And blkid is wrong in terms of LUKS. + #UUID = sys_command('blkid -s PARTUUID -o value {drive}{partition_2}'.format(**args)).decode('UTF-8').strip() + + # Setup the loader entry + with open(f'{self.mountpoint}/boot/loader/entries/{self.init_time}.conf', 'w') as entry: + entry.write(f'# Created by: archinstall\n') + entry.write(f'# Created on: {self.init_time}\n') + entry.write(f'title Arch Linux\n') + entry.write(f'linux /vmlinuz-linux\n') + entry.write(f'initrd /initramfs-linux.img\n') + ## blkid doesn't trigger on loopback devices really well, + ## so we'll use the old manual method until we get that sorted out. + + + if self.partition.encrypted: + log(f"Identifying root partition by DISK-UUID on {self.partition}, looking for '{os.path.basename(self.partition.real_device)}'.", level=LOG_LEVELS.Debug) + for root, folders, uids in os.walk('/dev/disk/by-uuid'): + for uid in uids: + real_path = os.path.realpath(os.path.join(root, uid)) + + log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.real_device)}: {os.path.basename(real_path) == os.path.basename(self.partition.real_device)}", level=LOG_LEVELS.Debug) + if not os.path.basename(real_path) == os.path.basename(self.partition.real_device): continue + + entry.write(f'options cryptdevice=UUID={uid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n') + + self.helper_flags['bootloader'] = bootloader + return True + break + else: + log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug) + for root, folders, uids in os.walk('/dev/disk/by-partuuid'): + for uid in uids: + real_path = os.path.realpath(os.path.join(root, uid)) + + log(f"Checking root partition match {os.path.basename(real_path)} against {os.path.basename(self.partition.path)}: {os.path.basename(real_path) == os.path.basename(self.partition.path)}", level=LOG_LEVELS.Debug) + if not os.path.basename(real_path) == os.path.basename(self.partition.path): continue + + entry.write(f'options root=PARTUUID={uid} rw intel_pstate=no_hwp\n') + + self.helper_flags['bootloader'] = bootloader + return True + break + + raise RequirementError(f"Could not identify the UUID of {self.partition}, there for {self.mountpoint}/boot/loader/entries/arch.conf will be broken until fixed.") else: raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") @@ -377,7 +405,17 @@ class Installer(): return self.pacstrap(*packages) def install_profile(self, profile): - profile = Profile(self, profile) + # TODO: Replace this with a import archinstall.session instead in the profiles. + # The tricky thing with doing the import archinstall.session instead is that + # profiles might be run from a different chroot, and there's no way we can + # guarantee file-path safety when accessing the installer object that way. + # Doing the __builtins__ replacement, ensures that the global vriable "installation" + # is always kept up to date. It's considered a nasty hack - but it's a safe way + # of ensuring 100% accuracy of archinstall session variables. + __builtins__['installation'] = self + + if type(profile) == str: + profile = Profile(self, profile) self.log(f'Installing network profile {profile}', level=LOG_LEVELS.Info) return profile.install() diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index e54641b8..19c21795 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -64,8 +64,37 @@ class luks2(): with open(key_file, 'wb') as fh: fh.write(password) - o = b''.join(sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}')) - if b'Command successful.' not in o: + try: + # Try to setup the crypt-device + cmd_handle = sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}') + except SysCallError as err: + if err.exit_code == 256: + log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=LOG_LEVELS.Debug) + # Partition was in use, unmount it and try again + partition.unmount() + + # Get crypt-information about the device by doing a reverse lookup starting with the partition path + # For instance: /dev/sda + devinfo = json.loads(b''.join(sys_command(f"lsblk --fs -J {partition.path}")).decode('UTF-8'))['blockdevices'][0] + + # For each child (sub-partition/sub-device) + if len(children := devinfo.get('children', [])): + for child in children: + # Unmount the child location + if child_mountpoint := child.get('mountpoint', None): + log(f'Unmounting {child_mountpoint}', level=LOG_LEVELS.Debug) + sys_command(f"umount -R {child_mountpoint}") + + # And close it if possible. + log(f"Closing crypt device {child['name']}", level=LOG_LEVELS.Debug) + sys_command(f"cryptsetup close {child['name']}") + + # Then try again to set up the crypt-device + cmd_handle = sys_command(f'/usr/bin/cryptsetup -q -v --type luks2 --pbkdf argon2i --hash {hash_type} --key-size {key_size} --iter-time {iter_time} --key-file {os.path.abspath(key_file)} --use-urandom luksFormat {partition.path}') + else: + raise err + + if b'Command successful.' not in b''.join(cmd_handle): raise DiskError(f'Could not encrypt volume "{partition.path}": {o}') return key_file @@ -84,7 +113,7 @@ class luks2(): sys_command(f'/usr/bin/cryptsetup open {partition.path} {mountpoint} --key-file {os.path.abspath(key_file)} --type luks2') if os.path.islink(f'/dev/mapper/{mountpoint}'): self.mapdev = f'/dev/mapper/{mountpoint}' - unlocked_partition = Partition(self.mapdev, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False) + unlocked_partition = Partition(self.mapdev, None, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False) unlocked_partition.allow_formatting = self.partition.allow_formatting return unlocked_partition diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 0e0a295b..537fb695 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -96,7 +96,17 @@ def log(*args, **kwargs): if (filename := storage.get('LOG_FILE', None)): absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) if not os.path.isfile(absolute_logfile): - os.makedirs(os.path.dirname(absolute_logfile)) + try: + Path(absolute_logfile).parents[0].mkdir(exist_ok=True, parents=True) + except PermissionError: + # Fallback to creating the log file in the current folder + err_string = f"Not enough permission to place log file at {absolute_logfile}, creating it in {Path('./').absolute()/filename} instead." + absolute_logfile = Path('./').absolute()/filename + absolute_logfile.parents[0].mkdir(exist_ok=True) + absolute_logfile = str(absolute_logfile) + storage['LOG_PATH'] = './' + log(err_string, fg="red") + Path(absolute_logfile).touch() # Overkill? with open(absolute_logfile, 'a') as log_file: diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 01c3288c..08b1d618 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -1,7 +1,7 @@ import os, urllib.request, urllib.parse, ssl, json, re import importlib.util, sys, glob, hashlib from collections import OrderedDict -from .general import multisplit, sys_command, log +from .general import multisplit, sys_command from .exceptions import * from .networking import * from .output import log, LOG_LEVELS @@ -76,6 +76,8 @@ class Script(): self.spec = None self.examples = None self.namespace = os.path.splitext(os.path.basename(self.path))[0] + self.original_namespace = self.namespace + log(f"Script {self} has been loaded with namespace '{self.namespace}'", level=LOG_LEVELS.Debug) def __enter__(self, *args, **kwargs): self.execute() @@ -131,14 +133,13 @@ class Script(): self.spec = importlib.util.spec_from_file_location(self.namespace, self.path) imported = importlib.util.module_from_spec(self.spec) sys.modules[self.namespace] = imported - + return self def execute(self): if not self.namespace in sys.modules or self.spec is None: self.load_instructions() - __builtins__['installation'] = self.installer # TODO: Replace this with a import archinstall.session instead self.spec.loader.exec_module(sys.modules[self.namespace]) return sys.modules[self.namespace] @@ -146,7 +147,6 @@ class Script(): class Profile(Script): def __init__(self, installer, path, args={}): super(Profile, self).__init__(path, installer) - self._cache = None def __dump__(self, *args, **kwargs): return {'path' : self.path} @@ -155,6 +155,10 @@ class Profile(Script): return f'Profile({os.path.basename(self.profile)})' def install(self): + # Before installing, revert any temporary changes to the namespace. + # This ensures that the namespace during installation is the original initation namespace. + # (For instance awesome instead of aweosme.py or app-awesome.py) + self.namespace = self.original_namespace return self.execute() def has_prep_function(self): @@ -202,4 +206,11 @@ class Application(Profile): elif parsed_url.scheme in ('https', 'http'): return self.localize_path(self.profile) else: - raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}")
\ No newline at end of file + raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}") + + def install(self): + # Before installing, revert any temporary changes to the namespace. + # This ensures that the namespace during installation is the original initation namespace. + # (For instance awesome instead of aweosme.py or app-awesome.py) + self.namespace = self.original_namespace + return self.execute()
\ No newline at end of file diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index 7e7f5873..80db7be1 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,4 +1,4 @@ -import getpass +import getpass, pathlib, os, shutil from .exceptions import * from .profiles import Profile from .locale_helpers import search_keyboard_layout @@ -9,15 +9,44 @@ from .networking import list_interfaces ## TODO: Some inconsistencies between the selection processes. ## Some return the keys from the options, some the values? +def get_terminal_height(): + return shutil.get_terminal_size().lines + +def get_terminal_width(): + return shutil.get_terminal_size().columns + +def get_longest_option(options): + return max([len(x) for x in options]) + def get_password(prompt="Enter a password: "): while (passwd := getpass.getpass(prompt)): passwd_verification = getpass.getpass(prompt='And one more time for verification: ') if passwd != passwd_verification: log(' * Passwords did not match * ', bg='black', fg='red') continue + + if len(passwd.strip()) <= 0: + break + return passwd return None +def print_large_list(options, padding=5, margin_bottom=0, separator=': '): + highest_index_number_length = len(str(len(options))) + longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding + max_num_of_columns = get_terminal_width() // longest_line + max_options_in_cells = max_num_of_columns * (get_terminal_height()-margin_bottom) + + if (len(options) > max_options_in_cells): + for index, option in enumerate(options): + print(f"{index}: {option}") + else: + for row in range(0, (get_terminal_height()-margin_bottom)): + for column in range(row, len(options), (get_terminal_height()-margin_bottom)): + spaces = " "*(longest_line - len(options[column])) + print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces) + print() + def ask_for_superuser_account(prompt='Create a required super-user with sudo privileges: ', forced=False): while 1: new_user = input(prompt).strip(' ') @@ -31,7 +60,7 @@ def ask_for_superuser_account(prompt='Create a required super-user with sudo pri raise UserError("No superuser was created.") password = get_password(prompt=f'Password for user {new_user}: ') - return {new_user: password} + return {new_user: {"!password" : password}} def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '): users = {} @@ -44,12 +73,23 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan password = get_password(prompt=f'Password for user {new_user}: ') if input("Should this user be a sudo (super) user (y/N): ").strip(' ').lower() in ('y', 'yes'): - super_users[new_user] = password + super_users[new_user] = {"!password" : password} else: - users[new_user] = password + users[new_user] = {"!password" : password} return users, super_users +def ask_for_a_timezone(): + timezone = input('Enter a valid timezone (Example: Europe/Stockholm): ').strip() + if (pathlib.Path("/usr")/"share"/"zoneinfo"/timezone).exists(): + return timezone + else: + log( + f"Time zone {timezone} does not exist, continuing with system default.", + level=LOG_LEVELS.Warning, + fg='red' + ) + def ask_to_configure_network(): # Optionally configure one network interface. #while 1: @@ -102,11 +142,10 @@ def ask_for_main_filesystem_format(): 'btrfs' : 'btrfs', 'ext4' : 'ext4', 'xfs' : 'xfs', - 'f2fs' : 'f2fs', - 'vfat' : 'vfat' + 'f2fs' : 'f2fs' } - value = generic_select(options.values(), "Select your main partitions filesystem by number or free-text: ") + value = generic_select(options.values(), "Select which filesystem your main partition should use (by number or name): ") return next((key for key, val in options.items() if val == value), None) def generic_select(options, input_text="Select one of the above by index or absolute value: ", sort=True): @@ -131,7 +170,10 @@ def generic_select(options, input_text="Select one of the above by index or abso if len(selected_option.strip()) <= 0: return None elif selected_option.isdigit(): - selected_option = options[int(selected_option)] + selected_option = int(selected_option) + if selected_option >= len(options): + raise RequirementError(f'Selected option "{selected_option}" is out of range') + selected_option = options[selected_option] elif selected_option in options: pass # We gave a correct absolute value else: @@ -156,7 +198,10 @@ def select_disk(dict_o_disks): print(f"{index}: {drive} ({dict_o_disks[drive]['size'], dict_o_disks[drive].device, dict_o_disks[drive]['label']})") drive = input('Select one of the above disks (by number or full path): ') if drive.isdigit(): - drive = dict_o_disks[drives[int(drive)]] + drive = int(drive) + if drive >= len(drives): + raise DiskError(f'Selected option "{drive}" is out of range') + drive = dict_o_disks[drives[drive]] elif drive in dict_o_disks: drive = dict_o_disks[drive] else: @@ -182,10 +227,10 @@ def select_profile(options): for index, profile in enumerate(profiles): print(f"{index}: {profile}") - print(' -- The above list is pre-programmed profiles. --') + print(' -- The above list is a set of pre-programmed profiles. --') print(' -- They might make it easier to install things like desktop environments. --') - print(' -- (Leave blank to skip this next optional step) --') - selected_profile = input('Any particular pre-programmed profile you want to install: ') + print(' -- (Leave blank and hit enter to skip this step and continue) --') + selected_profile = input('Enter a pre-programmed profile name if you want to install one: ') if len(selected_profile.strip()) <= 0: return None @@ -265,24 +310,13 @@ def select_mirror_regions(mirrors, show_top_mirrors=True): selected_mirrors = {} if len(regions) >= 1: - for index, region in enumerate(regions): - print(f"{index}: {region}") + print_large_list(regions, margin_bottom=2) - print(' -- You can enter ? or help to search for more regions --') print(' -- You can skip this step by leaving the option blank --') - print(' -- (You can use Shift + PageUp to scroll in the list --') selected_mirror = input('Select one of the above regions to download packages from (by number or full name): ') if len(selected_mirror.strip()) == 0: return {} - elif selected_mirror.lower() in ('?', 'help'): - filter_string = input('Search for a region containing (example: "united"): ').strip().lower() - for region in mirrors: - if filter_string in region.lower(): - selected_mirrors[region] = mirrors[region] - - return selected_mirrors - elif selected_mirror.isdigit() and (pos := int(selected_mirror)) <= len(regions)-1: region = regions[int(selected_mirror)] selected_mirrors[region] = mirrors[region] @@ -298,4 +332,4 @@ def select_mirror_regions(mirrors, show_top_mirrors=True): return selected_mirrors - raise RequirementError("Selecting mirror region require a least one region to be given as an option.")
\ No newline at end of file + raise RequirementError("Selecting mirror region require a least one region to be given as an option.") |