From bbb4599165a644bbd81b085fb3210cd0e497d503 Mon Sep 17 00:00:00 2001 From: Yash Tripathi Date: Thu, 20 May 2021 01:01:58 +0530 Subject: Added support for getting configuration from a config file (#364) * added support for ingesting config * fixed condition to check key in dictionary * Removed redundant code, profile and desktop keys are now optional * Added base-config.json and support for pulling credentials from .env * added base config file and env file for users credentials * added silent install switch * added python-dotenv as a dependency * Updated Readme to include argparse changes as well as config ingestion * Updated Readme to include argparse changes as well as config ingestion * fixed typo in pyproject.toml * Replaced the magic __builtin__ global variable. This should fix mypy complaints while still retaining the same functionality, kinda. It's less automatic but it's also less of dark magic, which makes sense for anyone but me. * Fixes string index error. * Quotation error. * fixed initializing --script argument * added python-dotenv as a dependency * Installation can't be silent if config is not passed * fixed silent install help * fixed condition for ask_user_questions * reverted to creating profile object properly * Cleaned up and incorporated suggestions * added Profile import * added condition if Profile is null * fixed condition * updated parsing vars from argparse * removed loading users from .env * Reworking SysCommand & Moving to localectl for locale related activities (#4) * Moving to `localectl` rather than local file manipulation *(both for listing locales and setting them)*. * Swapped `loadkeys` for localectl. * Renamed `main` to `maim` in awesome profile. * Created `archinstall.Boot()` which spawns a `systemd-nspawn` container against the installation target. * Exposing systemd.py's internals to archinstall global scope. * Re-worked `SysCommand` completely, it's now a wrapper for `SysCommandWorker` which supports interacting with the process in a different way. `SysCommand` should behave just like the old one, for backwards compatibility reasons. This fixes #68 and #69. * `SysCommand()` now has a `.decode()` function that defaults to `UTF-8`. * Adding back peak_output=True to pacstrap. Co-authored-by: Anton Hvornum Co-authored-by: Dylan Taylor Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum * fixed indent * removed redundant import * removed duplicate import * removed duplicate import Co-authored-by: Anton Hvornum Co-authored-by: Anton Hvornum Co-authored-by: Dylan M. Taylor --- README.md | 14 ++++++++-- archinstall/__init__.py | 60 +++++++++++++++++++++++++++++-------------- archinstall/lib/disk.py | 2 +- archinstall/lib/general.py | 3 ++- archinstall/lib/networking.py | 4 +-- archinstall/lib/systemd.py | 9 ++++--- examples/config-sample.json | 35 +++++++++++++++++++++++++ examples/guided.py | 38 +++++++++++++++++++-------- profiles/desktop.py | 3 ++- pyproject.toml | 2 +- setup.cfg | 2 +- 11 files changed, 129 insertions(+), 43 deletions(-) create mode 100644 examples/config-sample.json diff --git a/README.md b/README.md index c03b2e0f..100288f3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,17 @@ Or use `pip install --upgrade archinstall` to use as a library. Assuming you are on an Arch Linux live-ISO and booted into EFI mode. - # python -m archinstall guided + # python -m archinstall --script guided + + +## Running from a declarative [config](examples/base-config.json) + +Prequisites: + 1. Edit the [config](examples/base-config.json) according to your requirements. + +Assuming you are on a Arch Linux live-ISO and booted into EFI mode. + + # python -m archinstall --config --vars '' # Help? @@ -143,7 +153,7 @@ This can be done by installing `pacman -S arch-install-scripts util-linux` local # losetup -fP ./testimage.img # losetup -a | grep "testimage.img" | awk -F ":" '{print $1}' # pip install --upgrade archinstall - # python -m archinstall guided + # python -m archinstall --script guided # qemu-system-x86_64 -enable-kvm -machine q35,accel=kvm -device intel-iommu -cpu host -m 4096 -boot order=d -drive file=./testimage.img,format=raw -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF_CODE.fd -drive if=pflash,format=raw,readonly,file=/usr/share/ovmf/x64/OVMF_VARS.fd This will create a *5 GB* `testimage.img` and create a loop device which we can use to format and install to.
diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 075b6f50..276d122f 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -1,4 +1,6 @@ """Arch Linux installer - guided, templates etc.""" +from argparse import ArgumentParser, FileType + from .lib.disk import * from .lib.exceptions import * from .lib.general import * @@ -16,22 +18,46 @@ from .lib.storage import * from .lib.systemd import * from .lib.user_interaction import * +parser = ArgumentParser() + __version__ = "2.2.0.dev1" -# Basic version of arg.parse() supporting: -# --key=value -# --boolean -arguments = {} -positionals = [] -for arg in sys.argv[1:]: - if '--' == arg[:2]: - if '=' in arg: - key, val = [x.strip() for x in arg[2:].split('=', 1)] - else: - key, val = arg[2:], True - arguments[key] = val - else: - positionals.append(arg) + +def initialize_arguments(): + config = {} + parser.add_argument("--config", nargs="?", help="json config file", type=FileType("r", encoding="UTF-8")) + parser.add_argument("--silent", action="store_true", + help="Warning!!! No prompts, ignored if config is not passed") + parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str) + parser.add_argument("--vars", + metavar="KEY=VALUE", + nargs='?', + help="Set a number of key-value pairs " + "(do not put spaces before or after the = sign). " + "If a value contains spaces, you should define " + "it with double quotes: " + 'foo="this is a sentence". Note that ' + "values are always treated as strings.") + args = parser.parse_args() + if args.config is not None: + try: + config = json.load(args.config) + except Exception as e: + print(e) + # Installation can't be silent if config is not passed + config["silent"] = args.silent + if args.vars is not None: + try: + for var in args.vars.split(' '): + key, val = var.split("=") + config[key] = val + except Exception as e: + print(e) + config["script"] = args.script + return config + + +arguments = initialize_arguments() # TODO: Learn the dark arts of argparse... (I summon thee dark spawn of cPython) @@ -46,12 +72,8 @@ def run_as_a_module(): # 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]) + script = Script(arguments.get('script', None)) except ProfileNotFound as err: print(f"Couldn't find file: {err}") sys.exit(1) diff --git a/archinstall/lib/disk.py b/archinstall/lib/disk.py index 44f2742b..8f67111a 100644 --- a/archinstall/lib/disk.py +++ b/archinstall/lib/disk.py @@ -266,7 +266,7 @@ class Partition: files = len(glob.glob(f"{temporary_mountpoint}/*")) iterations = 0 - while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations+1) < 10: + while SysCommand(f"/usr/bin/umount -R {temporary_mountpoint}").exit_code != 0 and (iterations := iterations + 1) < 10: time.sleep(1) temporary_path.rmdir() diff --git a/archinstall/lib/general.py b/archinstall/lib/general.py index 65c83484..cec3891a 100644 --- a/archinstall/lib/general.py +++ b/archinstall/lib/general.py @@ -360,7 +360,8 @@ def prerequisite_check(): def reboot(): o = b''.join(SysCommand("/usr/bin/reboot")) -def pid_exists(pid :int): + +def pid_exists(pid: int): try: return any(subprocess.check_output(['/usr/bin/ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip()) except subprocess.CalledProcessError: diff --git a/archinstall/lib/networking.py b/archinstall/lib/networking.py index fdeefb84..eb11a47e 100644 --- a/archinstall/lib/networking.py +++ b/archinstall/lib/networking.py @@ -1,14 +1,14 @@ import fcntl -import os import logging +import os import socket import struct from collections import OrderedDict from .exceptions import * from .general import SysCommand -from .storage import storage from .output import log +from .storage import storage def get_hw_addr(ifname): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) diff --git a/archinstall/lib/systemd.py b/archinstall/lib/systemd.py index e64ff7e0..383f1f17 100644 --- a/archinstall/lib/systemd.py +++ b/archinstall/lib/systemd.py @@ -5,6 +5,7 @@ from .installer import Installer from .output import log from .storage import storage + class Ini: def __init__(self, *args, **kwargs): """ @@ -103,7 +104,7 @@ class Boot: return self.session.is_alive() - def SysCommand(self, cmd :list, *args, **kwargs): + def SysCommand(self, cmd: list, *args, **kwargs): if cmd[0][0] != '/' and cmd[0][:2] != './': # This check is also done in SysCommand & SysCommandWorker. # However, that check is done for `machinectl` and not for our chroot command. @@ -113,8 +114,8 @@ class Boot: return SysCommand(["machinectl", "shell", self.container_name, *cmd], *args, **kwargs) - def SysCommandWorker(self, cmd :list, *args, **kwargs): + def SysCommandWorker(self, cmd: list, *args, **kwargs): if cmd[0][0] != '/' and cmd[0][:2] != './': cmd[0] = locate_binary(cmd[0]) - - return SysCommandWorker(["machinectl", "shell", self.container_name, *cmd], *args, **kwargs) \ No newline at end of file + + return SysCommandWorker(["machinectl", "shell", self.container_name, *cmd], *args, **kwargs) diff --git a/examples/config-sample.json b/examples/config-sample.json new file mode 100644 index 00000000..55bdf04b --- /dev/null +++ b/examples/config-sample.json @@ -0,0 +1,35 @@ +{ + "!root-password": "", + "audio": null, + "bootloader": "systemd-bootctl", + "filesystem": "btrfs", + "harddrive": { + "path": "/dev/sda" + }, + "hostname": "box", + "kernels": [ + "linux" + ], + "keyboard-language": "us", + "mirror-region": { + "Worldwide": { + "https://mirror.rackspace.com/archlinux/$repo/os/$arch": true + } + }, + "nic": { + "NetworkManager": true + }, + "packages": [], + "profile": null, + "superusers": { + "": { + "!password": "" + } + }, + "timezone": "UTC", + "users": { + "": { + "!password": "" + } + } +} \ No newline at end of file diff --git a/examples/guided.py b/examples/guided.py index b9b06a64..f0d0db7a 100644 --- a/examples/guided.py +++ b/examples/guided.py @@ -1,11 +1,12 @@ import json import logging -import time import os +import time import archinstall from archinstall.lib.hardware import has_uefi from archinstall.lib.networking import check_mirror_reachable +from archinstall.lib.profiles import Profile if archinstall.arguments.get('help'): print("See `man archinstall` for help.") @@ -243,7 +244,8 @@ def perform_installation_steps(): archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=logging.INFO) print() - input('Press Enter to continue.') + if not archinstall.arguments.get('silent'): + input('Press Enter to continue.') """ Issue a final warning before we continue with something un-revertable. @@ -261,7 +263,6 @@ def perform_installation_steps(): mode = archinstall.GPT if has_uefi() is False: 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: @@ -297,7 +298,7 @@ def perform_installation_steps(): fs.find_partition('/').mount(archinstall.storage.get('MOUNT_POINT', '/mnt')) if has_uefi(): - fs.find_partition('/boot').mount(archinstall.storage.get('MOUNT_POINT', '/mnt')+'/boot') + fs.find_partition('/boot').mount(archinstall.storage.get('MOUNT_POINT', '/mnt') + '/boot') perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt')) @@ -381,12 +382,13 @@ def perform_installation(mountpoint): exit(1) installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") - choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ") - if choice.lower() in ("y", ""): - try: - installation.drop_to_shell() - except: - pass + if not archinstall.arguments.get('silent'): + choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ") + if choice.lower() in ("y", ""): + try: + installation.drop_to_shell() + except: + pass # For support reasons, we'll log the disk layout post installation (crash or no crash) archinstall.log(f"Disk states after installing: {archinstall.disk_layouts()}", level=logging.DEBUG) @@ -397,5 +399,19 @@ if not check_mirror_reachable(): archinstall.log(f"Arch Linux mirrors are not reachable. Please check your internet connection and the log file '{log_file}'.", level=logging.INFO, fg="red") exit(1) -ask_user_questions() +if archinstall.arguments.get('silent', None) is None: + ask_user_questions() +else: + # Workarounds if config is loaded from a file + # The harddrive section should be moved to perform_installation_steps, where it's actually being performed + # Blockdevice object should be created in perform_installation_steps + # This needs to be done until then + archinstall.arguments['harddrive'] = archinstall.BlockDevice(path=archinstall.arguments['harddrive']['path']) + # Temporarily disabling keep_partitions if config file is loaded + archinstall.arguments['harddrive'].keep_partitions = False + # Temporary workaround to make Desktop Environments work + archinstall.storage['_desktop_profile'] = archinstall.arguments.get('desktop', None) + if archinstall.arguments.get('profile', None): + archinstall.arguments['profile'] = Profile(installer=None, path=archinstall.arguments['profile']['path']) + perform_installation_steps() diff --git a/profiles/desktop.py b/profiles/desktop.py index 30bb9a6a..631c7f76 100644 --- a/profiles/desktop.py +++ b/profiles/desktop.py @@ -48,7 +48,8 @@ def _prep_function(*args, **kwargs): # Temporarily store the selected desktop profile # in a session-safe location, since this module will get reloaded # the next time it gets executed. - archinstall.storage['_desktop_profile'] = desktop + if '_desktop_profile' not in archinstall.storage.keys(): + archinstall.storage['_desktop_profile'] = desktop profile = archinstall.Profile(None, desktop) # Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered. diff --git a/pyproject.toml b/pyproject.toml index 73c7a876..7afde7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,4 +27,4 @@ include = ["docs/","profiles"] exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"] [tool.flit.metadata.requires-extra] -doc = ["sphinx"] \ No newline at end of file +doc = ["sphinx"] diff --git a/setup.cfg b/setup.cfg index e5d79ef3..79dff732 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ classifers = [options] packages = find: python_requires = >= 3.8 - + [options.packages.find] include = archinstall -- cgit v1.2.3-70-g09d2