index : archinstall32 | |
Archlinux32 installer | gitolite user |
summaryrefslogtreecommitdiff |
-rw-r--r-- | archinstall/__init__.py | 33 | ||||
-rw-r--r-- | archinstall/__main__.py | 30 | ||||
-rw-r--r-- | archinstall/lib/disk.py | 68 | ||||
-rw-r--r-- | archinstall/lib/installer.py | 221 | ||||
-rw-r--r-- | archinstall/lib/luks.py | 11 | ||||
-rw-r--r-- | archinstall/lib/profiles.py | 46 | ||||
-rw-r--r-- | archinstall/lib/user_interaction.py | 74 |
diff --git a/archinstall/__init__.py b/archinstall/__init__.py index d4452d38..d98b6daa 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -14,6 +14,8 @@ from .lib.output import * from .lib.storage import * from .lib.hardware import * +__version__ = "2.1.3" + ## Basic version of arg.parse() supporting: ## --key=value ## --boolean @@ -27,4 +29,33 @@ for arg in sys.argv[1:]: key, val = arg[2:], True arguments[key] = val else: - positionals.append(arg)
\ No newline at end of file + positionals.append(arg) + + +# TODO: Learn the dark arts of argparse... +# (I summon thee dark spawn of cPython) + +def run_as_a_module(): + """ + Since we're running this as a 'python -m archinstall' module OR + a nuitka3 compiled version of the project. + This function and the file __main__ acts as a entry point. + """ + + # Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc. + storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples')) + + if len(sys.argv) == 1: + sys.argv.append('guided') + + try: + script = Script(sys.argv[1]) + except ProfileNotFound as err: + print(f"Couldn't find file: {err}") + sys.exit(1) + + os.chdir(os.path.abspath(os.path.dirname(__file__))) + + # Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions. + storage['PROFILE_PATH'].pop() + script.execute() diff --git a/archinstall/__main__.py b/archinstall/__main__.py index 63c2f715..86ed0108 100644 --- a/archinstall/__main__.py +++ b/archinstall/__main__.py @@ -2,33 +2,5 @@ import archinstall import sys import os -# TODO: Learn the dark arts of argparse... -# (I summon thee dark spawn of cPython) - -def run_as_a_module(): - """ - Since we're running this as a 'python -m archinstall' module OR - a nuitka3 compiled version of the project. - This function and the file __main__ acts as a entry point. - """ - - # Add another path for finding profiles, so that list_profiles() in Script() can find guided.py, unattended.py etc. - archinstall.storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples')) - - if len(sys.argv) == 1: - sys.argv.append('guided') - - try: - script = archinstall.Script(sys.argv[1]) - except archinstall.ProfileNotFound as err: - print(f"Couldn't find file: {err}") - sys.exit(1) - - os.chdir(os.path.abspath(os.path.dirname(__file__))) - - # Remove the example directory from the PROFILE_PATH, to avoid guided.py etc shows up in user input questions. - archinstall.storage['PROFILE_PATH'].pop() - script.execute() - if __name__ == '__main__': - run_as_a_module() + archinstall.run_as_a_module() diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 0608b47b..bada4076 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -24,6 +24,7 @@ class BlockDevice(): self.path = path self.info = info + self.keep_partitions = True self.part_cache = OrderedDict() # TODO: Currently disk encryption is a BIT misleading. # It's actually partition-encryption, but for future-proofing this @@ -125,6 +126,18 @@ class BlockDevice(): def partition_table_type(self): return GPT + @property + def uuid(self): + log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.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. + """ + lsblk = b''.join(sys_command(f'lsblk -J -o+UUID {self.path}')) + for partition in json.loads(lsblk.decode('UTF-8'))['blockdevices']: + return partition.get('uuid', None) + def has_partitions(self): return len(self.partitions) @@ -165,7 +178,7 @@ class Partition(): self.mountpoint = target if not self.filesystem and autodetect_filesystem: - if (fstype := mount_information.get('fstype', get_filesystem_type(self.real_device))): + if (fstype := mount_information.get('fstype', get_filesystem_type(path))): self.filesystem = fstype if self.filesystem == 'crypto_LUKS': @@ -186,9 +199,9 @@ class Partition(): mount_repr = f", rel_mountpoint={self.target_mountpoint}" if self._encrypted: - return f'Partition(path={self.path}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})' + return f'Partition(path={self.path}, size={self.size}, real_device={self.real_device}, fs={self.filesystem}{mount_repr})' else: - return f'Partition(path={self.path}, fs={self.filesystem}{mount_repr})' + return f'Partition(path={self.path}, size={self.size}, fs={self.filesystem}{mount_repr})' @property def uuid(self) -> str: @@ -214,13 +227,14 @@ class Partition(): self._encrypted = value @property + def parent(self): + return self.real_device + + @property def real_device(self): - 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}" + 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}') return self.path @@ -365,14 +379,16 @@ class Partition(): if not fs: if not self.filesystem: raise DiskError(f'Need to format (or define) the filesystem on {self} before mounting.') fs = self.filesystem - ## libc has some issues with loop devices, defaulting back to sys calls - # ret = libc.mount(self.path.encode(), target.encode(), fs.encode(), 0, options.encode()) - # if ret < 0: - # errno = ctypes.get_errno() - # raise OSError(errno, f"Error mounting {self.path} ({fs}) on {target} with options '{options}': {os.strerror(errno)}") - if sys_command(f'/usr/bin/mount {self.path} {target}').exit_code == 0: - self.mountpoint = target - return True + + pathlib.Path(target).mkdir(parents=True, exist_ok=True) + + try: + sys_command(f'/usr/bin/mount {self.path} {target}') + except SysCallError as err: + raise err + + self.mountpoint = target + return True def unmount(self): try: @@ -571,6 +587,24 @@ def get_mount_info(path): return output['filesystems'][0] +def get_partitions_in_use(mountpoint): + try: + output = b''.join(sys_command(f'/usr/bin/findmnt --json -R {mountpoint}')) + except SysCallError: + return {} + + mounts = [] + + output = output.decode('UTF-8') + 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: handle = sys_command(f"blkid -o value -s TYPE {path}") diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index 49716ac5..4ff9e80a 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -34,30 +34,21 @@ class Installer(): :type hostname: str, optional """ - def __init__(self, partition, boot_partition, *, base_packages='base base-devel linux linux-firmware efibootmgr nano', profile=None, mountpoint='/mnt', hostname='ArchInstalled', logdir=None, logfile=None): - self.profile = profile - self.hostname = hostname - self.mountpoint = mountpoint + def __init__(self, target, *, base_packages='base base-devel linux linux-firmware efibootmgr'): + self.target = target self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.milliseconds = int(str(time.time()).split('.')[1]) - if logdir: - storage['LOG_PATH'] = logdir - if logfile: - storage['LOG_FILE'] = logfile - self.helper_flags = { - 'bootloader' : False, 'base' : False, - 'user' : False # Root counts as a user, if additional users are skipped. + 'bootloader' : False } - self.base_packages = base_packages.split(' ') + self.base_packages = base_packages.split(' ') if type(base_packages) is str else base_packages self.post_base_install = [] - storage['session'] = self - self.partition = partition - self.boot_partition = boot_partition + storage['session'] = self + self.partitions = get_partitions_in_use(self.target) def log(self, *args, level=LOG_LEVELS.Debug, **kwargs): """ @@ -67,9 +58,6 @@ class Installer(): log(*args, level=level, **kwargs) def __enter__(self, *args, **kwargs): - self.partition.mount(self.mountpoint) - os.makedirs(f'{self.mountpoint}/boot', exist_ok=True) - self.boot_partition.mount(f'{self.mountpoint}/boot') return self def __exit__(self, *args, **kwargs): @@ -98,8 +86,10 @@ class Installer(): self.log('Some required steps were not successfully installed/configured before leaving the installer:', bg='black', fg='red', level=LOG_LEVELS.Warning) for step in missing_steps: self.log(f' - {step}', bg='black', fg='red', level=LOG_LEVELS.Warning) - self.log(f"Detailed error logs can be found at: {log_path}", level=LOG_LEVELS.Warning) + + self.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=LOG_LEVELS.Warning) self.log(f"Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=LOG_LEVELS.Warning) + self.sync_log_to_install_medium() return False @@ -110,18 +100,18 @@ class Installer(): if (filename := storage.get('LOG_FILE', None)): absolute_logfile = os.path.join(storage.get('LOG_PATH', './'), filename) - if not os.path.isdir(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}"): - os.makedirs(f"{self.mountpoint}/{os.path.dirname(absolute_logfile)}") + if not os.path.isdir(f"{self.target}/{os.path.dirname(absolute_logfile)}"): + os.makedirs(f"{self.target}/{os.path.dirname(absolute_logfile)}") - shutil.copy2(absolute_logfile, f"{self.mountpoint}/{absolute_logfile}") + shutil.copy2(absolute_logfile, f"{self.target}/{absolute_logfile}") return True def mount(self, partition, mountpoint, create_mountpoint=True): - if create_mountpoint and not os.path.isdir(f'{self.mountpoint}{mountpoint}'): - os.makedirs(f'{self.mountpoint}{mountpoint}') + if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'): + os.makedirs(f'{self.target}{mountpoint}') - partition.mount(f'{self.mountpoint}{mountpoint}') + partition.mount(f'{self.target}{mountpoint}') def post_install_check(self, *args, **kwargs): return [step for step, flag in self.helper_flags.items() if flag is False] @@ -131,7 +121,7 @@ class Installer(): self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info) if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).exit_code == 0: - if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.mountpoint} {" ".join(packages)}', **kwargs)).exit_code == 0: + if (pacstrap := sys_command(f'/usr/bin/pacstrap {self.target} {" ".join(packages)}', **kwargs)).exit_code == 0: return True else: self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=LOG_LEVELS.Info) @@ -139,42 +129,41 @@ class Installer(): self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info) def set_mirrors(self, mirrors): - return use_mirrors(mirrors, destination=f'{self.mountpoint}/etc/pacman.d/mirrorlist') + return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') def genfstab(self, flags='-pU'): - self.log(f"Updating {self.mountpoint}/etc/fstab", level=LOG_LEVELS.Info) + self.log(f"Updating {self.target}/etc/fstab", level=LOG_LEVELS.Info) - fstab = sys_command(f'/usr/bin/genfstab {flags} {self.mountpoint}').trace_log - with open(f"{self.mountpoint}/etc/fstab", 'ab') as fstab_fh: + fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log + with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh: fstab_fh.write(fstab) - if not os.path.isfile(f'{self.mountpoint}/etc/fstab'): - raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{o}') + if not os.path.isfile(f'{self.target}/etc/fstab'): + raise RequirementError(f'Could not generate fstab, strapping in packages most likely failed (disk out of space?)\n{fstab}') return True - def set_hostname(self, hostname=None, *args, **kwargs): - if not hostname: hostname = self.hostname - with open(f'{self.mountpoint}/etc/hostname', 'w') as fh: - fh.write(self.hostname + '\n') + def set_hostname(self, hostname :str, *args, **kwargs): + with open(f'{self.target}/etc/hostname', 'w') as fh: + fh.write(hostname + '\n') def set_locale(self, locale, encoding='UTF-8', *args, **kwargs): if not len(locale): return True - with open(f'{self.mountpoint}/etc/locale.gen', 'a') as fh: + with open(f'{self.target}/etc/locale.gen', 'a') as fh: fh.write(f'{locale}.{encoding} {encoding}\n') - with open(f'{self.mountpoint}/etc/locale.conf', 'w') as fh: + with open(f'{self.target}/etc/locale.conf', 'w') as fh: fh.write(f'LANG={locale}.{encoding}\n') - return True if sys_command(f'/usr/bin/arch-chroot {self.mountpoint} locale-gen').exit_code == 0 else False + return True if sys_command(f'/usr/bin/arch-chroot {self.target} locale-gen').exit_code == 0 else False def set_timezone(self, zone, *args, **kwargs): if not zone: return True if not len(zone): return True # Redundant 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') + (pathlib.Path(self.target)/"etc"/"localtime").unlink(missing_ok=True) + sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime') return True else: self.log( @@ -196,7 +185,7 @@ class Installer(): raise ServiceException(f"Unable to start service {service}: {output}") def run_command(self, cmd, *args, **kwargs): - return sys_command(f'/usr/bin/arch-chroot {self.mountpoint} {cmd}') + return sys_command(f'/usr/bin/arch-chroot {self.target} {cmd}') def arch_chroot(self, cmd, *args, **kwargs): return self.run_command(cmd) @@ -216,15 +205,15 @@ class Installer(): conf = Networkd(Match={"Name": nic}, Network=network) - with open(f"{self.mountpoint}/etc/systemd/network/10-{nic}.network", "a") as netconf: + with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf: netconf.write(str(conf)) def copy_ISO_network_config(self, enable_services=False): # Copy (if any) iwd password and config files if os.path.isdir('/var/lib/iwd/'): if (psk_files := glob.glob('/var/lib/iwd/*.psk')): - if not os.path.isdir(f"{self.mountpoint}/var/lib/iwd"): - os.makedirs(f"{self.mountpoint}/var/lib/iwd") + if not os.path.isdir(f"{self.target}/var/lib/iwd"): + os.makedirs(f"{self.target}/var/lib/iwd") if enable_services: # If we haven't installed the base yet (function called pre-maturely) @@ -244,15 +233,15 @@ class Installer(): self.enable_service('iwd') for psk in psk_files: - shutil.copy2(psk, f"{self.mountpoint}/var/lib/iwd/{os.path.basename(psk)}") + shutil.copy2(psk, f"{self.target}/var/lib/iwd/{os.path.basename(psk)}") # Copy (if any) systemd-networkd config files if (netconfigurations := glob.glob('/etc/systemd/network/*')): - if not os.path.isdir(f"{self.mountpoint}/etc/systemd/network/"): - os.makedirs(f"{self.mountpoint}/etc/systemd/network/") + if not os.path.isdir(f"{self.target}/etc/systemd/network/"): + os.makedirs(f"{self.target}/etc/systemd/network/") for netconf_file in netconfigurations: - shutil.copy2(netconf_file, f"{self.mountpoint}/etc/systemd/network/{os.path.basename(netconf_file)}") + shutil.copy2(netconf_file, f"{self.target}/etc/systemd/network/{os.path.basename(netconf_file)}") if enable_services: # If we haven't installed the base yet (function called pre-maturely) @@ -267,54 +256,69 @@ class Installer(): return True + def detect_encryption(self, partition): + if partition.encrypted: + return partition + elif partition.parent not in partition.path and Partition(partition.parent, None, autodetect_filesystem=True).filesystem == 'crypto_LUKS': + return Partition(partition.parent, None, autodetect_filesystem=True) + + return False + def minimal_installation(self): ## Add necessary packages if encrypting the drive ## (encrypted partitions default to btrfs for now, so we need btrfs-progs) ## TODO: Perhaps this should be living in the function which dictates ## the partitioning. Leaving here for now. - if self.partition.filesystem == 'btrfs': - #if self.partition.encrypted: - self.base_packages.append('btrfs-progs') - if self.partition.filesystem == 'xfs': - self.base_packages.append('xfsprogs') - if self.partition.filesystem == 'f2fs': - self.base_packages.append('f2fs-tools') + MODULES = [] + BINARIES = [] + FILES = [] + HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"] + + for partition in self.partitions: + if partition.filesystem == 'btrfs': + #if partition.encrypted: + self.base_packages.append('btrfs-progs') + if partition.filesystem == 'xfs': + self.base_packages.append('xfsprogs') + if partition.filesystem == 'f2fs': + self.base_packages.append('f2fs-tools') + + # Configure mkinitcpio to handle some specific use cases. + if partition.filesystem == 'btrfs': + if 'btrfs' not in MODULES: + MODULES.append('btrfs') + if '/usr/bin/btrfs-progs' not in BINARIES: + BINARIES.append('/usr/bin/btrfs') + + if self.detect_encryption(partition): + if 'encrypt' not in HOOKS: + HOOKS.insert(HOOKS.index('filesystems'), 'encrypt') + self.pacstrap(self.base_packages) self.helper_flags['base-strapped'] = True #self.genfstab() - with open(f"{self.mountpoint}/etc/fstab", "a") as fstab: + with open(f"{self.target}/etc/fstab", "a") as fstab: fstab.write( "\ntmpfs /tmp tmpfs defaults,noatime,mode=1777 0 0\n" ) # Redundant \n at the start? who knows? ## TODO: Support locale and timezone - #os.remove(f'{self.mountpoint}/etc/localtime') - #sys_command(f'/usr/bin/arch-chroot {self.mountpoint} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime') + #os.remove(f'{self.target}/etc/localtime') + #sys_command(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{localtime} /etc/localtime') #sys_command('/usr/bin/arch-chroot /mnt hwclock --hctosys --localtime') - self.set_hostname() + self.set_hostname('archinstall') self.set_locale('en_US') # 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: - 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 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') + sys_command(f'/usr/bin/arch-chroot {self.target} chmod 700 /root') + + with open(f'{self.target}/etc/mkinitcpio.conf', 'w') as mkinit: + mkinit.write(f"MODULES=({' '.join(MODULES)})\n") + mkinit.write(f"BINARIES=({' '.join(BINARIES)})\n") + mkinit.write(f"FILES=({' '.join(FILES)})\n") + mkinit.write(f"HOOKS=({' '.join(HOOKS)})\n") + sys_command(f'/usr/bin/arch-chroot {self.target} mkinitcpio -p linux') self.helper_flags['base'] = True @@ -326,7 +330,15 @@ class Installer(): return True def add_bootloader(self, bootloader='systemd-bootctl'): - self.log(f'Adding bootloader {bootloader} to {self.boot_partition}', level=LOG_LEVELS.Info) + boot_partition = None + root_partition = None + for partition in self.partitions: + if partition.mountpoint == self.target+'/boot': + boot_partition = partition + elif partition.mountpoint == self.target: + root_partition = partition + + self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info) if bootloader == 'systemd-bootctl': # TODO: Ideally we would want to check if another config @@ -334,11 +346,11 @@ class Installer(): # 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') + sys_command(f'/usr/bin/arch-chroot {self.target} 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: + if os.path.isfile(f'{self.target}/boot/loader/loader.conf'): + with open(f'{self.target}/boot/loader/loader.conf', 'r') as loader: loader_data = loader.read().split('\n') else: loader_data = [ @@ -346,7 +358,7 @@ class Installer(): f"timeout 5" ] - with open(f'{self.mountpoint}/boot/loader/loader.conf', 'w') as loader: + with open(f'{self.target}/boot/loader/loader.conf', 'w') as loader: for line in loader_data: if line[:8] == 'default ': loader.write(f'default {self.init_time}\n') @@ -358,7 +370,7 @@ class Installer(): #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: + with open(f'{self.target}/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') @@ -368,28 +380,19 @@ class Installer(): ## 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 + if (real_device := self.detect_encryption(root_partition)): + # TODO: We need to detect if the encrypted device is a whole disk encryption, + # or simply a partition encryption. Right now we assume it's a partition (and we always have) + log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=LOG_LEVELS.Debug) + entry.write(f'options cryptdevice=PARTUUID={real_device.uuid}:luksdev root=/dev/mapper/luksdev rw intel_pstate=no_hwp\n') else: - log(f"Identifying root partition by PART-UUID on {self.partition}, looking for '{os.path.basename(self.partition.path)}'.", level=LOG_LEVELS.Debug) - entry.write(f'options root=PARTUUID={self.partition.uuid} rw intel_pstate=no_hwp\n') + log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=LOG_LEVELS.Debug) + entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n') - self.helper_flags['bootloader'] = bootloader - return True + self.helper_flags['bootloader'] = bootloader + return True - 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.") + raise RequirementError(f"Could not identify the UUID of {root_partition}, there for {self.target}/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}") @@ -414,19 +417,19 @@ class Installer(): def enable_sudo(self, entity :str, group=False): self.log(f'Enabling sudo permissions for {entity}.', level=LOG_LEVELS.Info) - with open(f'{self.mountpoint}/etc/sudoers', 'a') as sudoers: + with open(f'{self.target}/etc/sudoers', 'a') as sudoers: sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n') return True def user_create(self, user :str, password=None, groups=[], sudo=False): self.log(f'Creating user {user}', level=LOG_LEVELS.Info) - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} useradd -m -G wheel {user}')) + o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}')) if password: self.user_set_pw(user, password) if groups: for group in groups: - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} gpasswd -a {user} {group}')) + o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.target} gpasswd -a {user} {group}')) if sudo and self.enable_sudo(user): self.helper_flags['user'] = True @@ -438,12 +441,12 @@ class Installer(): # This means the root account isn't locked/disabled with * in /etc/passwd self.helper_flags['user'] = True - o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.mountpoint} sh -c \"echo '{user}:{password}' | chpasswd\"")) + o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\"")) pass def set_keyboard_language(self, language): if len(language.strip()): - with open(f'{self.mountpoint}/etc/vconsole.conf', 'w') as vconsole: + with open(f'{self.target}/etc/vconsole.conf', 'w') as vconsole: vconsole.write(f'KEYMAP={language}\n') vconsole.write(f'FONT=lat9w-16\n') return True diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 62067ec1..ca077b3d 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -1,5 +1,7 @@ import os import shlex +import time +import pathlib from .exceptions import * from .general import * from .disk import Partition @@ -43,8 +45,6 @@ class luks2(): return True def encrypt(self, partition, password=None, key_size=512, hash_type='sha512', iter_time=10000, key_file=None): - # TODO: We should be able to integrate this into the main log some how. - # Perhaps post-mortem? if not self.partition.allow_formatting: raise DiskError(f'Could not encrypt volume {self.partition} due to it having a formatting lock.') @@ -116,7 +116,7 @@ class luks2(): def unlock(self, partition, mountpoint, key_file): """ - Mounts a lukts2 compatible partition to a certain mountpoint. + Mounts a luks2 compatible partition to a certain mountpoint. Keyfile must be specified as there's no way to interact with the pw-prompt atm. :param mountpoint: The name without absolute path, for instance "luksdev" will point to /dev/mapper/luksdev @@ -125,6 +125,11 @@ class luks2(): from .disk import get_filesystem_type if '/' in mountpoint: os.path.basename(mountpoint) # TODO: Raise exception instead? + + wait_timer = time.time() + while pathlib.Path(partition.path).exists() is False and time.time() - wait_timer < 10: + time.sleep(0.025) + 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}' diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 4ef6c533..21ec5f6f 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -177,6 +177,52 @@ class Profile(Script): if hasattr(imported, '_prep_function'): return True return False + """ + def has_post_install(self): + with open(self.path, 'r') as source: + source_data = source.read() + + # Some crude safety checks, make sure the imported profile has + # a __name__ check and if so, check if it's got a _prep_function() + # we can call to ask for more user input. + # + # If the requirements are met, import with .py in the namespace to not + # trigger a traditional: + # if __name__ == 'moduleName' + if '__name__' in source_data and '_post_install' in source_data: + with self.load_instructions(namespace=f"{self.namespace}.py") as imported: + if hasattr(imported, '_post_install'): + return True + """ + + def is_top_level_profile(self): + with open(self.path, 'r') as source: + source_data = source.read() + + # TODO: I imagine that there is probably a better way to write this. + return 'top_level_profile = True' in source_data + + @property + def packages(self) -> list: + """ + Returns a list of packages baked into the profile definition. + If no package definition has been done, .packages() will return None. + """ + with open(self.path, 'r') as source: + source_data = source.read() + + # Some crude safety checks, make sure the imported profile has + # a __name__ check before importing. + # + # If the requirements are met, import with .py in the namespace to not + # trigger a traditional: + # if __name__ == 'moduleName' + if '__name__' in source_data and '__packages__' in source_data: + with self.load_instructions(namespace=f"{self.namespace}.py") as imported: + if hasattr(imported, '__packages__'): + return imported.__packages__ + return None + class Application(Profile): def __repr__(self, *args, **kwargs): diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index e7243a25..16627794 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,4 +1,5 @@ -import getpass, pathlib, os, shutil +import getpass, pathlib, os, shutil, re +import sys, time, signal from .exceptions import * from .profiles import Profile from .locale_helpers import search_keyboard_layout @@ -18,6 +19,51 @@ def get_terminal_width(): def get_longest_option(options): return max([len(x) for x in options]) +def check_for_correct_username(username): + if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: + return True + log( + "The username you entered is invalid. Try again", + level=LOG_LEVELS.Warning, + fg='red' + ) + return False + +def do_countdown(): + SIG_TRIGGER = False + def kill_handler(sig, frame): + print() + exit(0) + + def sig_handler(sig, frame): + global SIG_TRIGGER + SIG_TRIGGER = True + signal.signal(signal.SIGINT, kill_handler) + + original_sigint_handler = signal.getsignal(signal.SIGINT) + signal.signal(signal.SIGINT, sig_handler) + + for i in range(5, 0, -1): + print(f"{i}", end='') + + for x in range(4): + sys.stdout.flush() + time.sleep(0.25) + print(".", end='') + + if SIG_TRIGGER: + abort = input('\nDo you really want to abort (y/n)? ') + if abort.strip() != 'n': + exit(0) + + if SIG_TRIGGER is False: + sys.stdin.read() + SIG_TRIGGER = False + signal.signal(signal.SIGINT, sig_handler) + print() + signal.signal(signal.SIGINT, original_sigint_handler) + return True + def get_password(prompt="Enter a password: "): while (passwd := getpass.getpass(prompt)): passwd_verification = getpass.getpass(prompt='And one more time for verification: ') @@ -50,7 +96,7 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '): def ask_for_superuser_account(prompt='Create a required super-user with sudo privileges: ', forced=False): while 1: new_user = input(prompt).strip(' ') - + if not new_user and forced: # TODO: make this text more generic? # It's only used to create the first sudo user when root is disabled in guided.py @@ -58,6 +104,8 @@ def ask_for_superuser_account(prompt='Create a required super-user with sudo pri continue elif not new_user and not forced: raise UserError("No superuser was created.") + elif not check_for_correct_username(new_user): + continue password = get_password(prompt=f'Password for user {new_user}: ') return {new_user: {"!password" : password}} @@ -70,6 +118,8 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan new_user = input(prompt).strip(' ') if not new_user: break + if not check_for_correct_username(new_user): + continue 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'): @@ -89,15 +139,25 @@ def ask_for_a_timezone(): level=LOG_LEVELS.Warning, fg='red' ) + +def ask_for_audio_selection(): + audio = "pulseaudio" # Default for most desktop environments + pipewire_choice = input("Would you like to install pipewire instead of pulseaudio as the default audio server? [Y/n] ").lower() + if pipewire_choice in ("y", ""): + audio = "pipewire" + + return audio def ask_to_configure_network(): # Optionally configure one network interface. #while 1: # {MAC: Ifname} - interfaces = {'ISO-CONFIG' : 'Copy ISO network configuration to installation', **list_interfaces()} + interfaces = {'ISO-CONFIG' : 'Copy ISO network configuration to installation','NetworkManager':'Use NetworkManager to control and manage your internet connection', **list_interfaces()} nic = generic_select(interfaces.values(), "Select one network interface to configure (leave blank to skip): ") if nic and nic != 'Copy ISO network configuration to installation': + if nic == 'Use NetworkManager to control and manage your internet connection': + return {'nic': nic,'NetworkManager':True} mode = generic_select(['DHCP (auto detect)', 'IP (static)'], f"Select which mode to configure for {nic}: ") if mode == 'IP (static)': while 1: @@ -170,7 +230,7 @@ def generic_select(options, input_text="Select one of the above by index or abso return None elif selected_option.isdigit(): selected_option = int(selected_option) - if selected_option >= len(options): + 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: @@ -195,8 +255,10 @@ def select_disk(dict_o_disks): if len(drives) >= 1: for index, drive in enumerate(drives): 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 = input('Select one of the above disks (by number or full path) or write /mnt to skip partitioning: ') + if drive.strip() == '/mnt': + return None + elif drive.isdigit(): drive = int(drive) if drive >= len(drives): raise DiskError(f'Selected option "{drive}" is out of range') |