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(<installation>)` 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 <anton.feeds@gmail.com>
Co-authored-by: Dylan Taylor <dylan@dylanmtaylor.com>

Co-authored-by: Anton Hvornum <anton@hvornum.se>
Co-authored-by: Anton Hvornum <anton.feeds@gmail.com>

* fixed indent

* removed redundant import

* removed duplicate import

* removed duplicate import

Co-authored-by: Anton Hvornum <anton.feeds@gmail.com>
Co-authored-by: Anton Hvornum <anton@hvornum.se>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
This commit is contained in:
Yash Tripathi 2021-05-20 01:01:58 +05:30 committed by GitHub
parent 49e6cbdc54
commit bbb4599165
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 129 additions and 43 deletions

View File

@ -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. 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 <path to config file> --vars '<space_seperated KEY=VALUE pairs>'
# Help? # Help?
@ -143,7 +153,7 @@ This can be done by installing `pacman -S arch-install-scripts util-linux` local
# losetup -fP ./testimage.img # losetup -fP ./testimage.img
# losetup -a | grep "testimage.img" | awk -F ":" '{print $1}' # losetup -a | grep "testimage.img" | awk -F ":" '{print $1}'
# pip install --upgrade archinstall # 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 # 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.<br> This will create a *5 GB* `testimage.img` and create a loop device which we can use to format and install to.<br>

View File

@ -1,4 +1,6 @@
"""Arch Linux installer - guided, templates etc.""" """Arch Linux installer - guided, templates etc."""
from argparse import ArgumentParser, FileType
from .lib.disk import * from .lib.disk import *
from .lib.exceptions import * from .lib.exceptions import *
from .lib.general import * from .lib.general import *
@ -16,22 +18,46 @@ from .lib.storage import *
from .lib.systemd import * from .lib.systemd import *
from .lib.user_interaction import * from .lib.user_interaction import *
parser = ArgumentParser()
__version__ = "2.2.0.dev1" __version__ = "2.2.0.dev1"
# Basic version of arg.parse() supporting:
# --key=value def initialize_arguments():
# --boolean config = {}
arguments = {} parser.add_argument("--config", nargs="?", help="json config file", type=FileType("r", encoding="UTF-8"))
positionals = [] parser.add_argument("--silent", action="store_true",
for arg in sys.argv[1:]: help="Warning!!! No prompts, ignored if config is not passed")
if '--' == arg[:2]: parser.add_argument("--script", default="guided", nargs="?", help="Script to run for installation", type=str)
if '=' in arg: parser.add_argument("--vars",
key, val = [x.strip() for x in arg[2:].split('=', 1)] metavar="KEY=VALUE",
else: nargs='?',
key, val = arg[2:], True help="Set a number of key-value pairs "
arguments[key] = val "(do not put spaces before or after the = sign). "
else: "If a value contains spaces, you should define "
positionals.append(arg) "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) # 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. # 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')) storage['PROFILE_PATH'].append(os.path.abspath(f'{os.path.dirname(__file__)}/examples'))
if len(sys.argv) == 1:
sys.argv.append('guided')
try: try:
script = Script(sys.argv[1]) script = Script(arguments.get('script', None))
except ProfileNotFound as err: except ProfileNotFound as err:
print(f"Couldn't find file: {err}") print(f"Couldn't find file: {err}")
sys.exit(1) sys.exit(1)

View File

@ -266,7 +266,7 @@ class Partition:
files = len(glob.glob(f"{temporary_mountpoint}/*")) files = len(glob.glob(f"{temporary_mountpoint}/*"))
iterations = 0 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) time.sleep(1)
temporary_path.rmdir() temporary_path.rmdir()

View File

@ -360,7 +360,8 @@ def prerequisite_check():
def reboot(): def reboot():
o = b''.join(SysCommand("/usr/bin/reboot")) o = b''.join(SysCommand("/usr/bin/reboot"))
def pid_exists(pid :int):
def pid_exists(pid: int):
try: try:
return any(subprocess.check_output(['/usr/bin/ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip()) return any(subprocess.check_output(['/usr/bin/ps', '--no-headers', '-o', 'pid', '-p', str(pid)]).strip())
except subprocess.CalledProcessError: except subprocess.CalledProcessError:

View File

@ -1,14 +1,14 @@
import fcntl import fcntl
import os
import logging import logging
import os
import socket import socket
import struct import struct
from collections import OrderedDict from collections import OrderedDict
from .exceptions import * from .exceptions import *
from .general import SysCommand from .general import SysCommand
from .storage import storage
from .output import log from .output import log
from .storage import storage
def get_hw_addr(ifname): def get_hw_addr(ifname):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

View File

@ -5,6 +5,7 @@ from .installer import Installer
from .output import log from .output import log
from .storage import storage from .storage import storage
class Ini: class Ini:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
""" """
@ -103,7 +104,7 @@ class Boot:
return self.session.is_alive() 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] != './': if cmd[0][0] != '/' and cmd[0][:2] != './':
# This check is also done in SysCommand & SysCommandWorker. # This check is also done in SysCommand & SysCommandWorker.
# However, that check is done for `machinectl` and not for our chroot command. # 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) 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] != './': if cmd[0][0] != '/' and cmd[0][:2] != './':
cmd[0] = locate_binary(cmd[0]) cmd[0] = locate_binary(cmd[0])
return SysCommandWorker(["machinectl", "shell", self.container_name, *cmd], *args, **kwargs) return SysCommandWorker(["machinectl", "shell", self.container_name, *cmd], *args, **kwargs)

View File

@ -0,0 +1,35 @@
{
"!root-password": "<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": {
"<username>": {
"!password": "<password>"
}
},
"timezone": "UTC",
"users": {
"<username>": {
"!password": "<password>"
}
}
}

View File

@ -1,11 +1,12 @@
import json import json
import logging import logging
import time
import os import os
import time
import archinstall import archinstall
from archinstall.lib.hardware import has_uefi from archinstall.lib.hardware import has_uefi
from archinstall.lib.networking import check_mirror_reachable from archinstall.lib.networking import check_mirror_reachable
from archinstall.lib.profiles import Profile
if archinstall.arguments.get('help'): if archinstall.arguments.get('help'):
print("See `man archinstall` for 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) archinstall.log(json.dumps(archinstall.arguments, indent=4, sort_keys=True, cls=archinstall.JSON), level=logging.INFO)
print() 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. Issue a final warning before we continue with something un-revertable.
@ -261,7 +263,6 @@ def perform_installation_steps():
mode = archinstall.GPT mode = archinstall.GPT
if has_uefi() is False: if has_uefi() is False:
mode = archinstall.MBR mode = archinstall.MBR
with archinstall.Filesystem(archinstall.arguments['harddrive'], mode) as fs: with archinstall.Filesystem(archinstall.arguments['harddrive'], mode) as fs:
# Wipe the entire drive if the disk flag `keep_partitions`is False. # Wipe the entire drive if the disk flag `keep_partitions`is False.
if archinstall.arguments['harddrive'].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')) fs.find_partition('/').mount(archinstall.storage.get('MOUNT_POINT', '/mnt'))
if has_uefi(): 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')) perform_installation(archinstall.storage.get('MOUNT_POINT', '/mnt'))
@ -381,12 +382,13 @@ def perform_installation(mountpoint):
exit(1) exit(1)
installation.log("For post-installation tips, see https://wiki.archlinux.org/index.php/Installation_guide#Post-installation", fg="yellow") 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 not archinstall.arguments.get('silent'):
if choice.lower() in ("y", ""): choice = input("Would you like to chroot into the newly created installation and perform post-installation configuration? [Y/n] ")
try: if choice.lower() in ("y", ""):
installation.drop_to_shell() try:
except: installation.drop_to_shell()
pass except:
pass
# For support reasons, we'll log the disk layout post installation (crash or no crash) # 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) 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") 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) 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() perform_installation_steps()

View File

@ -48,7 +48,8 @@ def _prep_function(*args, **kwargs):
# Temporarily store the selected desktop profile # Temporarily store the selected desktop profile
# in a session-safe location, since this module will get reloaded # in a session-safe location, since this module will get reloaded
# the next time it gets executed. # 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) profile = archinstall.Profile(None, desktop)
# Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered. # Loading the instructions with a custom namespace, ensures that a __name__ comparison is never triggered.

View File

@ -27,4 +27,4 @@ include = ["docs/","profiles"]
exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"] exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"]
[tool.flit.metadata.requires-extra] [tool.flit.metadata.requires-extra]
doc = ["sphinx"] doc = ["sphinx"]

View File

@ -22,7 +22,7 @@ classifers =
[options] [options]
packages = find: packages = find:
python_requires = >= 3.8 python_requires = >= 3.8
[options.packages.find] [options.packages.find]
include = include =
archinstall archinstall