From 850fd2efa812508e2df67aa2b50cff8820389a0d Mon Sep 17 00:00:00 2001 From: advaithm Date: Fri, 12 Mar 2021 11:30:32 +0530 Subject: Started work on BIOS support --- archinstall/lib/disk.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index c05ba757..16ee72e7 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -5,6 +5,7 @@ from .exceptions import DiskError from .general import * from .output import log, LOG_LEVELS from .storage import storage +from .hardware import hasUEFI ROOT_DIR_PATTERN = re.compile('^.*?/devices') GPT = 0b00000001 @@ -331,9 +332,12 @@ 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=GPT): + def __init__(self, blockdevice): self.blockdevice = blockdevice - self.mode = mode + if hasUEFI(): + self.mode = GPT + else: + self.mode = MBR def __enter__(self, *args, **kwargs): if self.blockdevice.keep_partitions is False: @@ -343,6 +347,11 @@ class Filesystem(): return self else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') + elif self.mode == MBR: + if sys_command(f'/usr/bin/parted -s {self.blockdevice.} mklabel msdos').exit_code == 0: + return self + else: + raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos') else: raise DiskError(f'Unknown mode selected to format in: {self.mode}') @@ -405,6 +414,9 @@ class Filesystem(): log(f'Adding partition to {self.blockdevice}', level=LOG_LEVELS.Info) previous_partitions = self.blockdevice.partitions + 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 format: partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {format} {start} {end}') == 0 else: -- cgit v1.2.3-54-g00ecf From 563a50dbc0d126c9d2e1038b7e71cf7d30a2fb29 Mon Sep 17 00:00:00 2001 From: advaithm Date: Fri, 12 Mar 2021 11:59:46 +0530 Subject: fixed mistake in disk.py --- archinstall/lib/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 16ee72e7..cd740571 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -348,7 +348,7 @@ class Filesystem(): else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel gpt') elif self.mode == MBR: - if sys_command(f'/usr/bin/parted -s {self.blockdevice.} mklabel msdos').exit_code == 0: + if sys_command(f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos').exit_code == 0: return self else: raise DiskError(f'Problem setting the partition format to GPT:', f'/usr/bin/parted -s {self.blockdevice.device} mklabel msdos') -- cgit v1.2.3-54-g00ecf From f249476ea7c296459d175262f45c1f8273c6e8ea Mon Sep 17 00:00:00 2001 From: advaithm Date: Fri, 12 Mar 2021 13:40:54 +0530 Subject: figured out a way to get root device for installing grub --- archinstall/lib/disk.py | 18 ++++++++++-------- archinstall/lib/installer.py | 3 ++- 2 files changed, 12 insertions(+), 9 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index cd740571..d05588a6 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -394,18 +394,20 @@ class Filesystem(): def use_entire_disk(self, root_filesystem_type='ext4', encrypt_root_partition=True): self.add_partition('primary', start='1MiB', end='513MiB', format='vfat') - self.set_name(0, 'EFI') - self.set(0, 'boot on') + #TODO: figure out what do for bios, we don't need a seprate partion for the bootloader + if hasUEFI(): + 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 - self.set(0, 'esp on') - self.add_partition('primary', start='513MiB', end='100%') + 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 + self.blockdevice.partition[0].filesystem = 'vfat' + self.blockdevice.partition[1].filesystem = root_filesystem_type - self.blockdevice.partition[0].target_mountpoint = '/boot' - self.blockdevice.partition[1].target_mountpoint = '/' + self.blockdevice.partition[0].target_mountpoint = '/boot' + self.blockdevice.partition[1].target_mountpoint = '/' if encrypt_root_partition: self.blockdevice.partition[1].encrypted = True diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index dbcfb973..176b26f4 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -365,7 +365,8 @@ class Installer(): 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') else: - o = b''.join(sys_command(f'/usr/bin/arch-chroot {self.mountpoint} grub-install --target=--target=i386-pc {self}')) + root_device = subprocess.check_output(f'basename "$(readlink -f "/sys/class/block/{self.partition.path}/..")',shell=True).decode().strip() + 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') else: raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") -- cgit v1.2.3-54-g00ecf From b974b93004efa9912e404f5d3fca7c44a58dc0e3 Mon Sep 17 00:00:00 2001 From: advaithm Date: Fri, 2 Apr 2021 10:08:16 +0530 Subject: fixed some issues with the changes --- archinstall/lib/disk.py | 7 ++----- archinstall/lib/installer.py | 12 +++++++++++- examples/guided.py | 8 +++++++- 3 files changed, 20 insertions(+), 7 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index fbc11ca3..8e9e0234 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -390,12 +390,9 @@ 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): + def __init__(self, blockdevice,mode): self.blockdevice = blockdevice - if hasUEFI(): - self.mode = GPT - else: - self.mode = MBR + self.mode = mode def __enter__(self, *args, **kwargs): if self.blockdevice.keep_partitions is False: diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index d161c3b7..492d7715 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,4 +1,4 @@ -import os, stat, time, shutil, pathlib +import os, stat, time, shutil, pathlib, subprocess from .exceptions import * from .disk import * @@ -398,6 +398,16 @@ class Installer(): 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.") + 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') + 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') else: raise RequirementError(f"Unknown (or not yet implemented) bootloader added to add_bootloader(): {bootloader}") diff --git a/examples/guided.py b/examples/guided.py index 71e1e01d..f374a41c 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -1,5 +1,6 @@ import getpass, time, json, sys, signal, os import archinstall +from archinstall.lib.hardware import hasUEFI """ This signal-handler chain (and global variable) @@ -244,7 +245,12 @@ def perform_installation_steps(): Setup the blockdevice, filesystem (and optionally encryption). Once that's done, we'll hand over to perform_installation() """ - with archinstall.Filesystem(archinstall.arguments['harddrive'], archinstall.GPT) as fs: + # maybe we can ask the user what they would prefer on uefi systems? + if hasUEFI(): + mode = archinstall.GPT + else: + mode = archinstall.MBR + with archinstall.Filesystem(archinstall.arguments['harddrive'],mode) as fs: # Wipe the entire drive if the disk flag `keep_partitions`is False. if archinstall.arguments['harddrive'].keep_partitions is False: fs.use_entire_disk(root_filesystem_type=archinstall.arguments.get('filesystem', 'btrfs')) -- cgit v1.2.3-54-g00ecf From 655ec06119ee67cbf58733f2f1902c606006bef9 Mon Sep 17 00:00:00 2001 From: advaithm Date: Fri, 2 Apr 2021 10:12:45 +0530 Subject: accidentlly called dict as function --- archinstall/lib/disk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 8e9e0234..fdc2fbc5 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -481,7 +481,7 @@ class Filesystem(): previous_partitions = self.blockdevice.partitions if self.mode == MBR: - if len(self.blockdevice.partitions())>3: + if len(self.blockdevice.partitions)>3: DiskError("Too many partitions on disk, MBR disks can only have 3 parimary partitions") if format: partitioning = self.parted(f'{self.blockdevice.device} mkpart {type} {format} {start} {end}') == 0 -- cgit v1.2.3-54-g00ecf From 4221473e6782c659dbd1f2681b0d2e05a3aa54b6 Mon Sep 17 00:00:00 2001 From: kpcyrd Date: Fri, 2 Apr 2021 20:03:38 +0200 Subject: Support passing commands as lists --- archinstall/lib/disk.py | 8 ++++---- archinstall/lib/general.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 9 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 9ad49ac2..0f29a8bc 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -72,7 +72,7 @@ class BlockDevice(): raise DiskError(f'Could not locate backplane info for "{self.path}"') if self.info['type'] == 'loop': - for drive in json.loads(b''.join(sys_command(f'losetup --json', hide_from_log=True)).decode('UTF_8'))['loopdevices']: + for drive in json.loads(b''.join(sys_command(['losetup', '--json'], hide_from_log=True)).decode('UTF_8'))['loopdevices']: if not drive['name'] == self.path: continue return drive['back-file'] @@ -88,10 +88,10 @@ class BlockDevice(): @property def partitions(self): - o = b''.join(sys_command(f'partprobe {self.path}')) + o = b''.join(sys_command(['partprobe', self.path])) #o = b''.join(sys_command('/usr/bin/lsblk -o name -J -b {dev}'.format(dev=dev))) - o = b''.join(sys_command(f'/usr/bin/lsblk -J {self.path}')) + o = b''.join(sys_command(['/usr/bin/lsblk', '-J', self.path])) if b'not a block device' in o: raise DiskError(f'Can not read partitions off something that isn\'t a block device: {self.path}') @@ -202,7 +202,7 @@ class Partition(): if not self._encrypted: return self.path else: - for blockdevice in json.loads(b''.join(sys_command('lsblk -J')).decode('UTF-8'))['blockdevices']: + 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}') diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index f2a714e7..ae2501c2 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -86,11 +86,19 @@ class sys_command():#Thread): if kwargs['emulate']: self.log(f"Starting command '{cmd}' in emulation mode.", level=LOG_LEVELS.Debug) - self.raw_cmd = cmd - try: - self.cmd = shlex.split(cmd) - except Exception as e: - raise ValueError(f'Incorrect string to split: {cmd}\n{e}') + if type(cmd) is list: + # if we get a list of arguments + self.raw_cmd = shlex.join(cmd) + self.cmd = cmd + else: + # else consider it a single shell string + # this should only be used if really necessary + self.raw_cmd = cmd + try: + self.cmd = shlex.split(cmd) + except Exception as e: + raise ValueError(f'Incorrect string to split: {cmd}\n{e}') + self.args = args self.kwargs = kwargs -- cgit v1.2.3-54-g00ecf From 090b98b8307fd924882e78b69df9227b4621ec6b Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Tue, 27 Apr 2021 14:43:17 +0000 Subject: Moving away from custom log levels, to something that's well defined. (#360) * Moving away from custom log levels, to something that's well defined. * Added backward compability to log() as well. * Added an option to force log messages out on screen even if the level is below the log level threashold. * Added force log messages when wrong notation is used. * Added some more length to the deprecated message * Swapped all log levels to use logging. instead. Co-authored-by: Anton Hvornum --- archinstall/lib/disk.py | 42 +++++++++++------------ archinstall/lib/general.py | 30 ++++++++--------- archinstall/lib/installer.py | 55 +++++++++++++++--------------- archinstall/lib/luks.py | 11 +++--- archinstall/lib/mirrors.py | 6 ++-- archinstall/lib/output.py | 67 +++++++++++++++++++++++++++---------- archinstall/lib/profiles.py | 6 ++-- archinstall/lib/user_interaction.py | 12 +++---- examples/guided.py | 14 ++++---- 9 files changed, 139 insertions(+), 104 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 67c2bdcd..8c7c6818 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -1,9 +1,9 @@ import glob, re, os, json, time, hashlib -import pathlib, traceback +import pathlib, traceback, logging from collections import OrderedDict from .exceptions import DiskError from .general import * -from .output import log, LOG_LEVELS +from .output import log from .storage import storage from .hardware import hasUEFI @@ -88,7 +88,7 @@ class BlockDevice(): 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=LOG_LEVELS.Debug) + 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.') @@ -129,7 +129,7 @@ class BlockDevice(): @property def uuid(self): - log(f'BlockDevice().uuid is untested!', level=LOG_LEVELS.Warning, fg='yellow') + log(f'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 @@ -222,8 +222,8 @@ class Partition(): @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) + log(f'Marking {self} as encrypted: {value}', level=logging.DEBUG) + log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=logging.DEBUG) self._encrypted = value @@ -240,7 +240,7 @@ class Partition(): 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) + log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) from .luks import luks2 try: @@ -269,16 +269,16 @@ 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) + log(f"Partition {self} is not marked for formatting.", level=logging.DEBUG) return False elif self.target_mountpoint == '/boot': try: if self.has_content(): - log(f"Partition {self} is a boot partition and has content inside.", level=LOG_LEVELS.Debug) + log(f"Partition {self} is a boot partition and has content inside.", level=logging.DEBUG) return False except SysCallError as err: - log(err.message, LOG_LEVELS.Debug) - log(f"Partition {self} was identified as /boot but we could not mount to check for content, continuing!", level=LOG_LEVELS.Debug) + log(err.message, logging.DEBUG) + log(f"Partition {self} was identified as /boot but we could not mount to check for content, continuing!", level=logging.DEBUG) pass return True @@ -293,7 +293,7 @@ class Partition(): 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") + log(f"Partition {self} was marked as protected but encrypt() was called on it!", level=logging.ERROR, fg="red") return False handle = luks2(self, None, None) @@ -321,7 +321,7 @@ class Partition(): raise PermissionError(f"{self} is not formatable either because instance is locked ({self.allow_formatting}) or a blocking flag was given ({allow_formatting})") if log_formatting: - log(f'Formatting {path} -> {filesystem}', level=LOG_LEVELS.Info) + log(f'Formatting {path} -> {filesystem}', level=logging.INFO) if filesystem == 'btrfs': o = b''.join(sys_command(f'/usr/bin/mkfs.btrfs -f {path}')) @@ -376,7 +376,7 @@ class Partition(): def mount(self, target, fs=None, options=''): if not self.mountpoint: - log(f'Mounting {self} to {target}', level=LOG_LEVELS.Info) + 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 @@ -434,7 +434,7 @@ class Filesystem(): def __enter__(self, *args, **kwargs): if self.blockdevice.keep_partitions is False: - log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=LOG_LEVELS.Debug) + log(f'Wiping {self.blockdevice} by using partition format {self.mode}', level=logging.DEBUG) if self.mode == GPT: if self.raw_parted(f'{self.blockdevice.device} mklabel gpt').exit_code == 0: self.blockdevice.flush_cache() @@ -451,7 +451,7 @@ class Filesystem(): # 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=LOG_LEVELS.Debug) + 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}.') @@ -474,7 +474,7 @@ class Filesystem(): def raw_parted(self, string:str): x = sys_command(f'/usr/bin/parted -s {string}') - log(f"'parted -s {string}' returned: {b''.join(x)}", level=LOG_LEVELS.Debug) + log(f"'parted -s {string}' returned: {b''.join(x)}", level=logging.DEBUG) return x def parted(self, string:str): @@ -487,7 +487,7 @@ class Filesystem(): return self.raw_parted(string).exit_code def use_entire_disk(self, root_filesystem_type='ext4'): - log(f"Using and formatting the entire {self.blockdevice}.", level=LOG_LEVELS.Debug) + log(f"Using and formatting the entire {self.blockdevice}.", level=logging.DEBUG) if hasUEFI(): self.add_partition('primary', start='1MiB', end='513MiB', format='fat32') self.set_name(0, 'EFI') @@ -499,7 +499,7 @@ class Filesystem(): 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) + log(f"Set the root partition {self.blockdevice.partition[1]} to use filesystem {root_filesystem_type}.", level=logging.DEBUG) self.blockdevice.partition[0].target_mountpoint = '/boot' self.blockdevice.partition[1].target_mountpoint = '/' @@ -510,12 +510,12 @@ class Filesystem(): #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) + log(f"Set the root partition {self.blockdevice.partition[0]} to use filesystem {root_filesystem_type}.", level=logging.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) + log(f'Adding partition to {self.blockdevice}', level=logging.INFO) previous_partitions = self.blockdevice.partitions if self.mode == MBR: diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index dc0f018a..eb0c5d14 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -1,10 +1,10 @@ import os, json, hashlib, shlex, sys -import time, pty +import time, pty, logging from datetime import datetime, date from subprocess import Popen, STDOUT, PIPE, check_output from select import epoll, EPOLLIN, EPOLLHUP from .exceptions import * -from .output import log, LOG_LEVELS +from .output import log def gen_uid(entropy_length=256): return hashlib.sha512(os.urandom(entropy_length)).hexdigest() @@ -84,7 +84,7 @@ class sys_command():#Thread): self.log = kwargs.get('log', log) if kwargs['emulate']: - self.log(f"Starting command '{cmd}' in emulation mode.", level=LOG_LEVELS.Debug) + self.log(f"Starting command '{cmd}' in emulation mode.", level=logging.DEBUG) if type(cmd) is list: # if we get a list of arguments @@ -204,7 +204,7 @@ class sys_command():#Thread): os.execve(self.cmd[0], self.cmd, {**os.environ, **self.environment_vars}) except FileNotFoundError: self.status = 'done' - self.log(f"{self.cmd[0]} does not exist.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} does not exist.", level=logging.DEBUG) self.exit_code = 1 return False @@ -214,8 +214,8 @@ class sys_command():#Thread): poller.register(child_fd, EPOLLIN | EPOLLHUP) if 'events' in self.kwargs and 'debug' in self.kwargs: - self.log(f'[D] Using triggers for command: {self.cmd}', level=LOG_LEVELS.Debug) - self.log(json.dumps(self.kwargs['events']), level=LOG_LEVELS.Debug) + self.log(f'[D] Using triggers for command: {self.cmd}', level=logging.DEBUG) + self.log(json.dumps(self.kwargs['events']), level=logging.DEBUG) alive = True last_trigger_pos = 0 @@ -230,7 +230,7 @@ class sys_command():#Thread): break if 'debug' in self.kwargs and self.kwargs['debug'] and len(output): - self.log(self.cmd, 'gave:', output.decode('UTF-8'), level=LOG_LEVELS.Debug) + self.log(self.cmd, 'gave:', output.decode('UTF-8'), level=logging.DEBUG) if 'on_output' in self.kwargs: self.kwargs['on_output'](self.kwargs['worker'], output) @@ -251,8 +251,8 @@ class sys_command():#Thread): trigger_pos = self.trace_log[last_trigger_pos:].lower().find(trigger.lower()) if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=LOG_LEVELS.Debug) - self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=LOG_LEVELS.Debug) + self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=logging.DEBUG) + self.log(f"Writing to subprocess {self.cmd[0]}: {self.kwargs['events'][trigger].decode('UTF-8')}", level=logging.DEBUG) last_trigger_pos = trigger_pos os.write(child_fd, self.kwargs['events'][trigger]) @@ -266,18 +266,18 @@ class sys_command():#Thread): ## Adding a exit trigger: if len(self.kwargs['events']) == 0: if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"Waiting for last command {self.cmd[0]} to finish.", level=LOG_LEVELS.Debug) + self.log(f"Waiting for last command {self.cmd[0]} to finish.", level=logging.DEBUG) if bytes(f']$'.lower(), 'UTF-8') in self.trace_log[0-len(f']$')-5:].lower(): if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} has finished.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} has finished.", level=logging.DEBUG) alive = False break self.status = 'done' if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} waiting for exit code.", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} waiting for exit code.", level=logging.DEBUG) if not self.kwargs['emulate']: try: @@ -291,14 +291,14 @@ class sys_command():#Thread): self.exit_code = 0 if 'debug' in self.kwargs and self.kwargs['debug']: - self.log(f"{self.cmd[0]} got exit code: {self.exit_code}", level=LOG_LEVELS.Debug) + self.log(f"{self.cmd[0]} got exit code: {self.exit_code}", level=logging.DEBUG) if 'ignore_errors' in self.kwargs: self.exit_code = 0 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) + #self.log(self.trace_log.decode('UTF-8'), level=logging.DEBUG) + #self.log(f"'{self.raw_cmd}' did not exit gracefully, exit code {self.exit_code}.", level=logging.ERROR) 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() diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index e2762603..04fe44c8 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -1,4 +1,5 @@ -import os, stat, time, shutil, pathlib, subprocess +import os, stat, time, shutil, pathlib +import subprocess, logging from .exceptions import * from .disk import * @@ -7,7 +8,7 @@ from .user_interaction import * from .profiles import Profile from .mirrors import * from .systemd import Networkd -from .output import log, LOG_LEVELS +from .output import log from .storage import storage from .hardware import * @@ -60,7 +61,7 @@ class Installer(): storage['session'] = self self.partitions = get_partitions_in_use(self.target) - def log(self, *args, level=LOG_LEVELS.Debug, **kwargs): + def log(self, *args, level=logging.DEBUG, **kwargs): """ installer.log() wraps output.log() mainly to set a default log-level for this install session. Any manual override can be done per log() call. @@ -75,8 +76,8 @@ class Installer(): # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager if len(args) >= 2 and args[1]: - #self.log(self.trace_log.decode('UTF-8'), level=LOG_LEVELS.Debug) - self.log(args[1], level=LOG_LEVELS.Error, fg='red') + #self.log(self.trace_log.decode('UTF-8'), level=logging.DEBUG) + self.log(args[1], level=logging.ERROR, fg='red') self.sync_log_to_install_medium() @@ -89,17 +90,17 @@ class Installer(): self.genfstab() if not (missing_steps := self.post_install_check()): - self.log('Installation completed without any errors. You may now reboot.', bg='black', fg='green', level=LOG_LEVELS.Info) + self.log('Installation completed without any errors. You may now reboot.', bg='black', fg='green', level=logging.INFO) self.sync_log_to_install_medium() return True else: - self.log('Some required steps were not successfully installed/configured before leaving the installer:', bg='black', fg='red', level=LOG_LEVELS.Warning) + self.log('Some required steps were not successfully installed/configured before leaving the installer:', bg='black', fg='red', level=logging.WARNING) for step in missing_steps: - self.log(f' - {step}', bg='black', fg='red', level=LOG_LEVELS.Warning) + self.log(f' - {step}', bg='black', fg='red', level=logging.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.log(f"Detailed error logs can be found at: {storage['LOG_PATH']}", level=logging.WARNING) + self.log(f"Submit this zip file as an issue to https://github.com/archlinux/archinstall/issues", level=logging.WARNING) self.sync_log_to_install_medium() return False @@ -129,21 +130,21 @@ class Installer(): def pacstrap(self, *packages, **kwargs): if type(packages[0]) in (list, tuple): packages = packages[0] - self.log(f'Installing packages: {packages}', level=LOG_LEVELS.Info) + self.log(f'Installing packages: {packages}', level=logging.INFO) if (sync_mirrors := sys_command('/usr/bin/pacman -Syy')).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) + self.log(f'Could not strap in packages: {pacstrap.exit_code}', level=logging.INFO) else: - self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=LOG_LEVELS.Info) + self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=logging.INFO) def set_mirrors(self, mirrors): return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') def genfstab(self, flags='-pU'): - self.log(f"Updating {self.target}/etc/fstab", level=LOG_LEVELS.Info) + self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) fstab = sys_command(f'/usr/bin/genfstab {flags} {self.target}').trace_log with open(f"{self.target}/etc/fstab", 'ab') as fstab_fh: @@ -179,19 +180,19 @@ class Installer(): else: self.log( f"Time zone {zone} does not exist, continuing with system default.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) def activate_ntp(self): - self.log(f'Installing and activating NTP.', level=LOG_LEVELS.Info) + self.log(f'Installing and activating NTP.', level=logging.INFO) if self.pacstrap('ntp'): if self.enable_service('ntpd'): return True def enable_service(self, *services): for service in services: - self.log(f'Enabling service {service}', level=LOG_LEVELS.Info) + self.log(f'Enabling service {service}', level=logging.INFO) if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0: raise ServiceException(f"Unable to start service {service}: {output}") @@ -349,7 +350,7 @@ class Installer(): # Run registered post-install hooks for function in self.post_base_install: - self.log(f"Running post-installation hook: {function}", level=LOG_LEVELS.Info) + self.log(f"Running post-installation hook: {function}", level=logging.INFO) function(self) return True @@ -363,7 +364,7 @@ class Installer(): elif partition.mountpoint == self.target: root_partition = partition - self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=LOG_LEVELS.Info) + self.log(f'Adding bootloader {bootloader} to {boot_partition}', level=logging.INFO) if bootloader == 'systemd-bootctl': if not hasUEFI(): @@ -417,10 +418,10 @@ class Installer(): 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) + log(f"Identifying root partition by PART-UUID on {real_device}: '{real_device.uuid}'.", level=logging.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 {root_partition}, looking for '{root_partition.uuid}'.", level=LOG_LEVELS.Debug) + log(f"Identifying root partition by PART-UUID on {root_partition}, looking for '{root_partition.uuid}'.", level=logging.DEBUG) entry.write(f'options root=PARTUUID={root_partition.uuid} rw intel_pstate=no_hwp\n') self.helper_flags['bootloader'] = bootloader @@ -458,17 +459,17 @@ class Installer(): if type(profile) == str: profile = Profile(self, profile) - self.log(f'Installing network profile {profile}', level=LOG_LEVELS.Info) + self.log(f'Installing network profile {profile}', level=logging.INFO) return profile.install() def enable_sudo(self, entity :str, group=False): - self.log(f'Enabling sudo permissions for {entity}.', level=LOG_LEVELS.Info) + self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) 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) + self.log(f'Creating user {user}', level=logging.INFO) 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) @@ -481,7 +482,7 @@ class Installer(): self.helper_flags['user'] = True def user_set_pw(self, user, password): - self.log(f'Setting password for {user}', level=LOG_LEVELS.Info) + self.log(f'Setting password for {user}', level=logging.INFO) if user == 'root': # This means the root account isn't locked/disabled with * in /etc/passwd @@ -491,7 +492,7 @@ class Installer(): pass def user_set_shell(self, user, shell): - self.log(f'Setting shell for {user} to {shell}', level=LOG_LEVELS.Info) + self.log(f'Setting shell for {user} to {shell}', level=logging.INFO) o = b''.join(sys_command(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"")) pass @@ -502,5 +503,5 @@ class Installer(): vconsole.write(f'KEYMAP={language}\n') vconsole.write(f'FONT=lat9w-16\n') else: - self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=LOG_LEVELS.Info) + self.log(f'Keyboard language was not changed from default (no language specified).', fg="yellow", level=logging.INFO) return True diff --git a/archinstall/lib/luks.py b/archinstall/lib/luks.py index 894be1c8..7f8485e6 100644 --- a/archinstall/lib/luks.py +++ b/archinstall/lib/luks.py @@ -2,10 +2,11 @@ import os import shlex import time import pathlib +import logging from .exceptions import * from .general import * from .disk import Partition -from .output import log, LOG_LEVELS +from .output import log from .storage import storage class luks2(): @@ -48,7 +49,7 @@ class luks2(): if not self.partition.allow_formatting: raise DiskError(f'Could not encrypt volume {self.partition} due to it having a formatting lock.') - log(f'Encrypting {partition} (This might take a while)', level=LOG_LEVELS.Info) + log(f'Encrypting {partition} (This might take a while)', level=logging.INFO) if not key_file: if self.key_file: @@ -84,7 +85,7 @@ class luks2(): cmd_handle = sys_command(cryptsetup_args) 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) + log(f'{partition} is being used, trying to unmount and crypt-close the device and running one more attempt at encrypting the device.', level=logging.DEBUG) # Partition was in use, unmount it and try again partition.unmount() @@ -97,11 +98,11 @@ class luks2(): for child in children: # Unmount the child location if child_mountpoint := child.get('mountpoint', None): - log(f'Unmounting {child_mountpoint}', level=LOG_LEVELS.Debug) + log(f'Unmounting {child_mountpoint}', level=logging.DEBUG) sys_command(f"umount -R {child_mountpoint}") # And close it if possible. - log(f"Closing crypt device {child['name']}", level=LOG_LEVELS.Debug) + log(f"Closing crypt device {child['name']}", level=logging.DEBUG) sys_command(f"cryptsetup close {child['name']}") # Then try again to set up the crypt-device diff --git a/archinstall/lib/mirrors.py b/archinstall/lib/mirrors.py index 04f47c0d..ae6c6422 100644 --- a/archinstall/lib/mirrors.py +++ b/archinstall/lib/mirrors.py @@ -1,4 +1,4 @@ -import urllib.request +import urllib.request, logging from .exceptions import * from .general import * @@ -59,7 +59,7 @@ def insert_mirrors(mirrors, *args, **kwargs): return True def use_mirrors(regions :dict, destination='/etc/pacman.d/mirrorlist'): - log(f'A new package mirror-list has been created: {destination}', level=LOG_LEVELS.Info) + 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: for mirror in mirrors: @@ -79,7 +79,7 @@ def list_mirrors(): try: response = urllib.request.urlopen(url) except urllib.error.URLError as err: - log(f'Could not fetch an active mirror-list: {err}', level=LOG_LEVELS.Warning, fg="yellow") + log(f'Could not fetch an active mirror-list: {err}', level=logging.WARNING, fg="yellow") return regions diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py index 6b184b4b..73819422 100644 --- a/archinstall/lib/output.py +++ b/archinstall/lib/output.py @@ -17,8 +17,32 @@ class LOG_LEVELS: class journald(dict): @abc.abstractmethod - def log(message, level=LOG_LEVELS.Debug): - import systemd.journal + def log(message, level=logging.DEBUG): + try: + import systemd.journal + except ModuleNotFoundError: + return False + + # For backwards compability, convert old style log-levels + # to logging levels (and warn about deprecated usage) + # There's some code re-usage here but that should be fine. + # TODO: Remove these in a few versions: + if level == LOG_LEVELS.Critical: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.CRITICAL + elif level == LOG_LEVELS.Error: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.ERROR + elif level == LOG_LEVELS.Warning: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.WARNING + elif level == LOG_LEVELS.Info: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.INFO + elif level == LOG_LEVELS.Debug: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + level = logging.DEBUG + log_adapter = logging.getLogger('archinstall') log_fmt = logging.Formatter("[%(levelname)s]: %(message)s") log_ch = systemd.journal.JournalHandler() @@ -26,19 +50,7 @@ class journald(dict): log_adapter.addHandler(log_ch) log_adapter.setLevel(logging.DEBUG) - if level == LOG_LEVELS.Critical: - log_adapter.critical(message) - elif level == LOG_LEVELS.Error: - log_adapter.error(message) - elif level == LOG_LEVELS.Warning: - log_adapter.warning(message) - elif level == LOG_LEVELS.Info: - log_adapter.info(message) - elif level == LOG_LEVELS.Debug: - log_adapter.debug(message) - else: - # Fallback logger - log_adapter.debug(message) + log_adapter.log(level, message) # TODO: Replace log() for session based logging. class SessionLogging(): @@ -112,17 +124,38 @@ def log(*args, **kwargs): with open(absolute_logfile, 'a') as log_file: log_file.write(f"{orig_string}\n") + # If we assigned a level, try to log it to systemd's journald. # Unless the level is higher than we've decided to output interactively. # (Remember, log files still get *ALL* the output despite level restrictions) if 'level' in kwargs: - if kwargs['level'] > storage.get('LOG_LEVEL', LOG_LEVELS.Info): + # For backwards compability, convert old style log-levels + # to logging levels (and warn about deprecated usage) + # There's some code re-usage here but that should be fine. + # TODO: Remove these in a few versions: + if kwargs['level'] == LOG_LEVELS.Critical: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.CRITICAL + elif kwargs['level'] == LOG_LEVELS.Error: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.ERROR + elif kwargs['level'] == LOG_LEVELS.Warning: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.WARNING + elif kwargs['level'] == LOG_LEVELS.Info: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.INFO + elif kwargs['level'] == LOG_LEVELS.Debug: + log("Deprecated level detected in log message, please use new logging. instead for the following log message:", fg="red", level=logging.ERROR, force=True) + kwargs['level'] = logging.DEBUG + + if kwargs['level'] > storage.get('LOG_LEVEL', logging.INFO) and not 'force' in kwargs: # Level on log message was Debug, but output level is set to Info. # In that case, we'll drop it. return None try: - journald.log(string, level=kwargs.get('level', LOG_LEVELS.Info)) + journald.log(string, level=kwargs.get('level', logging.INFO)) except ModuleNotFoundError: pass # Ignore writing to journald diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 265ca26a..4988e7ab 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -1,10 +1,10 @@ import os, urllib.request, urllib.parse, ssl, json, re -import importlib.util, sys, glob, hashlib +import importlib.util, sys, glob, hashlib, logging from collections import OrderedDict from .general import multisplit, sys_command from .exceptions import * from .networking import * -from .output import log, LOG_LEVELS +from .output import log from .storage import storage def grab_url_data(path): @@ -82,7 +82,7 @@ class Script(): 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) + log(f"Script {self} has been loaded with namespace '{self.namespace}'", level=logging.DEBUG) def __enter__(self, *args, **kwargs): self.execute() diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index d8640a81..79b0e0b9 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,9 +1,9 @@ import getpass, pathlib, os, shutil, re -import sys, time, signal, ipaddress +import sys, time, signal, ipaddress, logging from .exceptions import * from .profiles import Profile from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout -from .output import log, LOG_LEVELS +from .output import log from .storage import storage from .networking import list_interfaces from .general import sys_command @@ -26,7 +26,7 @@ def check_for_correct_username(username): return True log( "The username you entered is invalid. Try again", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) return False @@ -141,7 +141,7 @@ def ask_for_a_timezone(): else: log( f"Specified timezone {timezone} does not exist.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) @@ -198,7 +198,7 @@ def ask_to_configure_network(): except ValueError: log( "You need to enter a valid IP in IP-config mode.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) @@ -214,7 +214,7 @@ def ask_to_configure_network(): except ValueError: log( "You need to enter a valid gateway (router) IP address.", - level=LOG_LEVELS.Warning, + level=logging.WARNING, fg='red' ) diff --git a/examples/guided.py b/examples/guided.py index 43aaa788..0e6f2904 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -1,4 +1,4 @@ -import getpass, time, json, os +import getpass, time, json, os, logging import archinstall from archinstall.lib.hardware import hasUEFI from archinstall.lib.profiles import Profile @@ -237,8 +237,8 @@ def ask_user_questions(): def perform_installation_steps(): print() print('This is your chosen configuration:') - archinstall.log("-- Guided template chosen (with below config) --", level=archinstall.LOG_LEVELS.Debug) - archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=archinstall.LOG_LEVELS.Info) + archinstall.log("-- Guided template chosen (with below config) --", level=logging.DEBUG) + archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=logging.INFO) print() input('Press Enter to continue.') @@ -282,7 +282,7 @@ def perform_installation_steps(): else: partition.format() else: - archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=archinstall.LOG_LEVELS.Debug) + archinstall.log(f"Did not format {partition} because .safe_to_format() returned False or .allow_formatting was False.", level=logging.DEBUG) if hasUEFI(): fs.find_partition('/boot').format('vfat')# we don't have a boot partition in bios mode @@ -313,7 +313,7 @@ def perform_installation(mountpoint): # Certain services might be running that affects the system during installation. # Currently, only one such service is "reflector.service" which updates /etc/pacman.d/mirrorlist # We need to wait for it before we continue since we opted in to use a custom mirror/region. - installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=archinstall.LOG_LEVELS.Info) + installation.log(f'Waiting for automatic mirror selection (reflector) to complete.', level=logging.INFO) while archinstall.service_state('reflector') not in ('dead', 'failed'): time.sleep(1) # Set mirrors used by pacstrap (outside of installation) @@ -342,7 +342,7 @@ def perform_installation(mountpoint): installation.enable_service('systemd-resolved') if archinstall.arguments.get('audio', None) != None: - installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=archinstall.LOG_LEVELS.Info) + installation.log(f"This audio server will be used: {archinstall.arguments.get('audio', None)}", level=logging.INFO) if archinstall.arguments.get('audio', None) == 'pipewire': print('Installing pipewire ...') @@ -351,7 +351,7 @@ def perform_installation(mountpoint): print('Installing pulseaudio ...') installation.add_additional_packages("pulseaudio") else: - installation.log("No audio server will be installed.", level=archinstall.LOG_LEVELS.Info) + installation.log("No audio server will be installed.", level=logging.INFO) if archinstall.arguments.get('packages', None) and archinstall.arguments.get('packages', None)[0] != '': installation.add_additional_packages(archinstall.arguments.get('packages', None)) -- cgit v1.2.3-54-g00ecf From 4079eebc70af3dff4c5f3071c397697283a6890c Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Wed, 28 Apr 2021 13:18:28 +0000 Subject: Added a mini curses class and generic-multi-select (#362) * Added a mini curses class. It can do some simple tricks to iterate over menu options and indicate which ones are chosen using generic_multi_select(). * Include the default parameter if set. * Modified 'select_kernel()' to use the new multi-select. * Sneaky character got in. * removed some debugging * removed some debugging * Spelling error * Adding error handling and loop support. * Enforce that 'default' is always selected if no other option is selected. * Fixed backspace issues and ghosting. Co-authored-by: Anton Hvornum --- archinstall/lib/disk.py | 3 - archinstall/lib/profiles.py | 1 - archinstall/lib/user_interaction.py | 175 ++++++++++++++++++++++++++++++++++-- 3 files changed, 170 insertions(+), 9 deletions(-) (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 8c7c6818..ff924f62 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -221,9 +221,6 @@ class Partition(): @encrypted.setter def encrypted(self, value :bool): - if value: - log(f'Marking {self} as encrypted: {value}', level=logging.DEBUG) - log(f"Callstrack when marking the partition: {''.join(traceback.format_stack())}", level=logging.DEBUG) self._encrypted = value diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index 4988e7ab..ad5d3bac 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -82,7 +82,6 @@ class Script(): 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=logging.DEBUG) def __enter__(self, *args, **kwargs): self.execute() diff --git a/archinstall/lib/user_interaction.py b/archinstall/lib/user_interaction.py index d08d7e25..451251cd 100644 --- a/archinstall/lib/user_interaction.py +++ b/archinstall/lib/user_interaction.py @@ -1,5 +1,6 @@ -import getpass, pathlib, os, shutil, re +import getpass, pathlib, os, shutil, re, time import sys, time, signal, ipaddress, logging +import termios, tty, select # Used for char by char polling of sys.stdin from .exceptions import * from .profiles import Profile from .locale_helpers import list_keyboard_languages, verify_keyboard_layout, search_keyboard_layout @@ -95,6 +96,173 @@ def print_large_list(options, padding=5, margin_bottom=0, separator=': '): print(f"{str(column): >{highest_index_number_length}}{separator}{options[column]}", end = spaces) print() + return column, row + + +def generic_multi_select(options, text="Select one or more of the options above (leave blank to continue): ", sort=True, default=None, allow_empty=False): + if sort: + options = sorted(options) + + section = MiniCurses(get_terminal_width(), len(options)) + + selected_options = [] + + while True: + if len(selected_options) <= 0 and default and default in options: + selected_options.append(default) + + printed_options = [] + for option in options: + if option in selected_options: + printed_options.append(f'>> {option}') + else: + printed_options.append(f'{option}') + + section.clear(0, get_terminal_height()-section._cursor_y-1) + x, y = print_large_list(printed_options, margin_bottom=2) + section._cursor_y = len(printed_options) + section._cursor_x = 0 + section.write_line(text) + section.input_pos = section._cursor_x + selected_option = section.get_keyboard_input(end=None) + + if selected_option is None: + if len(selected_options) <= 0 and default: + selected_options = [default] + + if len(selected_options) or allow_empty is True: + break + else: + log('* Need to select at least one option!', fg='red') + continue + + elif selected_option.isdigit(): + if (selected_option := int(selected_option)) >= len(options): + log('* Option is out of range, please select another one!', fg='red') + continue + selected_option = options[selected_option] + if selected_option in selected_options: + selected_options.remove(selected_option) + else: + selected_options.append(selected_option) + + return selected_options + + +class MiniCurses(): + def __init__(self, width, height): + self.width = width + self.height = height + + self._cursor_y = 0 + self._cursor_x = 0 + + self.input_pos = 0 + + def write_line(self, text, clear_line=True): + if clear_line: + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + sys.stdout.write(" " * (get_terminal_width()-1)) + sys.stdout.flush() + sys.stdout.write("\033[%dG" % 0) + sys.stdout.flush() + sys.stdout.write(text) + sys.stdout.flush() + self._cursor_x += len(text) + + def clear(self, x, y): + if x < 0: x = 0 + if y < 0: y = 0 + + #import time + #sys.stdout.write(f"Clearing from: {x, y}") + #sys.stdout.flush() + #time.sleep(2) + + sys.stdout.flush() + sys.stdout.write('\033[%d;%df' % (y, x)) + for line in range(get_terminal_height()-y-1, y): + sys.stdout.write(" " * (get_terminal_width()-1)) + sys.stdout.flush() + sys.stdout.write('\033[%d;%df' % (y, x)) + sys.stdout.flush() + + def deal_with_control_characters(self, char): + mapper = { + '\x7f' : 'BACKSPACE', + '\r' : 'CR', + '\n' : 'NL' + } + + if (mapped_char := mapper.get(char, None)) == 'BACKSPACE': + if self._cursor_x <= self.input_pos: + # Don't backspace futher back than the cursor start position during input + return True + # Move back to the current known position (BACKSPACE doesn't updated x-pos) + sys.stdout.flush() + sys.stdout.write("\033[%dG" % (self._cursor_x)) + sys.stdout.flush() + + # Write a blank space + sys.stdout.flush() + sys.stdout.write(" ") + sys.stdout.flush() + + # And move back again + sys.stdout.flush() + sys.stdout.write("\033[%dG" % (self._cursor_x)) + sys.stdout.flush() + + self._cursor_x -= 1 + + return True + elif mapped_char in ('CR', 'NL'): + return True + + return None + + def get_keyboard_input(self, strip_rowbreaks=True, end='\n'): + assert end in ['\r', '\n', None] + + poller = select.epoll() + response = '' + + sys_fileno = sys.stdin.fileno() + old_settings = termios.tcgetattr(sys_fileno) + tty.setraw(sys_fileno) + + poller.register(sys.stdin.fileno(), select.EPOLLIN) + + EOF = False + while EOF is False: + for fileno, event in poller.poll(0.025): + char = sys.stdin.read(1) + + #sys.stdout.write(f"{[char]}") + #sys.stdout.flush() + + if (newline := (char in ('\n', '\r'))): + EOF = True + + if not newline or strip_rowbreaks is False: + response += char + + if self.deal_with_control_characters(char) is not True: + self.write_line(response[-1], clear_line=False) + + termios.tcsetattr(sys_fileno, termios.TCSADRAIN, old_settings) + + if end: + sys.stdout.write(end) + sys.stdout.flush() + self._cursor_x = 0 + self._cursor_y += 1 + + if response: + return response + def ask_for_superuser_account(prompt='Username for required super-user with sudo privileges: ', forced=False): while 1: new_user = input(prompt).strip(' ') @@ -523,9 +691,6 @@ def select_kernel(options): kernels = sorted(list(options)) if kernels: - selected_kernels = generic_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ") - if not selected_kernels: - return DEFAULT_KERNEL - return selected_kernels + return generic_multi_select(kernels, f"Choose which kernel to use (leave blank for default: {DEFAULT_KERNEL}): ", default=DEFAULT_KERNEL) raise RequirementError("Selecting kernels require a least one kernel to be given as an option.") -- cgit v1.2.3-54-g00ecf From 0ebc6be7ae3b153f2baad722a08a2019bb04c905 Mon Sep 17 00:00:00 2001 From: Anton Hvornum Date: Thu, 29 Apr 2021 11:32:21 +0000 Subject: Added a postgresql application profile. (#383) * Added a postgres application profile. Also introducing runas to the arch_chroot of the installation, to run commands as emulated users. This is highly WIP at the moment. * Fixing top-level-listing of profiles. As well as testing some postgres installation steps. * Removed dupe functions. * Added safety check in case a comment mentions the top level profile thing. * Patching namespace corruption. * Avoiding runtime collision due to installation not being initiated yet. * Allow for parameterization of filesystem in guided. Co-authored-by: Anton Hvornum --- archinstall/lib/disk.py | 1 - archinstall/lib/installer.py | 3 +++ archinstall/lib/profiles.py | 53 +++++++------------------------------ examples/guided.py | 6 +++-- profiles/52-54-00-12-34-56.py | 8 +++++- profiles/applications/postgresql.py | 11 ++++++++ 6 files changed, 34 insertions(+), 48 deletions(-) create mode 100644 profiles/applications/postgresql.py (limited to 'archinstall/lib/disk.py') diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index ff924f62..49bef1be 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -471,7 +471,6 @@ class Filesystem(): def raw_parted(self, string:str): x = sys_command(f'/usr/bin/parted -s {string}') - log(f"'parted -s {string}' returned: {b''.join(x)}", level=logging.DEBUG) return x def parted(self, string:str): diff --git a/archinstall/lib/installer.py b/archinstall/lib/installer.py index dbc6d1b4..a7b36481 100644 --- a/archinstall/lib/installer.py +++ b/archinstall/lib/installer.py @@ -196,6 +196,9 @@ class Installer(): return sys_command(f'/usr/bin/arch-chroot {self.target} {cmd}') def arch_chroot(self, cmd, *args, **kwargs): + if 'runas' in kwargs: + cmd = f"su - {kwargs['runas']} -c \"{cmd}\"" + return self.run_command(cmd) def drop_to_shell(self): diff --git a/archinstall/lib/profiles.py b/archinstall/lib/profiles.py index ad5d3bac..06237c1c 100644 --- a/archinstall/lib/profiles.py +++ b/archinstall/lib/profiles.py @@ -92,6 +92,9 @@ class Script(): if len(args) >= 2 and args[1]: raise args[1] + if self.original_namespace: + self.namespace = self.original_namespace + def localize_path(self, profile_path): if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): if not self.converted_path: @@ -202,51 +205,14 @@ class Profile(Script): 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 - - - 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: + if '__name__' in source_data and 'is_top_level_profile' in source_data: with self.load_instructions(namespace=f"{self.namespace}.py") as imported: - if hasattr(imported, '_post_install'): - return True + if hasattr(imported, 'is_top_level_profile'): + return imported.is_top_level_profile - def is_top_level_profile(self): - with open(self.path, 'r') as source: - source_data = source.read() - return 'top_level_profile = True' in source_data + # Default to True if nothing is specified, + # since developers like less code - omitting it should assume they want to present it. + return True @property def packages(self) -> list: @@ -268,7 +234,6 @@ class Profile(Script): if hasattr(imported, '__packages__'): return imported.__packages__ return None - class Application(Profile): def __repr__(self, *args, **kwargs): diff --git a/examples/guided.py b/examples/guided.py index 2b8a06f5..c281d033 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -136,12 +136,14 @@ def ask_user_questions(): archinstall.log('Using existing partition table reported above.') elif option == 'format-all': - archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() + if not archinstall.arguments.get('filesystem', None): + archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['harddrive'].keep_partitions = False elif archinstall.arguments['harddrive']: # If the drive doesn't have any partitions, safely mark the disk with keep_partitions = False # and ask the user for a root filesystem. - archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() + if not archinstall.arguments.get('filesystem', None): + archinstall.arguments['filesystem'] = archinstall.ask_for_main_filesystem_format() archinstall.arguments['harddrive'].keep_partitions = False # Get disk encryption password (or skip if blank) diff --git a/profiles/52-54-00-12-34-56.py b/profiles/52-54-00-12-34-56.py index 442e053c..a3347760 100644 --- a/profiles/52-54-00-12-34-56.py +++ b/profiles/52-54-00-12-34-56.py @@ -4,6 +4,11 @@ import urllib.request __packages__ = ['nano', 'wget', 'git'] +if __name__ == '52-54-00-12-34-56': + awesome = archinstall.Application(installation, 'postgresql') + awesome.install() + +""" # Unmount and close previous runs (Mainly only used for re-runs, but won't hurt.) archinstall.sys_command(f'umount -R /mnt', suppress_errors=True) archinstall.sys_command(f'cryptsetup close /dev/mapper/luksloop', suppress_errors=True) @@ -51,4 +56,5 @@ with archinstall.Filesystem(harddrive) as fs: try: urllib.request.urlopen(req, timeout=5) except: - pass \ No newline at end of file + pass +""" \ No newline at end of file diff --git a/profiles/applications/postgresql.py b/profiles/applications/postgresql.py new file mode 100644 index 00000000..fcdce824 --- /dev/null +++ b/profiles/applications/postgresql.py @@ -0,0 +1,11 @@ +import archinstall + +# Define the package list in order for lib to source +# which packages will be installed by this profile +__packages__ = ["postgresql"] + +installation.add_additional_packages(__packages__) + +installation.arch_chroot("initdb -D /var/lib/postgres/data", runas='postgres') + +installation.enable_service('postgresql') \ No newline at end of file -- cgit v1.2.3-54-g00ecf