Merge branch 'master' of github.com:archlinux/archinstall

This commit is contained in:
Anton Hvornum 2022-01-14 08:11:30 +01:00
commit 4bd07ea19f
42 changed files with 1763 additions and 934 deletions

12
.github/workflows/bandit.yaml vendored Normal file
View File

@ -0,0 +1,12 @@
on: [ push, pull_request ]
name: Bandit security checkup
jobs:
flake8:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu bandit
- name: Security checkup with Bandit
run: bandit -r archinstall || exit 0

14
.github/workflows/flake8.yaml vendored Normal file
View File

@ -0,0 +1,14 @@
on: [ push, pull_request ]
name: flake8 linting (15 ignores)
jobs:
flake8:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python python-pip
- run: python -m pip install --upgrade pip
- run: pip install flake8
- name: Lint with flake8
run: flake8

View File

@ -1,36 +0,0 @@
on: [ push, pull_request ]
name: Lint Python and Find Syntax Errors
jobs:
mypy:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python mypy
- name: run mypy
run: mypy . --ignore-missing-imports || exit 0
flake8:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python python-pip
- run: python -m pip install --upgrade pip
- run: pip install flake8
- name: Lint with flake8
run: flake8 # See the .flake8 file for runtime parameters
pytest:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python python-pip
- run: python -m pip install --upgrade pip
- run: pip install pytest
# TODO: Add tests and enable pytest checks.
# - name: Test with pytest
# run: |
# pytest

16
.github/workflows/mypy.yaml vendored Normal file
View File

@ -0,0 +1,16 @@
on: [ push, pull_request ]
name: mypy type checking
jobs:
mypy:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python mypy python-pip
- run: python -m pip install --upgrade pip
- run: pip install fastapi pydantic
- run: python --version
- run: mypy --version
- name: run mypy
run: mypy --strict --module archinstall || exit 0

15
.github/workflows/pytest.yaml vendored Normal file
View File

@ -0,0 +1,15 @@
on: [ push, pull_request ]
name: pytest test validation
jobs:
pytest:
runs-on: ubuntu-latest
container:
image: archlinux:latest
options: --privileged
steps:
- uses: actions/checkout@v2
- run: pacman --noconfirm -Syu python python-pip qemu gcc
- run: python -m pip install --upgrade pip
- run: pip install pytest
- name: Test with pytest
run: python -m pytest || exit 0

View File

@ -3,7 +3,7 @@
<!-- </div> --> <!-- </div> -->
# Arch Installer # Arch Installer
[![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/lint-python.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/lint-python.yaml) [![Lint Python and Find Syntax Errors](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml/badge.svg)](https://github.com/archlinux/archinstall/actions/workflows/flake8.yaml)
Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist. Just another guided/automated [Arch Linux](https://wiki.archlinux.org/index.php/Arch_Linux) installer with a twist.
The installer also doubles as a python library to install Arch Linux and manage services, packages and other things inside the installed system *(Usually from a live medium)*. The installer also doubles as a python library to install Arch Linux and manage services, packages and other things inside the installed system *(Usually from a live medium)*.
@ -11,7 +11,7 @@ The installer also doubles as a python library to install Arch Linux and manage
* archinstall [discord](https://discord.gg/cqXU88y) server * archinstall [discord](https://discord.gg/cqXU88y) server
* archinstall [matrix.org](https://app.element.io/#/room/#archinstall:matrix.org) channel * archinstall [matrix.org](https://app.element.io/#/room/#archinstall:matrix.org) channel
* archinstall [#archinstall@irc.libera.chat](irc://#archinstall@irc.libera.chat:6697) * archinstall [#archinstall@irc.libera.chat](irc://#archinstall@irc.libera.chat:6697)
* archinstall [documentation](https://archinstall.readthedocs.io/en/latest/index.html) * archinstall [documentation](https://archinstall.readthedocs.io/)
# Installation & Usage # Installation & Usage

View File

@ -21,6 +21,8 @@ from .lib.storage import *
from .lib.systemd import * from .lib.systemd import *
from .lib.user_interaction import * from .lib.user_interaction import *
from .lib.menu import Menu from .lib.menu import Menu
from .lib.menu.selection_menu import GlobalMenu
parser = ArgumentParser() parser = ArgumentParser()

View File

@ -1,14 +1,20 @@
from __future__ import annotations
import os import os
import json import json
import logging import logging
import time import time
from typing import Optional, Dict, Any, Iterator, Tuple, List, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .partition import Partition
from ..exceptions import DiskError from ..exceptions import DiskError
from ..output import log from ..output import log
from ..general import SysCommand from ..general import SysCommand
from ..storage import storage from ..storage import storage
class BlockDevice: class BlockDevice:
def __init__(self, path, info=None): def __init__(self, path :str, info :Optional[Dict[str, Any]] = None):
if not info: if not info:
from .helpers import all_disks from .helpers import all_disks
# If we don't give any information, we need to auto-fill it. # If we don't give any information, we need to auto-fill it.
@ -24,32 +30,32 @@ class BlockDevice:
# It's actually partition-encryption, but for future-proofing this # It's actually partition-encryption, but for future-proofing this
# I'm placing the encryption password on a BlockDevice level. # I'm placing the encryption password on a BlockDevice level.
def __repr__(self, *args, **kwargs): def __repr__(self, *args :str, **kwargs :str) -> str:
return f"BlockDevice({self.device_or_backfile}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})" return f"BlockDevice({self.device_or_backfile}, size={self.size}GB, free_space={'+'.join(part[2] for part in self.free_space)}, bus_type={self.bus_type})"
def __iter__(self): def __iter__(self) -> Iterator[Partition]:
for partition in self.partitions: for partition in self.partitions:
yield self.partitions[partition] yield self.partitions[partition]
def __getitem__(self, key, *args, **kwargs): def __getitem__(self, key :str, *args :str, **kwargs :str) -> Any:
if key not in self.info: if key not in self.info:
raise KeyError(f'{self} does not contain information: "{key}"') raise KeyError(f'{self} does not contain information: "{key}"')
return self.info[key] return self.info[key]
def __len__(self): def __len__(self) -> int:
return len(self.partitions) return len(self.partitions)
def __lt__(self, left_comparitor): def __lt__(self, left_comparitor :'BlockDevice') -> bool:
return self.path < left_comparitor.path return self.path < left_comparitor.path
def json(self): def json(self) -> str:
""" """
json() has precedence over __dump__, so this is a way json() has precedence over __dump__, so this is a way
to give less/partial information for user readability. to give less/partial information for user readability.
""" """
return self.path return self.path
def __dump__(self): def __dump__(self) -> Dict[str, Dict[str, Any]]:
return { return {
self.path : { self.path : {
'partuuid' : self.uuid, 'partuuid' : self.uuid,
@ -59,14 +65,14 @@ class BlockDevice:
} }
@property @property
def partition_type(self): def partition_type(self) -> str:
output = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8')) output = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.path}").decode('UTF-8'))
for device in output['blockdevices']: for device in output['blockdevices']:
return device['pttype'] return device['pttype']
@property @property
def device_or_backfile(self): def device_or_backfile(self) -> str:
""" """
Returns the actual device-endpoint of the BlockDevice. Returns the actual device-endpoint of the BlockDevice.
If it's a loop-back-device it returns the back-file, If it's a loop-back-device it returns the back-file,
@ -82,7 +88,7 @@ class BlockDevice:
return self.device return self.device
@property @property
def device(self): def device(self) -> str:
""" """
Returns the device file of the BlockDevice. Returns the device file of the BlockDevice.
If it's a loop-back-device it returns the /dev/X device, If it's a loop-back-device it returns the /dev/X device,
@ -108,7 +114,7 @@ class BlockDevice:
# raise DiskError(f'Selected disk "{full_path}" is not a block device.') # raise DiskError(f'Selected disk "{full_path}" is not a block device.')
@property @property
def partitions(self): def partitions(self) -> Dict[str, Partition]:
from .filesystem import Partition from .filesystem import Partition
self.partprobe() self.partprobe()
@ -133,17 +139,19 @@ class BlockDevice:
return {k: self.part_cache[k] for k in sorted(self.part_cache)} return {k: self.part_cache[k] for k in sorted(self.part_cache)}
@property @property
def partition(self): def partition(self) -> Partition:
all_partitions = self.partitions all_partitions = self.partitions
return [all_partitions[k] for k in all_partitions] return [all_partitions[k] for k in all_partitions]
@property @property
def partition_table_type(self): def partition_table_type(self) -> int:
# TODO: Don't hardcode :)
# Remove if we don't use this function anywhere
from .filesystem import GPT from .filesystem import GPT
return GPT return GPT
@property @property
def uuid(self): def uuid(self) -> str:
log('BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow') log('BlockDevice().uuid is untested!', level=logging.WARNING, fg='yellow')
""" """
Returns the disk UUID as returned by lsblk. Returns the disk UUID as returned by lsblk.
@ -153,7 +161,7 @@ class BlockDevice:
return SysCommand(f'blkid -s PTUUID -o value {self.path}').decode('UTF-8') return SysCommand(f'blkid -s PTUUID -o value {self.path}').decode('UTF-8')
@property @property
def size(self): def size(self) -> float:
from .helpers import convert_size_to_gb from .helpers import convert_size_to_gb
output = json.loads(SysCommand(f"lsblk --json -b -o+SIZE {self.path}").decode('UTF-8')) output = json.loads(SysCommand(f"lsblk --json -b -o+SIZE {self.path}").decode('UTF-8'))
@ -162,21 +170,21 @@ class BlockDevice:
return convert_size_to_gb(device['size']) return convert_size_to_gb(device['size'])
@property @property
def bus_type(self): def bus_type(self) -> str:
output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8'))
for device in output['blockdevices']: for device in output['blockdevices']:
return device['tran'] return device['tran']
@property @property
def spinning(self): def spinning(self) -> bool:
output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8')) output = json.loads(SysCommand(f"lsblk --json -o+ROTA,TRAN {self.path}").decode('UTF-8'))
for device in output['blockdevices']: for device in output['blockdevices']:
return device['rota'] is True return device['rota'] is True
@property @property
def free_space(self): def free_space(self) -> Tuple[str, str, str]:
# NOTE: parted -s will default to `cancel` on prompt, skipping any partition # NOTE: parted -s will default to `cancel` on prompt, skipping any partition
# that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso, # that is "outside" the disk. in /dev/sr0 this is usually the case with Archiso,
# so the free will ignore the ESP partition and just give the "free" space. # so the free will ignore the ESP partition and just give the "free" space.
@ -187,7 +195,7 @@ class BlockDevice:
yield (start, end, size) yield (start, end, size)
@property @property
def largest_free_space(self): def largest_free_space(self) -> List[str]:
info = [] info = []
for space_info in self.free_space: for space_info in self.free_space:
if not info: if not info:
@ -199,7 +207,7 @@ class BlockDevice:
return info return info
@property @property
def first_free_sector(self): def first_free_sector(self) -> str:
if info := self.largest_free_space: if info := self.largest_free_space:
start = info[0] start = info[0]
else: else:
@ -207,29 +215,29 @@ class BlockDevice:
return start return start
@property @property
def first_end_sector(self): def first_end_sector(self) -> str:
if info := self.largest_free_space: if info := self.largest_free_space:
end = info[1] end = info[1]
else: else:
end = f"{self.size}GB" end = f"{self.size}GB"
return end return end
def partprobe(self): def partprobe(self) -> bool:
SysCommand(['partprobe', self.path]) return SysCommand(['partprobe', self.path]).exit_code == 0
def has_partitions(self): def has_partitions(self) -> int:
return len(self.partitions) return len(self.partitions)
def has_mount_point(self, mountpoint): def has_mount_point(self, mountpoint :str) -> bool:
for partition in self.partitions: for partition in self.partitions:
if self.partitions[partition].mountpoint == mountpoint: if self.partitions[partition].mountpoint == mountpoint:
return True return True
return False return False
def flush_cache(self): def flush_cache(self) -> None:
self.part_cache = {} self.part_cache = {}
def get_partition(self, uuid): def get_partition(self, uuid :str) -> Partition:
count = 0 count = 0
while count < 5: while count < 5:
for partition_uuid, partition in self.partitions.items(): for partition_uuid, partition in self.partitions.items():

View File

@ -1,23 +1,30 @@
from __future__ import annotations
import pathlib import pathlib
import glob import glob
import logging import logging
from typing import Union from typing import Union, Dict, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from ..installer import Installer
from .helpers import get_mount_info from .helpers import get_mount_info
from ..exceptions import DiskError from ..exceptions import DiskError
from ..general import SysCommand from ..general import SysCommand
from ..output import log from ..output import log
from .partition import Partition
def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str], force=False) -> bool: def mount_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str], force=False) -> bool:
""" """
This function uses mount to mount a subvolume on a given device, at a given location with a given subvolume name. This function uses mount to mount a subvolume on a given device, at a given location with a given subvolume name.
@installation: archinstall.Installer instance @installation: archinstall.Installer instance
@subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot @subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot
@force: overrides the check for weither or not the subvolume mountpoint is empty or not @force: overrides the check for weither or not the subvolume mountpoint is empty or not
"""
This function is DEPRECATED. you can get the same result creating a partition dict like any other partition, and using the standard mount procedure.
Only change partition['device_instance'].path with the apropriate bind name: real_partition_path[/subvolume_name]
"""
log("function btrfs.mount_subvolume DEPRECATED. See code for alternatives",fg="yellow",level=logging.WARNING)
installation_mountpoint = installation.target installation_mountpoint = installation.target
if type(installation_mountpoint) == str: if type(installation_mountpoint) == str:
installation_mountpoint = pathlib.Path(installation_mountpoint) installation_mountpoint = pathlib.Path(installation_mountpoint)
@ -42,7 +49,7 @@ def mount_subvolume(installation, subvolume_location :Union[pathlib.Path, str],
return SysCommand(f"mount {mount_information['source']} {target} -o subvol=@{subvolume_location}").exit_code == 0 return SysCommand(f"mount {mount_information['source']} {target} -o subvol=@{subvolume_location}").exit_code == 0
def create_subvolume(installation, subvolume_location :Union[pathlib.Path, str]) -> bool: def create_subvolume(installation :Installer, subvolume_location :Union[pathlib.Path, str]) -> bool:
""" """
This function uses btrfs to create a subvolume. This function uses btrfs to create a subvolume.
@ -75,22 +82,38 @@ def create_subvolume(installation, subvolume_location :Union[pathlib.Path, str])
if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0: if (cmd := SysCommand(f"btrfs subvolume create {target}")).exit_code != 0:
raise DiskError(f"Could not create a subvolume at {target}: {cmd}") raise DiskError(f"Could not create a subvolume at {target}: {cmd}")
def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, subvolumes :dict, unlocked_device :dict = None): def _has_option(option :str,options :list) -> bool:
""" auxiliary routine to check if an option is present in a list.
we check if the string appears in one of the options, 'cause it can appear in severl forms (option, option=val,...)
"""
if not options:
return False
for item in options:
if option in item:
return True
return False
def manage_btrfs_subvolumes(installation :Installer,
partition :Dict[str, str],) -> list:
from copy import deepcopy
""" we do the magic with subvolumes in a centralized place """ we do the magic with subvolumes in a centralized place
parameters: parameters:
* the installation object * the installation object
* the partition dictionary entry which represents the physical partition * the partition dictionary entry which represents the physical partition
* mountpoinst, the dictionary which contains all the partititon to be mounted returns
* subvolumes is the dictionary with the names of the subvolumes and its location * mountpoinst, the list which contains all the "new" partititon to be mounted
We expect the partition has been mounted as / , and it to be unmounted after the processing We expect the partition has been mounted as / , and it to be unmounted after the processing
Then we create all the subvolumes inside btrfs as demand Then we create all the subvolumes inside btrfs as demand
We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs We clone then, both the partition dictionary and the object inside it and adapt it to the subvolume needs
Then we add it them to the mountpoints dictionary to be processed as "normal" partitions Then we return a list of "new" partitions to be processed as "normal" partitions
# TODO For encrypted devices we need some special processing prior to it # TODO For encrypted devices we need some special processing prior to it
""" """
# We process each of the pairs <subvolume name: mount point | None | mount info dict> # We process each of the pairs <subvolume name: mount point | None | mount info dict>
# th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list # th mount info dict has an entry for the path of the mountpoint (named 'mountpoint') and 'options' which is a list
# of mount options (or similar used by brtfs) # of mount options (or similar used by brtfs)
mountpoints = []
subvolumes = partition['btrfs']['subvolumes']
for name, right_hand in subvolumes.items(): for name, right_hand in subvolumes.items():
try: try:
# we normalize the subvolume name (getting rid of slash at the start if exists. In our implemenation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use # we normalize the subvolume name (getting rid of slash at the start if exists. In our implemenation has no semantic load - every subvolume is created from the top of the hierarchy- and simplifies its further use
@ -98,7 +121,7 @@ def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, su
name = name[1:] name = name[1:]
# renormalize the right hand. # renormalize the right hand.
location = None location = None
mount_options = [] subvol_options = []
# no contents, so it is not to be mounted # no contents, so it is not to be mounted
if not right_hand: if not right_hand:
location = None location = None
@ -108,38 +131,37 @@ def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, su
# a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿? # a dict. two elements 'mountpoint' (obvious) and and a mount options list ¿?
elif isinstance(right_hand,dict): elif isinstance(right_hand,dict):
location = right_hand.get('mountpoint',None) location = right_hand.get('mountpoint',None)
mount_options = right_hand.get('options',[]) subvol_options = right_hand.get('options',[])
# we create the subvolume # we create the subvolume
create_subvolume(installation,name) create_subvolume(installation,name)
# Make the nodatacow processing now # Make the nodatacow processing now
# It will be the main cause of creation of subvolumes which are not to be mounted # It will be the main cause of creation of subvolumes which are not to be mounted
# it is not an options which can be established by subvolume (but for whole file systems), and can be # it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
if 'nodatacow' in mount_options: if 'nodatacow' in subvol_options:
if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0: if (cmd := SysCommand(f"chattr +C {installation.target}/{name}")).exit_code != 0:
raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}") raise DiskError(f"Could not set nodatacow attribute at {installation.target}/{name}: {cmd}")
# entry is deleted so nodatacow doesn't propagate to the mount options # entry is deleted so nodatacow doesn't propagate to the mount options
del mount_options[mount_options.index('nodatacow')] del subvol_options[subvol_options.index('nodatacow')]
# Make the compress processing now # Make the compress processing now
# it is not an options which can be established by subvolume (but for whole file systems), and can be # it is not an options which can be established by subvolume (but for whole file systems), and can be
# set up via a simple attribute change in a directory (if empty). And here the directories are brand new # set up via a simple attribute change in a directory (if empty). And here the directories are brand new
# in this way only zstd compression is activaded # in this way only zstd compression is activaded
# TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated # TODO WARNING it is not clear if it should be a standard feature, so it might need to be deactivated
if 'compress' in mount_options: if 'compress' in subvol_options:
if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0: if not _has_option('compress',partition.get('filesystem',{}).get('mount_options',[])):
raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}") if (cmd := SysCommand(f"chattr +c {installation.target}/{name}")).exit_code != 0:
# entry is deleted so nodatacow doesn't propagate to the mount options raise DiskError(f"Could not set compress attribute at {installation.target}/{name}: {cmd}")
del mount_options[mount_options.index('compress')] # entry is deleted so compress doesn't propagate to the mount options
del subvol_options[subvol_options.index('compress')]
# END compress processing. # END compress processing.
# we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume # we do not mount if THE basic partition will be mounted or if we exclude explicitly this subvolume
if not partition['mountpoint'] and location is not None: if not partition['mountpoint'] and location is not None:
# we begin to create a fake partition entry. First we copy the original -the one that corresponds to # we begin to create a fake partition entry. First we copy the original -the one that corresponds to
# the primary partition # the primary partition. We make a deepcopy to avoid altering the original content in any case
fake_partition = partition.copy() fake_partition = deepcopy(partition)
# we start to modify entries in the "fake partition" to match the needs of the subvolumes # we start to modify entries in the "fake partition" to match the needs of the subvolumes
#
# to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy # to avoid any chance of entering in a loop (not expected) we delete the list of subvolumes in the copy
# and reset the encryption parameters
del fake_partition['btrfs'] del fake_partition['btrfs']
fake_partition['encrypted'] = False fake_partition['encrypted'] = False
fake_partition['generate-encryption-key-file'] = False fake_partition['generate-encryption-key-file'] = False
@ -147,22 +169,16 @@ def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, su
fake_partition['mountpoint'] = location fake_partition['mountpoint'] = location
# we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path. # we load the name in an attribute called subvolume, but i think it is not needed anymore, 'cause the mount logic uses a different path.
fake_partition['subvolume'] = name fake_partition['subvolume'] = name
# here we add the mount options # here we add the special mount options for the subvolume, if any.
fake_partition['options'] = mount_options # if the original partition['options'] is not a list might give trouble
# Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer. if fake_partition.get('filesystem',{}).get('mount_options',[]):
# We instanciate a new object with following attributes coming / adapted from the instance which was in the primary partition entry (the one we are coping - partition['device_instance'] fake_partition['filesystem']['mount_options'].extend(subvol_options)
# * path, which will be expanded with the subvolume name to use the bind mount syntax the system uses for naming mounted subvolumes
# * size. When the OS queries all the subvolumes share the same size as the full partititon
# * uuid. All the subvolumes on a partition share the same uuid
if not unlocked_device:
fake_partition['device_instance'] = Partition(f"{partition['device_instance'].path}[/{name}]",partition['device_instance'].size,partition['device_instance'].uuid)
else: else:
# for subvolumes IN an encrypted partition we make our device instance from unlocked device instead of the raw partition. fake_partition['filesystem']['mount_options'] = subvol_options
# This time we make a copy (we should to the same above TODO) and alter the path by hand # Here comes the most exotic part. The dictionary attribute 'device_instance' contains an instance of Partition. This instance will be queried along the mount process at the installer.
from copy import copy # As the rest will query there the path of the "partition" to be mounted, we feed it with the bind name needed to mount subvolumes
# KIDS DONT'T DO THIS AT HOME # As we made a deepcopy we have a fresh instance of this object we can manipulate problemless
fake_partition['device_instance'] = copy(unlocked_device) fake_partition['device_instance'].path = f"{partition['device_instance'].path}[/{name}]"
fake_partition['device_instance'].path = f"{unlocked_device.path}[/{name}]"
# we reset this attribute, which holds where the partition is actually mounted. Remember, the physical partition is mounted at this moment and therefore has the value '/'. # we reset this attribute, which holds where the partition is actually mounted. Remember, the physical partition is mounted at this moment and therefore has the value '/'.
# If i don't reset it, process will abort as "already mounted' . # If i don't reset it, process will abort as "already mounted' .
# TODO It works for this purpose, but the fact that this bevahiour can happed, should make think twice # TODO It works for this purpose, but the fact that this bevahiour can happed, should make think twice
@ -170,9 +186,7 @@ def manage_btrfs_subvolumes(installation, partition :dict, mountpoints :dict, su
# #
# Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted, # Well, now that this "fake partition" is ready, we add it to the list of the ones which are to be mounted,
# as "normal" ones # as "normal" ones
mountpoints[fake_partition['mountpoint']] = fake_partition mountpoints.append(fake_partition)
except Exception as e: except Exception as e:
raise e raise e
# if the physical partition has been selected to be mounted, we include it at the list. Remmeber, all the above treatement won't happen except the creation of the subvolume return mountpoints
if partition['mountpoint']:
mountpoints[partition['mountpoint']] = partition

View File

@ -1,7 +1,13 @@
from __future__ import annotations
import time import time
import logging import logging
import json import json
import pathlib import pathlib
from typing import Optional, Dict, Any, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .blockdevice import BlockDevice
from .partition import Partition from .partition import Partition
from .validators import valid_fs_type from .validators import valid_fs_type
from ..exceptions import DiskError from ..exceptions import DiskError
@ -16,24 +22,25 @@ class Filesystem:
# TODO: # TODO:
# When instance of a HDD is selected, check all usages and gracefully unmount them # When instance of a HDD is selected, check all usages and gracefully unmount them
# as well as close any crypto handles. # as well as close any crypto handles.
def __init__(self, blockdevice, mode): def __init__(self, blockdevice :BlockDevice, mode :int):
self.blockdevice = blockdevice self.blockdevice = blockdevice
self.mode = mode self.mode = mode
def __enter__(self, *args, **kwargs): def __enter__(self, *args :str, **kwargs :str) -> 'Filesystem':
return self return self
def __repr__(self): def __repr__(self) -> str:
return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})" return f"Filesystem(blockdevice={self.blockdevice}, mode={self.mode})"
def __exit__(self, *args, **kwargs): def __exit__(self, *args :str, **kwargs :str) -> bool:
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]: if len(args) >= 2 and args[1]:
raise args[1] raise args[1]
SysCommand('sync') SysCommand('sync')
return True return True
def partuuid_to_index(self, uuid): def partuuid_to_index(self, uuid :str) -> Optional[int]:
for i in range(storage['DISK_RETRY_ATTEMPTS']): for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe() self.partprobe()
time.sleep(5) time.sleep(5)
@ -50,7 +57,7 @@ class Filesystem:
raise DiskError(f"Failed to convert PARTUUID {uuid} to a partition index number on blockdevice {self.blockdevice.device}") raise DiskError(f"Failed to convert PARTUUID {uuid} to a partition index number on blockdevice {self.blockdevice.device}")
def load_layout(self, layout :dict): def load_layout(self, layout :Dict[str, Any]) -> None:
from ..luks import luks2 from ..luks import luks2
# If the layout tells us to wipe the drive, we do so # If the layout tells us to wipe the drive, we do so
@ -83,6 +90,8 @@ class Filesystem:
raise ValueError(f"{self}.load_layout() doesn't know how to continue without a new partition definition or a UUID ({partition.get('PARTUUID')}) on the device ({self.blockdevice.get_partition(uuid=partition_uuid)}).") raise ValueError(f"{self}.load_layout() doesn't know how to continue without a new partition definition or a UUID ({partition.get('PARTUUID')}) on the device ({self.blockdevice.get_partition(uuid=partition_uuid)}).")
if partition.get('filesystem', {}).get('format', False): if partition.get('filesystem', {}).get('format', False):
# needed for backward compatibility with the introduction of the new "format_options"
format_options = partition.get('options',[]) + partition.get('filesystem',{}).get('format_options',[])
if partition.get('encrypted', False): if partition.get('encrypted', False):
if not partition.get('!password'): if not partition.get('!password'):
if not storage['arguments'].get('!encryption-password'): if not storage['arguments'].get('!encryption-password'):
@ -93,15 +102,12 @@ class Filesystem:
storage['arguments']['!encryption-password'] = get_password(f"Enter a encryption password for {partition['device_instance']}") storage['arguments']['!encryption-password'] = get_password(f"Enter a encryption password for {partition['device_instance']}")
partition['!password'] = storage['arguments']['!encryption-password'] partition['!password'] = storage['arguments']['!encryption-password']
# to be able to generate an unique name in case the partition will not be mounted
if partition.get('mountpoint',None): if partition.get('mountpoint',None):
ppath = partition['mountpoint'] loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
else: else:
ppath = partition['device_instance'].path loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}"
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(ppath).name}loop"
partition['device_instance'].encrypt(password=partition['!password']) partition['device_instance'].encrypt(password=partition['!password'])
# Immediately unlock the encrypted device to format the inner volume # Immediately unlock the encrypted device to format the inner volume
with luks2(partition['device_instance'], loopdev, partition['!password'], auto_unmount=True) as unlocked_device: with luks2(partition['device_instance'], loopdev, partition['!password'], auto_unmount=True) as unlocked_device:
if not partition.get('format'): if not partition.get('format'):
@ -119,29 +125,29 @@ class Filesystem:
continue continue
break break
unlocked_device.format(partition['filesystem']['format'], options=partition.get('options', [])) unlocked_device.format(partition['filesystem']['format'], options=format_options)
elif partition.get('format', False): elif partition.get('format', False):
partition['device_instance'].format(partition['filesystem']['format'], options=partition.get('options', [])) partition['device_instance'].format(partition['filesystem']['format'], options=format_options)
if partition.get('boot', False): if partition.get('boot', False):
log(f"Marking partition {partition['device_instance']} as bootable.") log(f"Marking partition {partition['device_instance']} as bootable.")
self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on') self.set(self.partuuid_to_index(partition['device_instance'].uuid), 'boot on')
def find_partition(self, mountpoint): def find_partition(self, mountpoint :str) -> Partition:
for partition in self.blockdevice: for partition in self.blockdevice:
if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint: if partition.target_mountpoint == mountpoint or partition.mountpoint == mountpoint:
return partition return partition
def partprobe(self): def partprobe(self) -> bool:
SysCommand(f'bash -c "partprobe"') return SysCommand(f'bash -c "partprobe"').exit_code == 0
def raw_parted(self, string: str): def raw_parted(self, string: str) -> SysCommand:
if (cmd_handle := SysCommand(f'/usr/bin/parted -s {string}')).exit_code != 0: if (cmd_handle := SysCommand(f'/usr/bin/parted -s {string}')).exit_code != 0:
log(f"Parted ended with a bad exit code: {cmd_handle}", level=logging.ERROR, fg="red") log(f"Parted ended with a bad exit code: {cmd_handle}", level=logging.ERROR, fg="red")
time.sleep(0.5) time.sleep(0.5)
return cmd_handle return cmd_handle
def parted(self, string: str): def parted(self, string: str) -> bool:
""" """
Performs a parted execution of the given string Performs a parted execution of the given string
@ -149,16 +155,17 @@ class Filesystem:
:type string: str :type string: str
""" """
if (parted_handle := self.raw_parted(string)).exit_code == 0: if (parted_handle := self.raw_parted(string)).exit_code == 0:
self.partprobe() if self.partprobe():
return True return True
return False
else: else:
raise DiskError(f"Parted failed to add a partition: {parted_handle}") raise DiskError(f"Parted failed to add a partition: {parted_handle}")
def use_entire_disk(self, root_filesystem_type='ext4') -> Partition: def use_entire_disk(self, root_filesystem_type :str = 'ext4') -> Partition:
# TODO: Implement this with declarative profiles instead. # TODO: Implement this with declarative profiles instead.
raise ValueError("Installation().use_entire_disk() has to be re-worked.") raise ValueError("Installation().use_entire_disk() has to be re-worked.")
def add_partition(self, partition_type, start, end, partition_format=None): def add_partition(self, partition_type :str, start :str, end :str, partition_format :Optional[str] = None) -> None:
log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO) log(f'Adding partition to {self.blockdevice}, {start}->{end}', level=logging.INFO)
previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()} previous_partition_uuids = {partition.uuid for partition in self.blockdevice.partitions.values()}
@ -197,14 +204,14 @@ class Filesystem:
log("Add partition is exiting due to excessive wait time",level=logging.INFO) log("Add partition is exiting due to excessive wait time",level=logging.INFO)
raise DiskError(f"New partition never showed up after adding new partition on {self}.") raise DiskError(f"New partition never showed up after adding new partition on {self}.")
def set_name(self, partition: int, name: str): def set_name(self, partition: int, name: str) -> bool:
return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0 return self.parted(f'{self.blockdevice.device} name {partition + 1} "{name}"') == 0
def set(self, partition: int, string: str): def set(self, partition: int, string: str) -> bool:
log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO) log(f"Setting {string} on (parted) partition index {partition+1}", level=logging.INFO)
return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0 return self.parted(f'{self.blockdevice.device} set {partition + 1} {string}') == 0
def parted_mklabel(self, device: str, disk_label: str): def parted_mklabel(self, device: str, disk_label: str) -> bool:
log(f"Creating a new partition label on {device}", level=logging.INFO, fg="yellow") log(f"Creating a new partition label on {device}", level=logging.INFO, fg="yellow")
# Try to unmount devices before attempting to run mklabel # Try to unmount devices before attempting to run mklabel
try: try:

View File

@ -1,10 +1,15 @@
from __future__ import annotations
import json import json
import logging import logging
import os import os
import pathlib import pathlib
import re import re
import time import time
from typing import Union from typing import Union, List, Iterator, Dict, Optional, Any, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .partition import Partition
from .blockdevice import BlockDevice from .blockdevice import BlockDevice
from ..exceptions import SysCallError, DiskError from ..exceptions import SysCallError, DiskError
from ..general import SysCommand from ..general import SysCommand
@ -14,10 +19,10 @@ from ..storage import storage
ROOT_DIR_PATTERN = re.compile('^.*?/devices') ROOT_DIR_PATTERN = re.compile('^.*?/devices')
GIGA = 2 ** 30 GIGA = 2 ** 30
def convert_size_to_gb(size): def convert_size_to_gb(size :Union[int, float]) -> float:
return round(size / GIGA,1) return round(size / GIGA,1)
def sort_block_devices_based_on_performance(block_devices): def sort_block_devices_based_on_performance(block_devices :List[BlockDevice]) -> Dict[BlockDevice, int]:
result = {device: 0 for device in block_devices} result = {device: 0 for device in block_devices}
for device, weight in result.items(): for device, weight in result.items():
@ -35,12 +40,12 @@ def sort_block_devices_based_on_performance(block_devices):
return result return result
def filter_disks_below_size_in_gb(devices, gigabytes): def filter_disks_below_size_in_gb(devices :List[BlockDevice], gigabytes :int) -> Iterator[BlockDevice]:
for disk in devices: for disk in devices:
if disk.size >= gigabytes: if disk.size >= gigabytes:
yield disk yield disk
def select_largest_device(devices, gigabytes, filter_out=None): def select_largest_device(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice:
if not filter_out: if not filter_out:
filter_out = [] filter_out = []
@ -56,7 +61,7 @@ def select_largest_device(devices, gigabytes, filter_out=None):
return max(copy_devices, key=(lambda device : device.size)) return max(copy_devices, key=(lambda device : device.size))
def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None): def select_disk_larger_than_or_close_to(devices :List[BlockDevice], gigabytes :int, filter_out :Optional[List[BlockDevice]] = None) -> BlockDevice:
if not filter_out: if not filter_out:
filter_out = [] filter_out = []
@ -70,7 +75,7 @@ def select_disk_larger_than_or_close_to(devices, gigabytes, filter_out=None):
return min(copy_devices, key=(lambda device : abs(device.size - gigabytes))) return min(copy_devices, key=(lambda device : abs(device.size - gigabytes)))
def convert_to_gigabytes(string): def convert_to_gigabytes(string :str) -> float:
unit = string.strip()[-1] unit = string.strip()[-1]
size = float(string.strip()[:-1]) size = float(string.strip()[:-1])
@ -81,7 +86,7 @@ def convert_to_gigabytes(string):
return size return size
def device_state(name, *args, **kwargs): def device_state(name :str, *args :str, **kwargs :str) -> Optional[bool]:
# Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709 # Based out of: https://askubuntu.com/questions/528690/how-to-get-list-of-all-non-removable-disk-device-names-ssd-hdd-and-sata-ide-onl/528709#528709
if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)): if os.path.isfile('/sys/block/{}/device/block/{}/removable'.format(name, name)):
with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f: with open('/sys/block/{}/device/block/{}/removable'.format(name, name)) as f:
@ -99,7 +104,7 @@ def device_state(name, *args, **kwargs):
return True return True
# lsblk --json -l -n -o path # lsblk --json -l -n -o path
def all_disks(*args, **kwargs): def all_disks(*args :str, **kwargs :str) -> List[BlockDevice]:
kwargs.setdefault("partitions", False) kwargs.setdefault("partitions", False)
drives = {} drives = {}
@ -113,7 +118,7 @@ def all_disks(*args, **kwargs):
return drives return drives
def harddrive(size=None, model=None, fuzzy=False): def harddrive(size :Optional[float] = None, model :Optional[str] = None, fuzzy :bool = False) -> Optional[BlockDevice]:
collection = all_disks() collection = all_disks()
for drive in collection: for drive in collection:
if size and convert_to_gigabytes(collection[drive]['size']) != size: if size and convert_to_gigabytes(collection[drive]['size']) != size:
@ -133,7 +138,7 @@ def split_bind_name(path :Union[pathlib.Path, str]) -> list:
bind_path = None bind_path = None
return device_path,bind_path return device_path,bind_path
def get_mount_info(path :Union[pathlib.Path, str], traverse=False, return_real_path=False) -> dict: def get_mount_info(path :Union[pathlib.Path, str], traverse :bool = False, return_real_path :bool = False) -> Dict[str, Any]:
device_path,bind_path = split_bind_name(path) device_path,bind_path = split_bind_name(path)
for traversal in list(map(str, [str(device_path)] + list(pathlib.Path(str(device_path)).parents))): for traversal in list(map(str, [str(device_path)] + list(pathlib.Path(str(device_path)).parents))):
try: try:
@ -170,7 +175,7 @@ def get_mount_info(path :Union[pathlib.Path, str], traverse=False, return_real_p
return {} return {}
def get_partitions_in_use(mountpoint) -> list: def get_partitions_in_use(mountpoint :str) -> List[Partition]:
from .partition import Partition from .partition import Partition
try: try:
@ -193,7 +198,7 @@ def get_partitions_in_use(mountpoint) -> list:
return mounts return mounts
def get_filesystem_type(path): def get_filesystem_type(path :str) -> Optional[str]:
device_name, bind_name = split_bind_name(path) device_name, bind_name = split_bind_name(path)
try: try:
return SysCommand(f"blkid -o value -s TYPE {device_name}").decode('UTF-8').strip() return SysCommand(f"blkid -o value -s TYPE {device_name}").decode('UTF-8').strip()
@ -201,10 +206,10 @@ def get_filesystem_type(path):
return None return None
def disk_layouts(): def disk_layouts() -> Optional[Dict[str, Any]]:
try: try:
if (handle := SysCommand("lsblk -f -o+TYPE,SIZE -J")).exit_code == 0: if (handle := SysCommand("lsblk -f -o+TYPE,SIZE -J")).exit_code == 0:
return json.loads(handle.decode('UTF-8')) return {str(key): val for key, val in json.loads(handle.decode('UTF-8')).items()}
else: else:
log(f"Could not return disk layouts: {handle}", level=logging.WARNING, fg="yellow") log(f"Could not return disk layouts: {handle}", level=logging.WARNING, fg="yellow")
return None return None
@ -216,20 +221,22 @@ def disk_layouts():
return None return None
def encrypted_partitions(blockdevices :dict) -> bool: def encrypted_partitions(blockdevices :Dict[str, Any]) -> bool:
for partition in blockdevices.values(): for partition in blockdevices.values():
if partition.get('encrypted', False): if partition.get('encrypted', False):
yield partition yield partition
def find_partition_by_mountpoint(block_devices, relative_mountpoint :str): def find_partition_by_mountpoint(block_devices :List[BlockDevice], relative_mountpoint :str) -> Partition:
for device in block_devices: for device in block_devices:
for partition in block_devices[device]['partitions']: for partition in block_devices[device]['partitions']:
if partition.get('mountpoint', None) == relative_mountpoint: if partition.get('mountpoint', None) == relative_mountpoint:
return partition return partition
def partprobe(): def partprobe() -> bool:
SysCommand(f'bash -c "partprobe"') if SysCommand(f'bash -c "partprobe"').exit_code == 0:
time.sleep(5) time.sleep(5) # TODO: Remove, we should be relying on blkid instead of lsblk
return True
return False
def convert_device_to_uuid(path :str) -> str: def convert_device_to_uuid(path :str) -> str:
device_name, bind_name = split_bind_name(path) device_name, bind_name = split_bind_name(path)

View File

@ -5,7 +5,8 @@ import logging
import json import json
import os import os
import hashlib import hashlib
from typing import Optional from typing import Optional, Dict, Any, List, Union
from .blockdevice import BlockDevice from .blockdevice import BlockDevice
from .helpers import get_mount_info, get_filesystem_type, convert_size_to_gb, split_bind_name from .helpers import get_mount_info, get_filesystem_type, convert_size_to_gb, split_bind_name
from ..storage import storage from ..storage import storage
@ -15,7 +16,15 @@ from ..general import SysCommand
class Partition: class Partition:
def __init__(self, path: str, block_device: BlockDevice, part_id=None, filesystem=None, mountpoint=None, encrypted=False, autodetect_filesystem=True): def __init__(self,
path: str,
block_device: BlockDevice,
part_id :Optional[str] = None,
filesystem :Optional[str] = None,
mountpoint :Optional[str] = None,
encrypted :bool = False,
autodetect_filesystem :bool = True):
if not part_id: if not part_id:
part_id = os.path.basename(path) part_id = os.path.basename(path)
@ -50,14 +59,16 @@ class Partition:
if self.filesystem == 'crypto_LUKS': if self.filesystem == 'crypto_LUKS':
self.encrypted = True self.encrypted = True
def __lt__(self, left_comparitor): def __lt__(self, left_comparitor :BlockDevice) -> bool:
if type(left_comparitor) == Partition: if type(left_comparitor) == Partition:
left_comparitor = left_comparitor.path left_comparitor = left_comparitor.path
else: else:
left_comparitor = str(left_comparitor) left_comparitor = str(left_comparitor)
return self.path < left_comparitor # Not quite sure the order here is correct. But /dev/nvme0n1p1 comes before /dev/nvme0n1p5 so seems correct.
def __repr__(self, *args, **kwargs): # The goal is to check if /dev/nvme0n1p1 comes before /dev/nvme0n1p5
return self.path < left_comparitor
def __repr__(self, *args :str, **kwargs :str) -> str:
mount_repr = '' mount_repr = ''
if self.mountpoint: if self.mountpoint:
mount_repr = f", mounted={self.mountpoint}" mount_repr = f", mounted={self.mountpoint}"
@ -69,7 +80,7 @@ class Partition:
else: else:
return f'Partition(path={self.path}, size={self.size}, PARTUUID={self._safe_uuid}, fs={self.filesystem}{mount_repr})' return f'Partition(path={self.path}, size={self.size}, PARTUUID={self._safe_uuid}, fs={self.filesystem}{mount_repr})'
def __dump__(self): def __dump__(self) -> Dict[str, Any]:
return { return {
'type': 'primary', 'type': 'primary',
'PARTUUID': self._safe_uuid, 'PARTUUID': self._safe_uuid,
@ -86,14 +97,14 @@ class Partition:
} }
@property @property
def sector_size(self): def sector_size(self) -> Optional[int]:
output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.device_path}").decode('UTF-8')) output = json.loads(SysCommand(f"lsblk --json -o+LOG-SEC {self.device_path}").decode('UTF-8'))
for device in output['blockdevices']: for device in output['blockdevices']:
return device.get('log-sec', None) return device.get('log-sec', None)
@property @property
def start(self): def start(self) -> Optional[str]:
output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8'))
for partition in output.get('partitiontable', {}).get('partitions', []): for partition in output.get('partitiontable', {}).get('partitions', []):
@ -101,7 +112,7 @@ class Partition:
return partition['start'] # * self.sector_size return partition['start'] # * self.sector_size
@property @property
def end(self): def end(self) -> Optional[str]:
# TODO: Verify that the logic holds up, that 'size' is the size without 'start' added to it. # TODO: Verify that the logic holds up, that 'size' is the size without 'start' added to it.
output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8'))
@ -110,7 +121,7 @@ class Partition:
return partition['size'] # * self.sector_size return partition['size'] # * self.sector_size
@property @property
def size(self): def size(self) -> Optional[float]:
for i in range(storage['DISK_RETRY_ATTEMPTS']): for i in range(storage['DISK_RETRY_ATTEMPTS']):
self.partprobe() self.partprobe()
@ -123,7 +134,7 @@ class Partition:
time.sleep(storage['DISK_TIMEOUTS']) time.sleep(storage['DISK_TIMEOUTS'])
@property @property
def boot(self): def boot(self) -> bool:
output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8')) output = json.loads(SysCommand(f"sfdisk --json {self.block_device.path}").decode('UTF-8'))
# Get the bootable flag from the sfdisk output: # Get the bootable flag from the sfdisk output:
@ -143,7 +154,7 @@ class Partition:
return False return False
@property @property
def partition_type(self): def partition_type(self) -> Optional[str]:
lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.device_path}").decode('UTF-8')) lsblk = json.loads(SysCommand(f"lsblk --json -o+PTTYPE {self.device_path}").decode('UTF-8'))
for device in lsblk['blockdevices']: for device in lsblk['blockdevices']:
@ -179,19 +190,19 @@ class Partition:
return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip() return SysCommand(f'blkid -s PARTUUID -o value {self.device_path}').decode('UTF-8').strip()
@property @property
def encrypted(self): def encrypted(self) -> Union[bool, None]:
return self._encrypted return self._encrypted
@encrypted.setter @encrypted.setter
def encrypted(self, value: bool): def encrypted(self, value: bool) -> None:
self._encrypted = value self._encrypted = value
@property @property
def parent(self): def parent(self) -> str:
return self.real_device return self.real_device
@property @property
def real_device(self): def real_device(self) -> str:
for blockdevice in json.loads(SysCommand('lsblk -J').decode('UTF-8'))['blockdevices']: for blockdevice in json.loads(SysCommand('lsblk -J').decode('UTF-8'))['blockdevices']:
if parent := self.find_parent_of(blockdevice, os.path.basename(self.device_path)): if parent := self.find_parent_of(blockdevice, os.path.basename(self.device_path)):
return f"/dev/{parent}" return f"/dev/{parent}"
@ -199,25 +210,27 @@ class Partition:
return self.path return self.path
@property @property
def device_path(self): def device_path(self) -> str:
""" for bind mounts returns the phisical path of the partition """ for bind mounts returns the phisical path of the partition
""" """
device_path, bind_name = split_bind_name(self.path) device_path, bind_name = split_bind_name(self.path)
return device_path return device_path
@property @property
def bind_name(self): def bind_name(self) -> str:
""" for bind mounts returns the bind name (subvolume path). """ for bind mounts returns the bind name (subvolume path).
Returns none if this property does not exist Returns none if this property does not exist
""" """
device_path, bind_name = split_bind_name(self.path) device_path, bind_name = split_bind_name(self.path)
return bind_name return bind_name
def partprobe(self): def partprobe(self) -> bool:
SysCommand(f'bash -c "partprobe"') if SysCommand(f'bash -c "partprobe"').exit_code == 0:
time.sleep(1) time.sleep(1)
return True
return False
def detect_inner_filesystem(self, password): def detect_inner_filesystem(self, password :str) -> Optional[str]:
log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO) log(f'Trying to detect inner filesystem format on {self} (This might take a while)', level=logging.INFO)
from ..luks import luks2 from ..luks import luks2
@ -227,7 +240,7 @@ class Partition:
except SysCallError: except SysCallError:
return None return None
def has_content(self): def has_content(self) -> bool:
fs_type = get_filesystem_type(self.path) fs_type = get_filesystem_type(self.path)
if not fs_type or "swap" in fs_type: if not fs_type or "swap" in fs_type:
return False return False
@ -248,7 +261,7 @@ class Partition:
return True if files > 0 else False return True if files > 0 else False
def encrypt(self, *args, **kwargs): def encrypt(self, *args :str, **kwargs :str) -> str:
""" """
A wrapper function for luks2() instances and the .encrypt() method of that instance. A wrapper function for luks2() instances and the .encrypt() method of that instance.
""" """
@ -257,7 +270,7 @@ class Partition:
handle = luks2(self, None, None) handle = luks2(self, None, None)
return handle.encrypt(self, *args, **kwargs) return handle.encrypt(self, *args, **kwargs)
def format(self, filesystem=None, path=None, log_formatting=True, options=[]): def format(self, filesystem :Optional[str] = None, path :Optional[str] = None, log_formatting :bool = True, options :List[str] = []) -> bool:
""" """
Format can be given an overriding path, for instance /dev/null to test Format can be given an overriding path, for instance /dev/null to test
the formatting functionality and in essence the support for the given filesystem. the formatting functionality and in essence the support for the given filesystem.
@ -286,9 +299,8 @@ class Partition:
elif filesystem == 'fat32': elif filesystem == 'fat32':
options = ['-F32'] + options options = ['-F32'] + options
mkfs = SysCommand(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}").decode('UTF-8') if (handle := SysCommand(f"/usr/bin/mkfs.vfat {' '.join(options)} {path}")).exit_code != 0:
if ('mkfs.fat' not in mkfs and 'mkfs.vfat' not in mkfs) or 'command not found' in mkfs: raise DiskError(f"Could not format {path} with {filesystem} because: {handle.decode('UTF-8')}")
raise DiskError(f"Could not format {path} with {filesystem} because: {mkfs}")
self.filesystem = filesystem self.filesystem = filesystem
elif filesystem == 'ext4': elif filesystem == 'ext4':
@ -342,7 +354,7 @@ class Partition:
return True return True
def find_parent_of(self, data, name, parent=None): def find_parent_of(self, data :Dict[str, Any], name :str, parent :Optional[str] = None) -> Optional[str]:
if data['name'] == name: if data['name'] == name:
return parent return parent
elif 'children' in data: elif 'children' in data:
@ -350,7 +362,7 @@ class Partition:
if parent := self.find_parent_of(child, name, parent=data['name']): if parent := self.find_parent_of(child, name, parent=data['name']):
return parent return parent
def mount(self, target, fs=None, options=''): def mount(self, target :str, fs :Optional[str] = None, options :str = '') -> bool:
if not self.mountpoint: if not self.mountpoint:
log(f'Mounting {self} to {target}', level=logging.INFO) log(f'Mounting {self} to {target}', level=logging.INFO)
if not fs: if not fs:
@ -386,25 +398,24 @@ class Partition:
self.mountpoint = target self.mountpoint = target
return True return True
def unmount(self): return False
try:
SysCommand(f"/usr/bin/umount {self.path}")
except SysCallError as err:
exit_code = err.exit_code
# Without to much research, it seams that low error codes are errors. def unmount(self) -> bool:
# And above 8k is indicators such as "/dev/x not mounted.". worker = SysCommand(f"/usr/bin/umount {self.path}")
# So anything in between 0 and 8k are errors (?).
if 0 < exit_code < 8000: # Without to much research, it seams that low error codes are errors.
raise err # And above 8k is indicators such as "/dev/x not mounted.".
# So anything in between 0 and 8k are errors (?).
if 0 < worker.exit_code < 8000:
raise SysCallError(f"Could not unmount {self.path} properly: {worker}", exit_code=worker.exit_code)
self.mountpoint = None self.mountpoint = None
return True return True
def umount(self): def umount(self) -> bool:
return self.unmount() return self.unmount()
def filesystem_supported(self): def filesystem_supported(self) -> bool:
""" """
The support for a filesystem (this partition) is tested by calling The support for a filesystem (this partition) is tested by calling
partition.format() with a path set to '/dev/null' which returns two exceptions: partition.format() with a path set to '/dev/null' which returns two exceptions:
@ -420,7 +431,7 @@ class Partition:
return True return True
def get_mount_fs_type(fs): def get_mount_fs_type(fs :str) -> str:
if fs == 'ntfs': if fs == 'ntfs':
return 'ntfs3' # Needed to use the Paragon R/W NTFS driver return 'ntfs3' # Needed to use the Paragon R/W NTFS driver
elif fs == 'fat32': elif fs == 'fat32':

View File

@ -1,14 +1,25 @@
from __future__ import annotations
import logging import logging
from typing import Optional, Dict, Any, List, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .blockdevice import BlockDevice
from .helpers import sort_block_devices_based_on_performance, select_largest_device, select_disk_larger_than_or_close_to from .helpers import sort_block_devices_based_on_performance, select_largest_device, select_disk_larger_than_or_close_to
from ..hardware import has_uefi
from ..output import log from ..output import log
def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_options=False): def suggest_single_disk_layout(block_device :BlockDevice,
default_filesystem :Optional[str] = None,
advanced_options :bool = False) -> Dict[str, Any]:
if not default_filesystem: if not default_filesystem:
from ..user_interaction import ask_for_main_filesystem_format from ..user_interaction import ask_for_main_filesystem_format
default_filesystem = ask_for_main_filesystem_format(advanced_options) default_filesystem = ask_for_main_filesystem_format(advanced_options)
MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb MIN_SIZE_TO_ALLOW_HOME_PART = 40 # GiB
using_subvolumes = False using_subvolumes = False
using_home_partition = False
if default_filesystem == 'btrfs': if default_filesystem == 'btrfs':
using_subvolumes = input('Would you like to use BTRFS subvolumes with a default structure? (Y/n): ').strip().lower() in ('', 'y', 'yes') using_subvolumes = input('Would you like to use BTRFS subvolumes with a default structure? (Y/n): ').strip().lower() in ('', 'y', 'yes')
@ -20,11 +31,19 @@ def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_o
} }
} }
# Used for reference: https://wiki.archlinux.org/title/partitioning
# 2 MiB is unallocated for GRUB on BIOS. Potentially unneeded for
# other bootloaders?
# TODO: On BIOS, /boot partition is only needed if the drive will
# be encrypted, otherwise it is not recommended. We should probably
# add a check for whether the drive will be encrypted or not.
layout[block_device.path]['partitions'].append({ layout[block_device.path]['partitions'].append({
# Boot # Boot
"type" : "primary", "type" : "primary",
"start" : "5MB", "start" : "3MiB",
"size" : "513MB", "size" : "203MiB",
"boot" : True, "boot" : True,
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
@ -33,10 +52,18 @@ def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_o
"format" : "fat32" "format" : "fat32"
} }
}) })
# Increase the UEFI partition if UEFI is detected.
# Also re-align the start to 1MiB since we don't need the first sectors
# like we do in MBR layouts where the boot loader is installed traditionally.
if has_uefi():
layout[block_device.path]['partitions'][-1]['start'] = '1MiB'
layout[block_device.path]['partitions'][-1]['size'] = '512MiB'
layout[block_device.path]['partitions'].append({ layout[block_device.path]['partitions'].append({
# Root # Root
"type" : "primary", "type" : "primary",
"start" : "518MB", "start" : "206MiB",
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
"mountpoint" : "/", "mountpoint" : "/",
@ -45,13 +72,20 @@ def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_o
} }
}) })
if has_uefi():
layout[block_device.path]['partitions'][-1]['start'] = '513MiB'
if not using_subvolumes and block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART:
using_home_partition = input('Would you like to create a separate partition for /home? (Y/n): ').strip().lower() in ('', 'y', 'yes')
# Set a size for / (/root) # Set a size for / (/root)
if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART: if using_subvolumes or block_device.size < MIN_SIZE_TO_ALLOW_HOME_PART or not using_home_partition:
# We'll use subvolumes # We'll use subvolumes
# Or the disk size is too small to allow for a separate /home # Or the disk size is too small to allow for a separate /home
# Or the user doesn't want to create a separate partition for /home
layout[block_device.path]['partitions'][-1]['size'] = '100%' layout[block_device.path]['partitions'][-1]['size'] = '100%'
else: else:
layout[block_device.path]['partitions'][-1]['size'] = f"{min(block_device.size, 20)}GB" layout[block_device.path]['partitions'][-1]['size'] = f"{min(block_device.size, 20)}GiB"
if default_filesystem == 'btrfs' and using_subvolumes: if default_filesystem == 'btrfs' and using_subvolumes:
# if input('Do you want to use a recommended structure? (Y/n): ').strip().lower() in ('', 'y', 'yes'): # if input('Do you want to use a recommended structure? (Y/n): ').strip().lower() in ('', 'y', 'yes'):
@ -69,17 +103,17 @@ def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_o
# else: # else:
# pass # ... implement a guided setup # pass # ... implement a guided setup
elif block_device.size >= MIN_SIZE_TO_ALLOW_HOME_PART: elif using_home_partition:
# If we don't want to use subvolumes, # If we don't want to use subvolumes,
# But we want to be able to re-use data between re-installs.. # But we want to be able to re-use data between re-installs..
# A second partition for /home would be nice if we have the space for it # A second partition for /home would be nice if we have the space for it
layout[block_device.path]['partitions'].append({ layout[block_device.path]['partitions'].append({
# Home # Home
"type" : "primary", "type" : "primary",
"start" : f"{min(block_device.size, 20)}GiB",
"size" : "100%",
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
"start" : f"{min(block_device.size+0.5, 20.5)}GB",
"size" : "100%",
"mountpoint" : "/home", "mountpoint" : "/home",
"filesystem" : { "filesystem" : {
"format" : default_filesystem "format" : default_filesystem
@ -89,7 +123,10 @@ def suggest_single_disk_layout(block_device, default_filesystem=None, advanced_o
return layout return layout
def suggest_multi_disk_layout(block_devices, default_filesystem=None, advanced_options=False): def suggest_multi_disk_layout(block_devices :List[BlockDevice],
default_filesystem :Optional[str] = None,
advanced_options :bool = False) -> Dict[str, Any]:
if not default_filesystem: if not default_filesystem:
from ..user_interaction import ask_for_main_filesystem_format from ..user_interaction import ask_for_main_filesystem_format
default_filesystem = ask_for_main_filesystem_format(advanced_options) default_filesystem = ask_for_main_filesystem_format(advanced_options)
@ -98,8 +135,8 @@ def suggest_multi_disk_layout(block_devices, default_filesystem=None, advanced_o
# https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/ # https://www.reddit.com/r/btrfs/comments/m287gp/partition_strategy_for_two_physical_disks/
# https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/ # https://www.reddit.com/r/btrfs/comments/9us4hr/what_is_your_btrfs_partitionsubvolumes_scheme/
MIN_SIZE_TO_ALLOW_HOME_PART = 40 # Gb MIN_SIZE_TO_ALLOW_HOME_PART = 40 # GiB
ARCH_LINUX_INSTALLED_SIZE = 20 # Gb, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size? ARCH_LINUX_INSTALLED_SIZE = 20 # GiB, rough estimate taking in to account user desktops etc. TODO: Catch user packages to detect size?
block_devices = sort_block_devices_based_on_performance(block_devices).keys() block_devices = sort_block_devices_based_on_performance(block_devices).keys()
@ -119,11 +156,13 @@ def suggest_multi_disk_layout(block_devices, default_filesystem=None, advanced_o
}, },
} }
# TODO: Same deal as with the single disk layout, we should
# probably check if the drive will be encrypted.
layout[root_device.path]['partitions'].append({ layout[root_device.path]['partitions'].append({
# Boot # Boot
"type" : "primary", "type" : "primary",
"start" : "5MB", "start" : "3MiB",
"size" : "513MB", "size" : "203MiB",
"boot" : True, "boot" : True,
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
@ -132,26 +171,33 @@ def suggest_multi_disk_layout(block_devices, default_filesystem=None, advanced_o
"format" : "fat32" "format" : "fat32"
} }
}) })
if has_uefi():
layout[root_device.path]['partitions'][-1]['start'] = '1MiB'
layout[root_device.path]['partitions'][-1]['size'] = '512MiB'
layout[root_device.path]['partitions'].append({ layout[root_device.path]['partitions'].append({
# Root # Root
"type" : "primary", "type" : "primary",
"start" : "518MB", "start" : "206MiB",
"size" : "100%",
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
"size" : "100%",
"mountpoint" : "/", "mountpoint" : "/",
"filesystem" : { "filesystem" : {
"format" : default_filesystem "format" : default_filesystem
} }
}) })
if has_uefi():
layout[root_device.path]['partitions'][-1]['start'] = '513MiB'
layout[home_device.path]['partitions'].append({ layout[home_device.path]['partitions'].append({
# Home # Home
"type" : "primary", "type" : "primary",
"start" : "1MiB",
"size" : "100%",
"encrypted" : False, "encrypted" : False,
"format" : True, "format" : True,
"start" : "5MB",
"size" : "100%",
"mountpoint" : "/home", "mountpoint" : "/home",
"filesystem" : { "filesystem" : {
"format" : default_filesystem "format" : default_filesystem

View File

@ -1,4 +1,6 @@
def valid_parted_position(pos :str): from typing import List
def valid_parted_position(pos :str) -> bool:
if not len(pos): if not len(pos):
return False return False
@ -17,7 +19,7 @@ def valid_parted_position(pos :str):
return False return False
def fs_types(): def fs_types() -> List[str]:
# https://www.gnu.org/software/parted/manual/html_node/mkpart.html # https://www.gnu.org/software/parted/manual/html_node/mkpart.html
# Above link doesn't agree with `man parted` /mkpart documentation: # Above link doesn't agree with `man parted` /mkpart documentation:
""" """

View File

@ -17,12 +17,16 @@ class ProfileError(BaseException):
class SysCallError(BaseException): class SysCallError(BaseException):
def __init__(self, message :str, exit_code :Optional[int]) -> None: def __init__(self, message :str, exit_code :Optional[int] = None) -> None:
super(SysCallError, self).__init__(message) super(SysCallError, self).__init__(message)
self.message = message self.message = message
self.exit_code = exit_code self.exit_code = exit_code
class PermissionError(BaseException):
pass
class ProfileNotFound(BaseException): class ProfileNotFound(BaseException):
pass pass

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import hashlib import hashlib
import json import json
import logging import logging
@ -9,7 +10,10 @@ import string
import sys import sys
import time import time
from datetime import datetime, date from datetime import datetime, date
from typing import Callable, Optional, Dict, Any, List, Union, Iterator from typing import Callable, Optional, Dict, Any, List, Union, Iterator, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .installer import Installer
if sys.platform == 'linux': if sys.platform == 'linux':
from select import epoll, EPOLLIN, EPOLLHUP from select import epoll, EPOLLIN, EPOLLHUP
@ -46,14 +50,14 @@ from .exceptions import RequirementError, SysCallError
from .output import log from .output import log
from .storage import storage from .storage import storage
def gen_uid(entropy_length=256): def gen_uid(entropy_length :int = 256) -> str:
return hashlib.sha512(os.urandom(entropy_length)).hexdigest() return hashlib.sha512(os.urandom(entropy_length)).hexdigest()
def generate_password(length=64): def generate_password(length :int = 64) -> str:
haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace haystack = string.printable # digits, ascii_letters, punctiation (!"#$[] etc) and whitespace
return ''.join(secrets.choice(haystack) for i in range(length)) return ''.join(secrets.choice(haystack) for i in range(length))
def multisplit(s, splitters): def multisplit(s :str, splitters :List[str]) -> str:
s = [s, ] s = [s, ]
for key in splitters: for key in splitters:
ns = [] ns = []
@ -77,12 +81,12 @@ def locate_binary(name :str) -> str:
raise RequirementError(f"Binary {name} does not exist.") raise RequirementError(f"Binary {name} does not exist.")
def json_dumps(*args, **kwargs): def json_dumps(*args :str, **kwargs :str) -> str:
return json.dumps(*args, **{**kwargs, 'cls': JSON}) return json.dumps(*args, **{**kwargs, 'cls': JSON})
class JsonEncoder: class JsonEncoder:
@staticmethod @staticmethod
def _encode(obj): def _encode(obj :Any) -> Any:
""" """
This JSON encoder function will try it's best to convert This JSON encoder function will try it's best to convert
any archinstall data structures, instances or variables into any archinstall data structures, instances or variables into
@ -119,7 +123,7 @@ class JsonEncoder:
return obj return obj
@staticmethod @staticmethod
def _unsafe_encode(obj): def _unsafe_encode(obj :Any) -> Any:
""" """
Same as _encode() but it keeps dictionary keys starting with ! Same as _encode() but it keeps dictionary keys starting with !
""" """
@ -141,20 +145,20 @@ class JSON(json.JSONEncoder, json.JSONDecoder):
""" """
A safe JSON encoder that will omit private information in dicts (starting with !) A safe JSON encoder that will omit private information in dicts (starting with !)
""" """
def _encode(self, obj): def _encode(self, obj :Any) -> Any:
return JsonEncoder._encode(obj) return JsonEncoder._encode(obj)
def encode(self, obj): def encode(self, obj :Any) -> Any:
return super(JSON, self).encode(self._encode(obj)) return super(JSON, self).encode(self._encode(obj))
class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder): class UNSAFE_JSON(json.JSONEncoder, json.JSONDecoder):
""" """
UNSAFE_JSON will call/encode and keep private information in dicts (starting with !) UNSAFE_JSON will call/encode and keep private information in dicts (starting with !)
""" """
def _encode(self, obj): def _encode(self, obj :Any) -> Any:
return JsonEncoder._unsafe_encode(obj) return JsonEncoder._unsafe_encode(obj)
def encode(self, obj): def encode(self, obj :Any) -> Any:
return super(UNSAFE_JSON, self).encode(self._encode(obj)) return super(UNSAFE_JSON, self).encode(self._encode(obj))
class SysCommandWorker: class SysCommandWorker:
@ -184,7 +188,8 @@ class SysCommandWorker:
self.cmd = cmd self.cmd = cmd
self.callbacks = callbacks self.callbacks = callbacks
self.peak_output = peak_output self.peak_output = peak_output
self.environment_vars = environment_vars # define the standard locale for command outputs. For now the C ascii one. Can be overriden
self.environment_vars = {'LC_ALL':'C' , **environment_vars}
self.logfile = logfile self.logfile = logfile
self.working_directory = working_directory self.working_directory = working_directory
@ -455,13 +460,17 @@ class SysCommand:
return None return None
def prerequisite_check(): def prerequisite_check() -> bool:
if not os.path.isdir("/sys/firmware/efi"): """
raise RequirementError("Archinstall only supports machines in UEFI mode.") This function is used as a safety check before
continuing with an installation.
Could be anything from checking that /boot is big enough
to check if nvidia hardware exists when nvidia driver was chosen.
"""
return True return True
def reboot(): def reboot():
SysCommand("/usr/bin/reboot") SysCommand("/usr/bin/reboot")
@ -473,12 +482,15 @@ def pid_exists(pid: int) -> bool:
return False return False
def run_custom_user_commands(commands, installation): def run_custom_user_commands(commands :List[str], installation :Installer) -> None:
for index, command in enumerate(commands): for index, command in enumerate(commands):
log(f'Executing custom command "{command}" ...', fg='yellow') log(f'Executing custom command "{command}" ...', level=logging.INFO)
with open(f"{installation.target}/var/tmp/user-command.{index}.sh", "w") as temp_script: with open(f"{installation.target}/var/tmp/user-command.{index}.sh", "w") as temp_script:
temp_script.write(command) temp_script.write(command)
execution_output = SysCommand(f"arch-chroot {installation.target} bash /var/tmp/user-command.{index}.sh") execution_output = SysCommand(f"arch-chroot {installation.target} bash /var/tmp/user-command.{index}.sh")
log(execution_output) log(execution_output)
os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh") os.unlink(f"{installation.target}/var/tmp/user-command.{index}.sh")

View File

@ -1,5 +1,4 @@
import time import time
from typing import Union
import logging import logging
import os import os
import shutil import shutil
@ -7,6 +6,8 @@ import shlex
import pathlib import pathlib
import subprocess import subprocess
import glob import glob
from types import ModuleType
from typing import Union, Dict, Any, List, Optional, Iterator, Mapping
from .disk import get_partitions_in_use, Partition from .disk import get_partitions_in_use, Partition
from .general import SysCommand, generate_password from .general import SysCommand, generate_password
from .hardware import has_uefi, is_vm, cpu_vendor from .hardware import has_uefi, is_vm, cpu_vendor
@ -30,29 +31,29 @@ __accessibility_packages__ = ["brltty", "espeakup", "alsa-utils"]
class InstallationFile: class InstallationFile:
def __init__(self, installation, filename, owner, mode="w"): def __init__(self, installation :'Installer', filename :str, owner :str, mode :str = "w"):
self.installation = installation self.installation = installation
self.filename = filename self.filename = filename
self.owner = owner self.owner = owner
self.mode = mode self.mode = mode
self.fh = None self.fh = None
def __enter__(self): def __enter__(self) -> 'InstallationFile':
self.fh = open(self.filename, self.mode) self.fh = open(self.filename, self.mode)
return self return self
def __exit__(self, *args): def __exit__(self, *args :str) -> None:
self.fh.close() self.fh.close()
self.installation.chown(self.owner, self.filename) self.installation.chown(self.owner, self.filename)
def write(self, data: Union[str, bytes]): def write(self, data: Union[str, bytes]) -> int:
return self.fh.write(data) return self.fh.write(data)
def read(self, *args): def read(self, *args) -> Union[str, bytes]:
return self.fh.read(*args) return self.fh.read(*args)
def poll(self, *args): # def poll(self, *args) -> bool:
return self.fh.poll(*args) # return self.fh.poll(*args)
def accessibility_tools_in_use() -> bool: def accessibility_tools_in_use() -> bool:
@ -84,11 +85,12 @@ class Installer:
""" """
def __init__(self, target, *, base_packages=None, kernels=None): def __init__(self, target :str, *, base_packages :Optional[List[str]] = None, kernels :Optional[List[str]] = None):
if base_packages is None: if base_packages is None:
base_packages = __packages__[:3] base_packages = __packages__[:3]
if kernels is None: if kernels is None:
kernels = ['linux'] kernels = ['linux']
self.kernels = kernels self.kernels = kernels
self.target = target self.target = target
self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S') self.init_time = time.strftime('%Y-%m-%d_%H-%M-%S')
@ -119,18 +121,17 @@ class Installer:
self.HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"] self.HOOKS = ["base", "udev", "autodetect", "keyboard", "keymap", "modconf", "block", "filesystems", "fsck"]
self.KERNEL_PARAMS = [] self.KERNEL_PARAMS = []
def log(self, *args, level=logging.DEBUG, **kwargs): def log(self, *args :str, level :int = logging.DEBUG, **kwargs :str):
""" """
installer.log() wraps output.log() mainly to set a default log-level for this install session. 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. Any manual override can be done per log() call.
""" """
log(*args, level=level, **kwargs) log(*args, level=level, **kwargs)
def __enter__(self, *args, **kwargs): def __enter__(self, *args :str, **kwargs :str) -> 'Installer':
return self return self
def __exit__(self, *args, **kwargs): def __exit__(self, *args :str, **kwargs :str) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]: if len(args) >= 2 and args[1]:
@ -163,10 +164,10 @@ class Installer:
return False return False
@property @property
def partitions(self): def partitions(self) -> List[Partition]:
return get_partitions_in_use(self.target) return get_partitions_in_use(self.target)
def sync_log_to_install_medium(self): def sync_log_to_install_medium(self) -> bool:
# Copy over the install log (if there is one) to the install medium if # Copy over the install log (if there is one) to the install medium if
# at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to. # at least the base has been strapped in, otherwise we won't have a filesystem/structure to copy to.
if self.helper_flags.get('base-strapped', False) is True: if self.helper_flags.get('base-strapped', False) is True:
@ -180,90 +181,111 @@ class Installer:
return True return True
def mount_ordered_layout(self, layouts: dict): def _create_keyfile(self,luks_handle , partition :dict, password :str):
from .luks import luks2 """ roiutine to create keyfiles, so it can be moved elsewere
"""
mountpoints = {} if partition.get('generate-encryption-key-file'):
for blockdevice in layouts: if not (cryptkey_dir := pathlib.Path(f"{self.target}/etc/cryptsetup-keys.d")).exists():
for partition in layouts[blockdevice]['partitions']: cryptkey_dir.mkdir(parents=True)
if (subvolumes := partition.get('btrfs', {}).get('subvolumes', {})): # Once we store the key as ../xyzloop.key systemd-cryptsetup can automatically load this key
if partition.get('encrypted',False): # if we name the device to "xyzloop".
if partition.get('mountpoint',None): if partition.get('mountpoint',None):
ppath = partition['mountpoint'] encryption_key_path = f"/etc/cryptsetup-keys.d/{pathlib.Path(partition['mountpoint']).name}loop.key"
else:
ppath = partition['device_instance'].path
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(ppath).name}loop"
# Immediately unlock the encrypted device to format the inner volume
with luks2(partition['device_instance'], loopdev, partition['!password'], auto_unmount=False) as unlocked_device:
unlocked_device.mount(f"{self.target}/")
try:
manage_btrfs_subvolumes(self,partition,mountpoints,subvolumes,unlocked_device)
except Exception as e:
# every exception unmounts the physical volume. Otherwise we let the system in an unstable state
unlocked_device.unmount()
raise e
unlocked_device.unmount()
# TODO generate key
else:
self.mount(partition['device_instance'],"/")
try:
manage_btrfs_subvolumes(self,partition,mountpoints,subvolumes)
except Exception as e:
# every exception unmounts the physical volume. Otherwise we let the system in an unstable state
partition['device_instance'].unmount()
raise e
partition['device_instance'].unmount()
else:
mountpoints[partition['mountpoint']] = partition
for mountpoint in sorted([mnt_dest for mnt_dest in mountpoints.keys() if mnt_dest is not None]):
partition = mountpoints[mountpoint]
if partition.get('encrypted', False) and not partition.get('subvolume',None):
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
if not (password := partition.get('!password', None)):
raise RequirementError(f"Missing mountpoint {mountpoint} encryption password in layout: {partition}")
with (luks_handle := luks2(partition['device_instance'], loopdev, password, auto_unmount=False)) as unlocked_device:
if partition.get('generate-encryption-key-file'):
if not (cryptkey_dir := pathlib.Path(f"{self.target}/etc/cryptsetup-keys.d")).exists():
cryptkey_dir.mkdir(parents=True)
# Once we store the key as ../xyzloop.key systemd-cryptsetup can automatically load this key
# if we name the device to "xyzloop".
encryption_key_path = f"/etc/cryptsetup-keys.d/{pathlib.Path(partition['mountpoint']).name}loop.key"
with open(f"{self.target}{encryption_key_path}", "w") as keyfile:
keyfile.write(generate_password(length=512))
os.chmod(f"{self.target}{encryption_key_path}", 0o400)
luks_handle.add_key(pathlib.Path(f"{self.target}{encryption_key_path}"), password=password)
luks_handle.crypttab(self, encryption_key_path, options=["luks", "key-slot=1"])
log(f"Mounting {mountpoint} to {self.target}{mountpoint} using {unlocked_device}", level=logging.INFO)
unlocked_device.mount(f"{self.target}{mountpoint}")
else: else:
log(f"Mounting {mountpoint} to {self.target}{mountpoint} using {partition['device_instance']}", level=logging.INFO) encryption_key_path = f"/etc/cryptsetup-keys.d/{pathlib.Path(partition['device_instance'].path).name}.key"
if partition.get('options',[]): with open(f"{self.target}{encryption_key_path}", "w") as keyfile:
mount_options = ','.join(partition['options']) keyfile.write(generate_password(length=512))
partition['device_instance'].mount(f"{self.target}{mountpoint}",options=mount_options)
else: os.chmod(f"{self.target}{encryption_key_path}", 0o400)
partition['device_instance'].mount(f"{self.target}{mountpoint}")
luks_handle.add_key(pathlib.Path(f"{self.target}{encryption_key_path}"), password=password)
luks_handle.crypttab(self, encryption_key_path, options=["luks", "key-slot=1"])
def _has_root(self, partition :dict) -> bool:
"""
Determine if an encrypted partition contains root in it
"""
if partition.get("mountpoint") is None:
if (sub_list := partition.get("btrfs",{}).get('subvolumes',{})):
for mountpoint in [sub_list[subvolume] if isinstance(sub_list[subvolume],str) else sub_list[subvolume].get("mountpoint") for subvolume in sub_list]:
if mountpoint == '/':
return True
return False
else:
return False
elif partition.get("mountpoint") == '/':
return True
else:
return False
def mount_ordered_layout(self, layouts: Dict[str, Any]) -> None:
from .luks import luks2
# set the partitions as a list not part of a tree (which we don't need anymore (i think)
list_part = []
list_luks_handles = []
for blockdevice in layouts:
list_part.extend(layouts[blockdevice]['partitions'])
# we manage the encrypted partititons
for partition in [entry for entry in list_part if entry.get('encrypted',False)]:
# open the luks device and all associate stuff
if not (password := partition.get('!password', None)):
raise RequirementError(f"Missing partition {partition['device_instance'].path} encryption password in layout: {partition}")
# i change a bit the naming conventions for the loop device
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['mountpoint']).name}loop"
else:
loopdev = f"{storage.get('ENC_IDENTIFIER', 'ai')}{pathlib.Path(partition['device_instance'].path).name}"
# note that we DON'T auto_unmount (i.e. close the encrypted device so it can be used
with (luks_handle := luks2(partition['device_instance'], loopdev, password, auto_unmount=False)) as unlocked_device:
if partition.get('generate-encryption-key-file',False) and not self._has_root(partition):
list_luks_handles.append([luks_handle,partition,password])
# this way all the requesrs will be to the dm_crypt device and not to the physical partition
partition['device_instance'] = unlocked_device
# we manage the btrfs partitions
for partition in [entry for entry in list_part if entry.get('btrfs', {}).get('subvolumes', {})]:
self.mount(partition['device_instance'],"/")
try:
new_mountpoints = manage_btrfs_subvolumes(self,partition)
except Exception as e:
# every exception unmounts the physical volume. Otherwise we let the system in an unstable state
partition['device_instance'].unmount()
raise e
partition['device_instance'].unmount()
if new_mountpoints:
list_part.extend(new_mountpoints)
# we mount. We need to sort by mountpoint to get a good working order
for partition in sorted([entry for entry in list_part if entry.get('mountpoint',False)],key=lambda part: part['mountpoint']):
mountpoint = partition['mountpoint']
log(f"Mounting {mountpoint} to {self.target}{mountpoint} using {partition['device_instance']}", level=logging.INFO)
if partition.get('filesystem',{}).get('mount_options',[]):
mount_options = ','.join(partition['filesystem']['mount_options'])
partition['device_instance'].mount(f"{self.target}{mountpoint}",options=mount_options)
else:
partition['device_instance'].mount(f"{self.target}{mountpoint}")
time.sleep(1) time.sleep(1)
try: try:
get_mount_info(f"{self.target}{mountpoint}", traverse=False) get_mount_info(f"{self.target}{mountpoint}", traverse=False)
except DiskError: except DiskError:
raise DiskError(f"Target {self.target}{mountpoint} never got mounted properly (unable to get mount information using findmnt).") raise DiskError(f"Target {self.target}{mountpoint} never got mounted properly (unable to get mount information using findmnt).")
def mount(self, partition, mountpoint, create_mountpoint=True): # once everything is mounted, we generate the key files in the correct place
for handle in list_luks_handles:
ppath = handle[1]['device_instance'].path
log(f"creating key-file for {ppath}",level=logging.INFO)
self._create_keyfile(handle[0],handle[1],handle[2])
def mount(self, partition :Partition, mountpoint :str, create_mountpoint :bool = True) -> None:
if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'): if create_mountpoint and not os.path.isdir(f'{self.target}{mountpoint}'):
os.makedirs(f'{self.target}{mountpoint}') os.makedirs(f'{self.target}{mountpoint}')
partition.mount(f'{self.target}{mountpoint}') partition.mount(f'{self.target}{mountpoint}')
def post_install_check(self, *args, **kwargs): def post_install_check(self, *args :str, **kwargs :str) -> List[bool]:
return [step for step, flag in self.helper_flags.items() if flag is False] return [step for step, flag in self.helper_flags.items() if flag is False]
def pacstrap(self, *packages, **kwargs): def pacstrap(self, *packages :str, **kwargs :str) -> bool:
if type(packages[0]) in (list, tuple): if type(packages[0]) in (list, tuple):
packages = packages[0] packages = packages[0]
@ -284,7 +306,7 @@ class Installer:
else: else:
self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=logging.INFO) self.log(f'Could not sync mirrors: {sync_mirrors.exit_code}', level=logging.INFO)
def set_mirrors(self, mirrors): def set_mirrors(self, mirrors :Mapping[str, Iterator[str]]) -> None:
for plugin in plugins.values(): for plugin in plugins.values():
if hasattr(plugin, 'on_mirrors'): if hasattr(plugin, 'on_mirrors'):
if result := plugin.on_mirrors(mirrors): if result := plugin.on_mirrors(mirrors):
@ -292,7 +314,7 @@ class Installer:
return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist') return use_mirrors(mirrors, destination=f'{self.target}/etc/pacman.d/mirrorlist')
def genfstab(self, flags='-pU'): def genfstab(self, flags :str = '-pU') -> bool:
self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO) self.log(f"Updating {self.target}/etc/fstab", level=logging.INFO)
with open(f"{self.target}/etc/fstab", 'a') as fstab_fh: with open(f"{self.target}/etc/fstab", 'a') as fstab_fh:
@ -307,11 +329,11 @@ class Installer:
return True return True
def set_hostname(self, hostname: str, *args, **kwargs): def set_hostname(self, hostname: str, *args :str, **kwargs :str) -> None:
with open(f'{self.target}/etc/hostname', 'w') as fh: with open(f'{self.target}/etc/hostname', 'w') as fh:
fh.write(hostname + '\n') fh.write(hostname + '\n')
def set_locale(self, locale, encoding='UTF-8', *args, **kwargs): def set_locale(self, locale :str, encoding :str = 'UTF-8', *args :str, **kwargs :str) -> bool:
if not len(locale): if not len(locale):
return True return True
@ -322,7 +344,7 @@ class Installer:
return True if SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen').exit_code == 0 else False return True if SysCommand(f'/usr/bin/arch-chroot {self.target} locale-gen').exit_code == 0 else False
def set_timezone(self, zone, *args, **kwargs): def set_timezone(self, zone :str, *args :str, **kwargs :str) -> bool:
if not zone: if not zone:
return True return True
if not len(zone): if not len(zone):
@ -337,6 +359,7 @@ class Installer:
(pathlib.Path(self.target) / "etc" / "localtime").unlink(missing_ok=True) (pathlib.Path(self.target) / "etc" / "localtime").unlink(missing_ok=True)
SysCommand(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime') SysCommand(f'/usr/bin/arch-chroot {self.target} ln -s /usr/share/zoneinfo/{zone} /etc/localtime')
return True return True
else: else:
self.log( self.log(
f"Time zone {zone} does not exist, continuing with system default.", f"Time zone {zone} does not exist, continuing with system default.",
@ -344,11 +367,13 @@ class Installer:
fg='red' fg='red'
) )
def activate_ntp(self): return False
def activate_ntp(self) -> None:
log(f"activate_ntp() is deprecated, use activate_time_syncronization()", fg="yellow", level=logging.INFO) log(f"activate_ntp() is deprecated, use activate_time_syncronization()", fg="yellow", level=logging.INFO)
self.activate_time_syncronization() self.activate_time_syncronization()
def activate_time_syncronization(self): def activate_time_syncronization(self) -> None:
self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO) self.log('Activating systemd-timesyncd for time synchronization using Arch Linux and ntp.org NTP servers.', level=logging.INFO)
self.enable_service('systemd-timesyncd') self.enable_service('systemd-timesyncd')
@ -361,11 +386,11 @@ class Installer:
with Boot(self) as session: with Boot(self) as session:
session.SysCommand(["timedatectl", "set-ntp", 'true']) session.SysCommand(["timedatectl", "set-ntp", 'true'])
def enable_espeakup(self): def enable_espeakup(self) -> None:
self.log('Enabling espeakup.service for speech synthesis (accessibility).', level=logging.INFO) self.log('Enabling espeakup.service for speech synthesis (accessibility).', level=logging.INFO)
self.enable_service('espeakup') self.enable_service('espeakup')
def enable_service(self, *services): def enable_service(self, *services :str) -> None:
for service in services: for service in services:
self.log(f'Enabling service {service}', level=logging.INFO) self.log(f'Enabling service {service}', level=logging.INFO)
if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0: if (output := self.arch_chroot(f'systemctl enable {service}')).exit_code != 0:
@ -375,19 +400,27 @@ class Installer:
if hasattr(plugin, 'on_service'): if hasattr(plugin, 'on_service'):
plugin.on_service(service) plugin.on_service(service)
def run_command(self, cmd, *args, **kwargs): def run_command(self, cmd :str, *args :str, **kwargs :str) -> None:
return SysCommand(f'/usr/bin/arch-chroot {self.target} {cmd}') return SysCommand(f'/usr/bin/arch-chroot {self.target} {cmd}')
def arch_chroot(self, cmd, run_as=None): def arch_chroot(self, cmd :str, run_as :Optional[str] = None):
if run_as: if run_as:
cmd = f"su - {run_as} -c {shlex.quote(cmd)}" cmd = f"su - {run_as} -c {shlex.quote(cmd)}"
return self.run_command(cmd) return self.run_command(cmd)
def drop_to_shell(self): def drop_to_shell(self) -> None:
subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True) subprocess.check_call(f"/usr/bin/arch-chroot {self.target}", shell=True)
def configure_nic(self, nic, dhcp=True, ip=None, gateway=None, dns=None, *args, **kwargs): def configure_nic(self,
nic :str,
dhcp :bool = True,
ip :Optional[str] = None,
gateway :Optional[str] = None,
dns :Optional[str] = None,
*args :str,
**kwargs :str
) -> None:
from .systemd import Networkd from .systemd import Networkd
if dhcp: if dhcp:
@ -412,7 +445,7 @@ class Installer:
with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf: with open(f"{self.target}/etc/systemd/network/10-{nic}.network", "a") as netconf:
netconf.write(str(conf)) netconf.write(str(conf))
def copy_iso_network_config(self, enable_services=False): def copy_iso_network_config(self, enable_services :bool = False) -> bool:
# Copy (if any) iwd password and config files # Copy (if any) iwd password and config files
if os.path.isdir('/var/lib/iwd/'): if os.path.isdir('/var/lib/iwd/'):
if psk_files := glob.glob('/var/lib/iwd/*.psk'): if psk_files := glob.glob('/var/lib/iwd/*.psk'):
@ -427,7 +460,7 @@ class Installer:
# This function will be called after minimal_installation() # This function will be called after minimal_installation()
# as a hook for post-installs. This hook is only needed if # as a hook for post-installs. This hook is only needed if
# base is not installed yet. # base is not installed yet.
def post_install_enable_iwd_service(*args, **kwargs): def post_install_enable_iwd_service(*args :str, **kwargs :str):
self.enable_service('iwd') self.enable_service('iwd')
self.post_base_install.append(post_install_enable_iwd_service) self.post_base_install.append(post_install_enable_iwd_service)
@ -452,7 +485,7 @@ class Installer:
# If we haven't installed the base yet (function called pre-maturely) # If we haven't installed the base yet (function called pre-maturely)
if self.helper_flags.get('base', False) is False: if self.helper_flags.get('base', False) is False:
def post_install_enable_networkd_resolved(*args, **kwargs): def post_install_enable_networkd_resolved(*args :str, **kwargs :str):
self.enable_service('systemd-networkd', 'systemd-resolved') self.enable_service('systemd-networkd', 'systemd-resolved')
self.post_base_install.append(post_install_enable_networkd_resolved) self.post_base_install.append(post_install_enable_networkd_resolved)
@ -462,7 +495,7 @@ class Installer:
return True return True
def detect_encryption(self, partition): def detect_encryption(self, partition :Partition) -> bool:
part = Partition(partition.parent, None, autodetect_filesystem=True) part = Partition(partition.parent, None, autodetect_filesystem=True)
if partition.encrypted: if partition.encrypted:
return partition return partition
@ -471,7 +504,7 @@ class Installer:
return False return False
def mkinitcpio(self, *flags): def mkinitcpio(self, *flags :str) -> bool:
for plugin in plugins.values(): for plugin in plugins.values():
if hasattr(plugin, 'on_mkinitcpio'): if hasattr(plugin, 'on_mkinitcpio'):
# Allow plugins to override the usage of mkinitcpio altogether. # Allow plugins to override the usage of mkinitcpio altogether.
@ -483,9 +516,10 @@ class Installer:
mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n") mkinit.write(f"BINARIES=({' '.join(self.BINARIES)})\n")
mkinit.write(f"FILES=({' '.join(self.FILES)})\n") mkinit.write(f"FILES=({' '.join(self.FILES)})\n")
mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n") mkinit.write(f"HOOKS=({' '.join(self.HOOKS)})\n")
SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}')
def minimal_installation(self): return SysCommand(f'/usr/bin/arch-chroot {self.target} mkinitcpio {" ".join(flags)}').exit_code == 0
def minimal_installation(self) -> bool:
# Add necessary packages if encrypting the drive # Add necessary packages if encrypting the drive
# (encrypted partitions default to btrfs for now, so we need btrfs-progs) # (encrypted partitions default to btrfs for now, so we need btrfs-progs)
# TODO: Perhaps this should be living in the function which dictates # TODO: Perhaps this should be living in the function which dictates
@ -562,7 +596,7 @@ class Installer:
return True return True
def setup_swap(self, kind='zram'): def setup_swap(self, kind :str = 'zram') -> bool:
if kind == 'zram': if kind == 'zram':
self.log(f"Setting up swap on zram") self.log(f"Setting up swap on zram")
self.pacstrap('zram-generator') self.pacstrap('zram-generator')
@ -578,7 +612,18 @@ class Installer:
else: else:
raise ValueError(f"Archinstall currently only supports setting up swap on zram") raise ValueError(f"Archinstall currently only supports setting up swap on zram")
def add_bootloader(self, bootloader='systemd-bootctl'): def add_bootloader(self, bootloader :str = 'systemd-bootctl') -> bool:
"""
Adds a bootloader to the installation instance.
Archinstall supports one of three types:
* systemd-bootctl
* grub
* efistub (beta)
:param bootloader: Can be one of the three strings
'systemd-bootctl', 'grub' or 'efistub' (beta)
"""
for plugin in plugins.values(): for plugin in plugins.values():
if hasattr(plugin, 'on_add_bootloader'): if hasattr(plugin, 'on_add_bootloader'):
# Allow plugins to override the boot-loader handling. # Allow plugins to override the boot-loader handling.
@ -669,6 +714,7 @@ class Installer:
base_path,bind_path = split_bind_name(str(root_partition.path)) base_path,bind_path = split_bind_name(str(root_partition.path))
if bind_path is not None: # and root_fs_type == 'btrfs': if bind_path is not None: # and root_fs_type == 'btrfs':
options_entry = f"rootflags=subvol={bind_path} " + options_entry options_entry = f"rootflags=subvol={bind_path} " + options_entry
if real_device := self.detect_encryption(root_partition): if real_device := self.detect_encryption(root_partition):
# TODO: We need to detect if the encrypted device is a whole disk encryption, # 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) # or simply a partition encryption. Right now we assume it's a partition (and we always have)
@ -757,10 +803,19 @@ class Installer:
return True return True
def add_additional_packages(self, *packages): def add_additional_packages(self, *packages :str) -> bool:
return self.pacstrap(*packages) return self.pacstrap(*packages)
def install_profile(self, profile): def install_profile(self, profile :str) -> ModuleType:
"""
Installs a archinstall profile script (.py file).
This profile can be either local, remote or part of the library.
:param profile: Can be a local path or a remote path (URL)
:return: Returns the imported script as a module, this way
you can access any remaining functions exposed by the profile.
:rtype: module
"""
storage['installation_session'] = self storage['installation_session'] = self
if type(profile) == str: if type(profile) == str:
@ -769,13 +824,13 @@ class Installer:
self.log(f'Installing network profile {profile}', level=logging.INFO) self.log(f'Installing network profile {profile}', level=logging.INFO)
return profile.install() return profile.install()
def enable_sudo(self, entity: str, group=False): def enable_sudo(self, entity: str, group :bool = False) -> bool:
self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO) self.log(f'Enabling sudo permissions for {entity}.', level=logging.INFO)
with open(f'{self.target}/etc/sudoers', 'a') as sudoers: with open(f'{self.target}/etc/sudoers', 'a') as sudoers:
sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n') sudoers.write(f'{"%" if group else ""}{entity} ALL=(ALL) ALL\n')
return True return True
def user_create(self, user: str, password=None, groups=None, sudo=False): def user_create(self, user :str, password :Optional[str] = None, groups :Optional[str] = None, sudo :bool = False) -> None:
if groups is None: if groups is None:
groups = [] groups = []
@ -789,7 +844,8 @@ class Installer:
if not handled_by_plugin: if not handled_by_plugin:
self.log(f'Creating user {user}', level=logging.INFO) self.log(f'Creating user {user}', level=logging.INFO)
SysCommand(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}') if not (output := SysCommand(f'/usr/bin/arch-chroot {self.target} useradd -m -G wheel {user}')).exit_code == 0:
raise SystemError(f"Could not create user inside installation: {output}")
for plugin in plugins.values(): for plugin in plugins.values():
if hasattr(plugin, 'on_user_created'): if hasattr(plugin, 'on_user_created'):
@ -806,24 +862,24 @@ class Installer:
if sudo and self.enable_sudo(user): if sudo and self.enable_sudo(user):
self.helper_flags['user'] = True self.helper_flags['user'] = True
def user_set_pw(self, user, password): def user_set_pw(self, user :str, password :str) -> bool:
self.log(f'Setting password for {user}', level=logging.INFO) self.log(f'Setting password for {user}', level=logging.INFO)
if user == 'root': if user == 'root':
# This means the root account isn't locked/disabled with * in /etc/passwd # This means the root account isn't locked/disabled with * in /etc/passwd
self.helper_flags['user'] = True self.helper_flags['user'] = True
SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\"") return SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"echo '{user}:{password}' | chpasswd\"").exit_code == 0
def user_set_shell(self, user, shell): def user_set_shell(self, user :str, shell :str) -> bool:
self.log(f'Setting shell for {user} to {shell}', level=logging.INFO) self.log(f'Setting shell for {user} to {shell}', level=logging.INFO)
SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"") return SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c \"chsh -s {shell} {user}\"").exit_code == 0
def chown(self, owner, path, options=[]): def chown(self, owner :str, path :str, options :List[str] = []) -> bool:
return SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c 'chown {' '.join(options)} {owner} {path}") return SysCommand(f"/usr/bin/arch-chroot {self.target} sh -c 'chown {' '.join(options)} {owner} {path}").exit_code == 0
def create_file(self, filename, owner=None): def create_file(self, filename :str, owner :Optional[str] = None) -> InstallationFile:
return InstallationFile(self, filename, owner) return InstallationFile(self, filename, owner)
def set_keyboard_language(self, language: str) -> bool: def set_keyboard_language(self, language: str) -> bool:

View File

@ -1,41 +1,60 @@
import logging import logging
from typing import Iterator, List
from .exceptions import ServiceException from .exceptions import ServiceException
from .general import SysCommand from .general import SysCommand
from .output import log from .output import log
def list_keyboard_languages(): def list_keyboard_languages() -> Iterator[str]:
for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}): for line in SysCommand("localectl --no-pager list-keymaps", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip() yield line.decode('UTF-8').strip()
def list_x11_keyboard_languages(): def list_locales() -> List[str]:
with open('/etc/locale.gen', 'r') as fp:
locales = []
# before the list of locales begins there's an empty line with a '#' in front
# so we'll collect the localels from bottom up and halt when we're donw
entries = fp.readlines()
entries.reverse()
for entry in entries:
text = entry[1:].strip()
if text == '':
break
locales.append(text)
locales.reverse()
return locales
def list_x11_keyboard_languages() -> Iterator[str]:
for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}): for line in SysCommand("localectl --no-pager list-x11-keymap-layouts", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip() yield line.decode('UTF-8').strip()
def verify_keyboard_layout(layout): def verify_keyboard_layout(layout :str) -> bool:
for language in list_keyboard_languages(): for language in list_keyboard_languages():
if layout.lower() == language.lower(): if layout.lower() == language.lower():
return True return True
return False return False
def verify_x11_keyboard_layout(layout): def verify_x11_keyboard_layout(layout :str) -> bool:
for language in list_x11_keyboard_languages(): for language in list_x11_keyboard_languages():
if layout.lower() == language.lower(): if layout.lower() == language.lower():
return True return True
return False return False
def search_keyboard_layout(layout): def search_keyboard_layout(layout :str) -> Iterator[str]:
for language in list_keyboard_languages(): for language in list_keyboard_languages():
if layout.lower() in language.lower(): if layout.lower() in language.lower():
yield language yield language
def set_keyboard_language(locale): def set_keyboard_language(locale :str) -> bool:
if len(locale.strip()): if len(locale.strip()):
if not verify_keyboard_layout(locale): if not verify_keyboard_layout(locale):
log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR) log(f"Invalid keyboard locale specified: {locale}", fg="red", level=logging.ERROR)
@ -49,6 +68,6 @@ def set_keyboard_language(locale):
return False return False
def list_timezones(): def list_timezones() -> Iterator[str]:
for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}): for line in SysCommand("timedatectl --no-pager list-timezones", environment_vars={'SYSTEMD_COLORS': '0'}):
yield line.decode('UTF-8').strip() yield line.decode('UTF-8').strip()

View File

@ -1,9 +1,15 @@
from __future__ import annotations
import json import json
import logging import logging
import os import os
import pathlib import pathlib
import shlex import shlex
import time import time
from typing import Optional, List,TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .installer import Installer
from .disk import Partition, convert_device_to_uuid from .disk import Partition, convert_device_to_uuid
from .general import SysCommand, SysCommandWorker from .general import SysCommand, SysCommandWorker
from .output import log from .output import log
@ -11,7 +17,15 @@ from .exceptions import SysCallError, DiskError
from .storage import storage from .storage import storage
class luks2: class luks2:
def __init__(self, partition, mountpoint, password, key_file=None, auto_unmount=False, *args, **kwargs): def __init__(self,
partition :Partition,
mountpoint :str,
password :str,
key_file :Optional[str] = None,
auto_unmount :bool = False,
*args :str,
**kwargs :str):
self.password = password self.password = password
self.partition = partition self.partition = partition
self.mountpoint = mountpoint self.mountpoint = mountpoint
@ -22,7 +36,7 @@ class luks2:
self.filesystem = 'crypto_LUKS' self.filesystem = 'crypto_LUKS'
self.mapdev = None self.mapdev = None
def __enter__(self): def __enter__(self) -> Partition:
if not self.key_file: if not self.key_file:
self.key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique? self.key_file = f"/tmp/{os.path.basename(self.partition.path)}.disk_pw" # TODO: Make disk-pw-file randomly unique?
@ -34,16 +48,23 @@ class luks2:
return self.unlock(self.partition, self.mountpoint, self.key_file) return self.unlock(self.partition, self.mountpoint, self.key_file)
def __exit__(self, *args, **kwargs): def __exit__(self, *args :str, **kwargs :str) -> bool:
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if self.auto_unmount: if self.auto_unmount:
self.close() self.close()
if len(args) >= 2 and args[1]: if len(args) >= 2 and args[1]:
raise args[1] raise args[1]
return True return True
def encrypt(self, partition, password=None, key_size=512, hash_type='sha512', iter_time=10000, key_file=None): def encrypt(self, partition :Partition,
password :Optional[str] = None,
key_size :int = 512,
hash_type :str = 'sha512',
iter_time :int = 10000,
key_file :Optional[str] = None) -> str:
log(f'Encrypting {partition} (This might take a while)', level=logging.INFO) log(f'Encrypting {partition} (This might take a while)', level=logging.INFO)
if not key_file: if not key_file:
@ -119,7 +140,7 @@ class luks2:
return key_file return key_file
def unlock(self, partition, mountpoint, key_file): def unlock(self, partition :Partition, mountpoint :str, key_file :str) -> Partition:
""" """
Mounts a luks2 compatible partition to a certain mountpoint. Mounts a luks2 compatible partition to a certain mountpoint.
Keyfile must be specified as there's no way to interact with the pw-prompt atm. Keyfile must be specified as there's no way to interact with the pw-prompt atm.
@ -142,24 +163,24 @@ class luks2:
unlocked_partition = Partition(self.mapdev, None, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False) unlocked_partition = Partition(self.mapdev, None, encrypted=True, filesystem=get_filesystem_type(self.mapdev), autodetect_filesystem=False)
return unlocked_partition return unlocked_partition
def close(self, mountpoint=None): def close(self, mountpoint :Optional[str] = None) -> bool:
if not mountpoint: if not mountpoint:
mountpoint = self.mapdev mountpoint = self.mapdev
SysCommand(f'/usr/bin/cryptsetup close {self.mapdev}') SysCommand(f'/usr/bin/cryptsetup close {self.mapdev}')
return os.path.islink(self.mapdev) is False return os.path.islink(self.mapdev) is False
def format(self, path): def format(self, path :str) -> None:
if (handle := SysCommand(f"/usr/bin/cryptsetup -q -v luksErase {path}")).exit_code != 0: if (handle := SysCommand(f"/usr/bin/cryptsetup -q -v luksErase {path}")).exit_code != 0:
raise DiskError(f'Could not format {path} with {self.filesystem} because: {b"".join(handle)}') raise DiskError(f'Could not format {path} with {self.filesystem} because: {b"".join(handle)}')
def add_key(self, path :pathlib.Path, password :str): def add_key(self, path :pathlib.Path, password :str) -> bool:
if not path.exists(): if not path.exists():
raise OSError(2, f"Could not import {path} as a disk encryption key, file is missing.", str(path)) raise OSError(2, f"Could not import {path} as a disk encryption key, file is missing.", str(path))
log(f'Adding additional key-file {path} for {self.partition}', level=logging.INFO) log(f'Adding additional key-file {path} for {self.partition}', level=logging.INFO)
worker = SysCommandWorker(f"/usr/bin/cryptsetup -q -v luksAddKey {self.partition.path} {path}",
worker = SysCommandWorker(f"/usr/bin/cryptsetup -q -v luksAddKey {self.partition.path} {path}") environment_vars={'LC_ALL':'C'})
pw_injected = False pw_injected = False
while worker.is_alive(): while worker.is_alive():
if b'Enter any existing passphrase' in worker and pw_injected is False: if b'Enter any existing passphrase' in worker and pw_injected is False:
@ -169,7 +190,9 @@ class luks2:
if worker.exit_code != 0: if worker.exit_code != 0:
raise DiskError(f'Could not add encryption key {path} to {self.partition} because: {worker}') raise DiskError(f'Could not add encryption key {path} to {self.partition} because: {worker}')
def crypttab(self, installation, key_path :str, options=["luks", "key-slot=1"]): return True
def crypttab(self, installation :Installer, key_path :str, options :List[str] = ["luks", "key-slot=1"]) -> None:
log(f'Adding a crypttab entry for key {key_path} in {installation}', level=logging.INFO) log(f'Adding a crypttab entry for key {key_path} in {installation}', level=logging.INFO)
with open(f"{installation.target}/etc/crypttab", "a") as crypttab: with open(f"{installation.target}/etc/crypttab", "a") as crypttab:
crypttab.write(f"{self.mountpoint} UUID={convert_device_to_uuid(self.partition.path)} {key_path} {','.join(options)}\n") crypttab.write(f"{self.mountpoint} UUID={convert_device_to_uuid(self.partition.path)} {key_path} {','.join(options)}\n")

View File

@ -0,0 +1 @@
from .menu import Menu

View File

@ -1,15 +1,20 @@
from .simple_menu import TerminalMenu from archinstall.lib.menu.simple_menu import TerminalMenu
from ..exceptions import RequirementError
from ..output import log
from collections.abc import Iterable
import sys
import logging
class Menu(TerminalMenu): class Menu(TerminalMenu):
def __init__(self, title, options, skip=True, multi=False, default_option=None, sort=True): def __init__(self, title, p_options, skip=True, multi=False, default_option=None, sort=True):
""" """
Creates a new menu Creates a new menu
:param title: Text that will be displayed above the menu :param title: Text that will be displayed above the menu
:type title: str :type title: str
:param options: Options to be displayed in the menu to chose from; :param p_options: Options to be displayed in the menu to chose from;
if dict is specified then the keys of such will be used as options if dict is specified then the keys of such will be used as options
:type options: list, dict :type options: list, dict
@ -25,9 +30,29 @@ class Menu(TerminalMenu):
:param sort: Indicate if the options should be sorted alphabetically before displaying :param sort: Indicate if the options should be sorted alphabetically before displaying
:type sort: bool :type sort: bool
""" """
# we guarantee the inmutability of the options outside the class.
# an unknown number of iterables (.keys(),.values(),generator,...) can't be directly copied, in this case
# we recourse to make them lists before, but thru an exceptions
# this is the old code, which is not maintenable with more types
# options = copy(list(p_options) if isinstance(p_options,(type({}.keys()),type({}.values()))) else p_options)
# We check that the options are iterable. If not we abort. Else we copy them to lists
# it options is a dictionary we use the values as entries of the list
# if options is a string object, each character becomes an entry
# if options is a list, we implictily build a copy to mantain immutability
if not isinstance(p_options,Iterable):
log(f"Objects of type {type(p_options)} is not iterable, and are not supported at Menu",fg="red")
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("Menu() requires an iterable as option.")
if isinstance(options, dict): if isinstance(p_options,dict):
options = list(options) options = list(p_options.keys())
else:
options = list(p_options)
if not options:
log(" * Menu didn't find any options to choose from * ", fg='red')
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError('Menu.__init__() requires at least one option to proceed.')
if sort: if sort:
options = sorted(options) options = sorted(options)

View File

@ -0,0 +1,392 @@
import sys
import archinstall
from archinstall import Menu
class Selector:
def __init__(
self,
description,
func=None,
display_func=None,
default=None,
enabled=False,
dependencies=[],
dependencies_not=[]
):
"""
Create a new menu selection entry
:param description: Text that will be displayed as the menu entry
:type description: str
:param func: Function that is called when the menu entry is selected
:type func: Callable
:param display_func: After specifying a setting for a menu item it is displayed
on the right side of the item as is; with this function one can modify the entry
to be displayed; e.g. when specifying a password one can display **** instead
:type display_func: Callable
:param default: Default value for this menu entry
:type default: Any
:param enabled: Specify if this menu entry should be displayed
:type enabled: bool
:param dependencies: Specify dependencies for this menu entry; if the dependencies
are not set yet, then this item is not displayed; e.g. disk_layout depends on selectiong
harddrive(s) first
:type dependencies: list
:param dependencies_not: These are the exclusive options; the menu item will only be
displayed if non of the entries in the list have been specified
:type dependencies_not: list
"""
self._description = description
self.func = func
self._display_func = display_func
self._current_selection = default
self.enabled = enabled
self.text = self.menu_text()
self._dependencies = dependencies
self._dependencies_not = dependencies_not
@property
def dependencies(self):
return self._dependencies
@property
def dependencies_not(self):
return self._dependencies_not
def set_enabled(self):
self.enabled = True
def update_description(self, description):
self._description = description
self.text = self.menu_text()
def menu_text(self):
current = ''
if self._display_func:
current = self._display_func(self._current_selection)
else:
if self._current_selection is not None:
current = str(self._current_selection)
if current:
padding = 35 - len(self._description)
current = ' ' * padding + f'SET: {current}'
return f'{self._description} {current}'
def set_current_selection(self, current):
self._current_selection = current
self.text = self.menu_text()
def has_selection(self):
if self._current_selection is None:
return False
return True
def is_empty(self):
if self._current_selection is None:
return True
elif isinstance(self._current_selection, (str, list, dict)) and len(self._current_selection) == 0:
return True
return False
class GlobalMenu:
def __init__(self):
self._menu_options = {}
self._setup_selection_menu_options()
def _setup_selection_menu_options(self):
self._menu_options['keyboard-layout'] = \
Selector('Select keyboard layout', lambda: archinstall.select_language('us'), default='us')
self._menu_options['mirror-region'] = \
Selector(
'Select mirror region',
lambda: archinstall.select_mirror_regions(),
display_func=lambda x: list(x.keys()) if x else '[]',
default={})
self._menu_options['sys-language'] = \
Selector('Select locale language', lambda: archinstall.select_locale_lang('en_US'), default='en_US')
self._menu_options['sys-encoding'] = \
Selector('Select locale encoding', lambda: archinstall.select_locale_enc('utf-8'), default='utf-8')
self._menu_options['harddrives'] = \
Selector(
'Select harddrives',
lambda: self._select_harddrives())
self._menu_options['disk_layouts'] = \
Selector(
'Select disk layout',
lambda: archinstall.select_disk_layout(
archinstall.arguments['harddrives'],
archinstall.arguments.get('advanced', False)
),
dependencies=['harddrives'])
self._menu_options['!encryption-password'] = \
Selector(
'Set encryption password',
lambda: archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): '),
display_func=lambda x: self._secret(x) if x else 'None',
dependencies=['harddrives'])
self._menu_options['swap'] = \
Selector(
'Use swap',
lambda: archinstall.ask_for_swap(),
default=True)
self._menu_options['bootloader'] = \
Selector(
'Select bootloader',
lambda: archinstall.ask_for_bootloader(archinstall.arguments.get('advanced', False)),)
self._menu_options['hostname'] = \
Selector('Specify hostname', lambda: archinstall.ask_hostname())
self._menu_options['!root-password'] = \
Selector(
'Set root password',
lambda: self._set_root_password(),
display_func=lambda x: self._secret(x) if x else 'None')
self._menu_options['!superusers'] = \
Selector(
'Specify superuser account',
lambda: self._create_superuser_account(),
dependencies_not=['!root-password'],
display_func=lambda x: list(x.keys()) if x else '')
self._menu_options['!users'] = \
Selector(
'Specify user account',
lambda: self._create_user_account(),
default={},
display_func=lambda x: list(x.keys()) if x else '[]')
self._menu_options['profile'] = \
Selector(
'Specify profile',
lambda: self._select_profile(),
display_func=lambda x: x if x else 'None')
self._menu_options['audio'] = \
Selector(
'Select audio',
lambda: archinstall.ask_for_audio_selection(archinstall.is_desktop_profile(archinstall.arguments.get('profile', None))))
self._menu_options['kernels'] = \
Selector(
'Select kernels',
lambda: archinstall.select_kernel(),
default='linux')
self._menu_options['packages'] = \
Selector(
'Additional packages to install',
lambda: archinstall.ask_additional_packages_to_install(archinstall.arguments.get('packages', None)),
default=[])
self._menu_options['nic'] = \
Selector(
'Configure network',
lambda: archinstall.ask_to_configure_network(),
display_func=lambda x: x if x else 'Not configured, unavailable unless setup manually',
default={})
self._menu_options['timezone'] = \
Selector('Select timezone', lambda: archinstall.ask_for_a_timezone())
self._menu_options['ntp'] = \
Selector(
'Set automatic time sync (NTP)',
lambda: archinstall.ask_ntp(),
default=True)
self._menu_options['install'] = \
Selector(
self._install_text(),
enabled=True)
self._menu_options['abort'] = Selector('Abort', enabled=True)
def enable(self, selector_name, omit_if_set=False):
arg = archinstall.arguments.get(selector_name, None)
# don't display the menu option if it was defined already
if arg is not None and omit_if_set:
return
if self._menu_options.get(selector_name, None):
self._menu_options[selector_name].set_enabled()
if arg is not None:
self._menu_options[selector_name].set_current_selection(arg)
else:
print(f'No selector found: {selector_name}')
sys.exit(1)
def run(self):
while True:
# # Before continuing, set the preferred keyboard layout/language in the current terminal.
# # This will just help the user with the next following questions.
self._set_kb_language()
enabled_menus = self._menus_to_enable()
menu_text = [m.text for m in enabled_menus.values()]
selection = Menu('Set/Modify the below options', menu_text, sort=False).run()
if selection:
selection = selection.strip()
if 'Abort' in selection:
exit(0)
elif 'Install' in selection:
if self._missing_configs() == 0:
self._post_processing()
break
else:
self._process_selection(selection)
def _process_selection(self, selection):
# find the selected option in our option list
option = [[k, v] for k, v in self._menu_options.items() if v.text.strip() == selection]
if len(option) != 1:
raise ValueError(f'Selection not found: {selection}')
selector_name = option[0][0]
selector = option[0][1]
result = selector.func()
self._menu_options[selector_name].set_current_selection(result)
archinstall.arguments[selector_name] = result
self._update_install()
def _update_install(self):
text = self._install_text()
self._menu_options.get('install').update_description(text)
def _post_processing(self):
if archinstall.arguments.get('harddrives', None) and archinstall.arguments.get('!encryption-password', None):
# If no partitions was marked as encrypted, but a password was supplied and we have some disks to format..
# Then we need to identify which partitions to encrypt. This will default to / (root).
if len(list(archinstall.encrypted_partitions(archinstall.storage['disk_layouts']))) == 0:
archinstall.storage['disk_layouts'] = archinstall.select_encrypted_partitions(
archinstall.storage['disk_layouts'], archinstall.arguments['!encryption-password'])
def _install_text(self):
missing = self._missing_configs()
if missing > 0:
return f'Install ({missing} config(s) missing)'
return 'Install'
def _missing_configs(self):
def check(s):
return self._menu_options.get(s).has_selection()
missing = 0
if not check('bootloader'):
missing += 1
if not check('hostname'):
missing += 1
if not check('audio'):
missing += 1
if not check('timezone'):
missing += 1
if not check('!root-password') and not check('!superusers'):
missing += 1
if not check('harddrives'):
missing += 1
if check('harddrives'):
if not self._menu_options.get('harddrives').is_empty() and not check('disk_layouts'):
missing += 1
return missing
def _set_root_password(self):
prompt = 'Enter root password (leave blank to disable root & create superuser): '
password = archinstall.get_password(prompt=prompt)
if password is not None:
self._menu_options.get('!superusers').set_current_selection(None)
archinstall.arguments['!users'] = {}
archinstall.arguments['!superusers'] = {}
return password
def _select_harddrives(self):
old_haddrives = archinstall.arguments.get('harddrives')
harddrives = archinstall.select_harddrives()
# in case the harddrives got changed we have to reset the disk layout as well
if old_haddrives != harddrives:
self._menu_options.get('disk_layouts').set_current_selection(None)
archinstall.arguments['disk_layouts'] = {}
if not harddrives:
prompt = 'You decided to skip harddrive selection\n'
prompt += f"and will use whatever drive-setup is mounted at {archinstall.storage['MOUNT_POINT']} (experimental)\n"
prompt += "WARNING: Archinstall won't check the suitability of this setup\n"
prompt += 'Do you wish to continue?'
choice = Menu(prompt, ['yes', 'no'], default_option='yes').run()
if choice == 'no':
return self._select_harddrives()
return harddrives
def _secret(self, x):
return '*' * len(x)
def _select_profile(self):
profile = archinstall.select_profile()
# Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if profile and profile.has_prep_function():
namespace = f'{profile.namespace}.py'
with profile.load_instructions(namespace=namespace) as imported:
if not imported._prep_function():
archinstall.log(' * Profile\'s preparation requirements was not fulfilled.', fg='red')
exit(1)
return profile
def _create_superuser_account(self):
superuser = archinstall.ask_for_superuser_account('Create a required super-user with sudo privileges: ', forced=True)
return superuser
def _create_user_account(self):
users, superusers = archinstall.ask_for_additional_users('Enter a username to create an additional user: ')
if not archinstall.arguments.get('!superusers', None):
archinstall.arguments['!superusers'] = superusers
else:
archinstall.arguments['!superusers'] = {**archinstall.arguments['!superusers'], **superusers}
return users
def _set_kb_language(self):
# Before continuing, set the preferred keyboard layout/language in the current terminal.
# This will just help the user with the next following questions.
if archinstall.arguments.get('keyboard-layout', None) and len(archinstall.arguments['keyboard-layout']):
archinstall.set_keyboard_language(archinstall.arguments['keyboard-layout'])
def _verify_selection_enabled(self, selection_name):
if selection := self._menu_options.get(selection_name, None):
if not selection.enabled:
return False
if len(selection.dependencies) > 0:
for d in selection.dependencies:
if not self._verify_selection_enabled(d) or self._menu_options.get(d).is_empty():
return False
if len(selection.dependencies_not) > 0:
for d in selection.dependencies_not:
if not self._menu_options.get(d).is_empty():
return False
return True
raise ValueError(f'No selection found: {selection_name}')
def _menus_to_enable(self):
enabled_menus = {}
for name, selection in self._menu_options.items():
if self._verify_selection_enabled(name):
enabled_menus[name] = selection
return enabled_menus

View File

@ -61,7 +61,6 @@ try:
except ImportError as e: except ImportError as e:
raise NotImplementedError('"{}" is currently not supported.'.format(platform.system())) from e raise NotImplementedError('"{}" is currently not supported.'.format(platform.system())) from e
__author__ = "Ingo Meyer" __author__ = "Ingo Meyer"
__email__ = "i.meyer@fz-juelich.de" __email__ = "i.meyer@fz-juelich.de"
__copyright__ = "Copyright © 2021 Forschungszentrum Jülich GmbH. All rights reserved." __copyright__ = "Copyright © 2021 Forschungszentrum Jülich GmbH. All rights reserved."

View File

@ -1,7 +1,7 @@
import logging import logging
import urllib.error import urllib.error
import urllib.request import urllib.request
from typing import Union, Mapping, Iterable from typing import Union, Mapping, Iterable, Dict, Any, List
from .general import SysCommand from .general import SysCommand
from .output import log from .output import log
@ -51,7 +51,12 @@ def sort_mirrorlist(raw_data :bytes, sort_order=["https", "http"]) -> bytes:
return new_raw_data return new_raw_data
def filter_mirrors_by_region(regions, destination='/etc/pacman.d/mirrorlist', sort_order=["https", "http"], *args, **kwargs) -> Union[bool, bytes]: def filter_mirrors_by_region(regions :str,
destination :str = '/etc/pacman.d/mirrorlist',
sort_order :List[str] = ["https", "http"],
*args :str,
**kwargs :str
) -> Union[bool, bytes]:
""" """
This function will change the active mirrors on the live medium by This function will change the active mirrors on the live medium by
filtering which regions are active based on `regions`. filtering which regions are active based on `regions`.
@ -75,7 +80,7 @@ def filter_mirrors_by_region(regions, destination='/etc/pacman.d/mirrorlist', so
return new_list.decode('UTF-8') return new_list.decode('UTF-8')
def add_custom_mirrors(mirrors: list, *args, **kwargs): def add_custom_mirrors(mirrors: List[str], *args :str, **kwargs :str) -> bool:
""" """
This will append custom mirror definitions in pacman.conf This will append custom mirror definitions in pacman.conf
@ -91,7 +96,7 @@ def add_custom_mirrors(mirrors: list, *args, **kwargs):
return True return True
def insert_mirrors(mirrors, *args, **kwargs): def insert_mirrors(mirrors :Dict[str, Any], *args :str, **kwargs :str) -> bool:
""" """
This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`. This function will insert a given mirror-list at the top of `/etc/pacman.d/mirrorlist`.
It will not flush any other mirrors, just insert new ones. It will not flush any other mirrors, just insert new ones.
@ -138,7 +143,7 @@ def re_rank_mirrors(
return True return True
def list_mirrors(sort_order=["https", "http"]): def list_mirrors(sort_order :List[str] = ["https", "http"]) -> Dict[str, Any]:
url = "https://archlinux.org/mirrorlist/?protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on" url = "https://archlinux.org/mirrorlist/?protocol=https&protocol=http&ip_version=4&ip_version=6&use_mirror_status=on"
regions = {} regions = {}

View File

@ -2,7 +2,7 @@ import logging
import os import os
import socket import socket
import struct import struct
from collections import OrderedDict from typing import Union, Dict, Any, List
from .exceptions import HardwareIncompatibilityError from .exceptions import HardwareIncompatibilityError
from .general import SysCommand from .general import SysCommand
@ -10,36 +10,40 @@ from .output import log
from .storage import storage from .storage import storage
def get_hw_addr(ifname): def get_hw_addr(ifname :str) -> str:
import fcntl import fcntl
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15])) info = fcntl.ioctl(s.fileno(), 0x8927, struct.pack('256s', bytes(ifname, 'utf-8')[:15]))
return ':'.join('%02x' % b for b in info[18:24]) return ':'.join('%02x' % b for b in info[18:24])
def list_interfaces(skip_loopback=True): def list_interfaces(skip_loopback :bool = True) -> Dict[str, str]:
interfaces = OrderedDict() interfaces = {}
for index, iface in socket.if_nameindex(): for index, iface in socket.if_nameindex():
if skip_loopback and iface == "lo": if skip_loopback and iface == "lo":
continue continue
mac = get_hw_addr(iface).replace(':', '-').lower() mac = get_hw_addr(iface).replace(':', '-').lower()
interfaces[mac] = iface interfaces[mac] = iface
return interfaces return interfaces
def check_mirror_reachable(): def check_mirror_reachable() -> bool:
log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO) log("Testing connectivity to the Arch Linux mirrors ...", level=logging.INFO)
if SysCommand("pacman -Sy").exit_code == 0: if SysCommand("pacman -Sy").exit_code == 0:
return True return True
elif os.geteuid() != 0: elif os.geteuid() != 0:
log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red") log("check_mirror_reachable() uses 'pacman -Sy' which requires root.", level=logging.ERROR, fg="red")
return False return False
def enrich_iface_types(interfaces: dict): def enrich_iface_types(interfaces: Union[Dict[str, Any], List[str]]) -> Dict[str, str]:
result = {} result = {}
for iface in interfaces: for iface in interfaces:
if os.path.isdir(f"/sys/class/net/{iface}/bridge/"): if os.path.isdir(f"/sys/class/net/{iface}/bridge/"):
result[iface] = 'BRIDGE' result[iface] = 'BRIDGE'
@ -53,19 +57,21 @@ def enrich_iface_types(interfaces: dict):
result[iface] = 'PHYSICAL' result[iface] = 'PHYSICAL'
else: else:
result[iface] = 'UNKNOWN' result[iface] = 'UNKNOWN'
return result return result
def get_interface_from_mac(mac): def get_interface_from_mac(mac :str) -> str:
return list_interfaces().get(mac.lower(), None) return list_interfaces().get(mac.lower(), None)
def wireless_scan(interface): def wireless_scan(interface :str) -> None:
interfaces = enrich_iface_types(list_interfaces().values()) interfaces = enrich_iface_types(list_interfaces().values())
if interfaces[interface] != 'WIRELESS': if interfaces[interface] != 'WIRELESS':
raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}") raise HardwareIncompatibilityError(f"Interface {interface} is not a wireless interface: {interfaces}")
SysCommand(f"iwctl station {interface} scan") if not (output := SysCommand(f"iwctl station {interface} scan")).exit_code == 0:
raise SystemError(f"Could not scan for wireless networks: {output}")
if '_WIFI' not in storage: if '_WIFI' not in storage:
storage['_WIFI'] = {} storage['_WIFI'] = {}
@ -76,8 +82,9 @@ def wireless_scan(interface):
# TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25 # TODO: Full WiFi experience might get evolved in the future, pausing for now 2021-01-25
def get_wireless_networks(interface): def get_wireless_networks(interface :str) -> None:
# TODO: Make this oneliner pritter to check if the interface is scanning or not. # TODO: Make this oneliner pritter to check if the interface is scanning or not.
# TODO: Rename this to list_wireless_networks() as it doesn't return anything
if '_WIFI' not in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False: if '_WIFI' not in storage or interface not in storage['_WIFI'] or storage['_WIFI'][interface].get('scanning', False) is False:
import time import time

View File

@ -3,6 +3,7 @@ import ssl
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from typing import Dict, Any
from .exceptions import RequirementError from .exceptions import RequirementError
@ -10,7 +11,7 @@ BASE_URL = 'https://archlinux.org/packages/search/json/?name={package}'
BASE_GROUP_URL = 'https://archlinux.org/groups/x86_64/{group}/' BASE_GROUP_URL = 'https://archlinux.org/groups/x86_64/{group}/'
def find_group(name): def find_group(name :str) -> bool:
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
@ -27,7 +28,7 @@ def find_group(name):
return True return True
def find_package(name): def find_package(name :str) -> Any:
""" """
Finds a specific package via the package database. Finds a specific package via the package database.
It makes a simple web-request, which might be a bit slow. It makes a simple web-request, which might be a bit slow.
@ -40,7 +41,7 @@ def find_package(name):
return json.loads(data) return json.loads(data)
def find_packages(*names): def find_packages(*names :str) -> Dict[str, Any]:
""" """
This function returns the search results for many packages. This function returns the search results for many packages.
The function itself is rather slow, so consider not sending to The function itself is rather slow, so consider not sending to
@ -49,7 +50,7 @@ def find_packages(*names):
return {package: find_package(package) for package in names} return {package: find_package(package) for package in names}
def validate_package_list(packages: list): def validate_package_list(packages: list) -> bool:
""" """
Validates a list of given packages. Validates a list of given packages.
Raises `RequirementError` if one or more packages are not found. Raises `RequirementError` if one or more packages are not found.

View File

@ -7,6 +7,8 @@ import pathlib
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from importlib import metadata from importlib import metadata
from typing import Optional, List
from types import ModuleType
from .output import log from .output import log
from .storage import storage from .storage import storage
@ -38,7 +40,7 @@ def localize_path(profile_path :str) -> str:
return profile_path return profile_path
def import_via_path(path :str, namespace=None): # -> module (not sure how to write that in type definitions) def import_via_path(path :str, namespace :Optional[str] = None) -> ModuleType:
if not namespace: if not namespace:
namespace = os.path.basename(path) namespace = os.path.basename(path)
@ -62,14 +64,14 @@ def import_via_path(path :str, namespace=None): # -> module (not sure how to wri
except: except:
pass pass
def find_nth(haystack, needle, n): def find_nth(haystack :List[str], needle :str, n :int) -> int:
start = haystack.find(needle) start = haystack.find(needle)
while start >= 0 and n > 1: while start >= 0 and n > 1:
start = haystack.find(needle, start + len(needle)) start = haystack.find(needle, start + len(needle))
n -= 1 n -= 1
return start return start
def load_plugin(path :str): # -> module (not sure how to write that in type definitions) def load_plugin(path :str) -> ModuleType:
parsed_url = urllib.parse.urlparse(path) parsed_url = urllib.parse.urlparse(path)
# The Profile was not a direct match on a remote URL # The Profile was not a direct match on a remote URL

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import hashlib import hashlib
import importlib.util import importlib.util
import json import json
@ -8,7 +9,11 @@ import sys
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from typing import Optional from typing import Optional, Dict, Union, TYPE_CHECKING
from types import ModuleType
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .installer import Installer
from .general import multisplit from .general import multisplit
from .networking import list_interfaces from .networking import list_interfaces
@ -16,16 +21,16 @@ from .storage import storage
from .exceptions import ProfileNotFound from .exceptions import ProfileNotFound
def grab_url_data(path): def grab_url_data(path :str) -> str:
safe_path = path[: path.find(':') + 1] + ''.join([item if item in ('/', '?', '=', '&') else urllib.parse.quote(item) for item in multisplit(path[path.find(':') + 1:], ('/', '?', '=', '&'))]) safe_path = path[: path.find(':') + 1] + ''.join([item if item in ('/', '?', '=', '&') else urllib.parse.quote(item) for item in multisplit(path[path.find(':') + 1:], ('/', '?', '=', '&'))])
ssl_context = ssl.create_default_context() ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE ssl_context.verify_mode = ssl.CERT_NONE
response = urllib.request.urlopen(safe_path, context=ssl_context) response = urllib.request.urlopen(safe_path, context=ssl_context)
return response.read() return response.read() # bytes?
def is_desktop_profile(profile) -> bool: def is_desktop_profile(profile :str) -> bool:
if str(profile) == 'Profile(desktop)': if str(profile) == 'Profile(desktop)':
return True return True
@ -42,8 +47,13 @@ def is_desktop_profile(profile) -> bool:
return False return False
def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_profiles=False): def list_profiles(
filter_irrelevant_macs :bool = True,
subpath :str = '',
filter_top_level_profiles :bool = False
) -> Dict[str, Dict[str, Union[str, bool]]]:
# TODO: Grab from github page as well, not just local static files # TODO: Grab from github page as well, not just local static files
if filter_irrelevant_macs: if filter_irrelevant_macs:
local_macs = list_interfaces() local_macs = list_interfaces()
@ -101,23 +111,27 @@ def list_profiles(filter_irrelevant_macs=True, subpath='', filter_top_level_prof
class Script: class Script:
def __init__(self, profile, installer=None): def __init__(self, profile :str, installer :Optional[Installer] = None):
# profile: https://hvornum.se/something.py """
# profile: desktop :param profile: A string representing either a boundled profile, a local python file
# profile: /path/to/profile.py or a remote path (URL) to a python script-profile. Three examples:
* profile: https://archlinux.org/some_profile.py
* profile: desktop
* profile: /path/to/profile.py
"""
self.profile = profile self.profile = profile
self.installer = installer self.installer = installer # TODO: Appears not to be used anymore?
self.converted_path = None self.converted_path = None
self.spec = None self.spec = None
self.examples = None self.examples = None
self.namespace = os.path.splitext(os.path.basename(self.path))[0] self.namespace = os.path.splitext(os.path.basename(self.path))[0]
self.original_namespace = self.namespace self.original_namespace = self.namespace
def __enter__(self, *args, **kwargs): def __enter__(self, *args :str, **kwargs :str) -> ModuleType:
self.execute() self.execute()
return sys.modules[self.namespace] return sys.modules[self.namespace]
def __exit__(self, *args, **kwargs): def __exit__(self, *args :str, **kwargs :str) -> None:
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
if len(args) >= 2 and args[1]: if len(args) >= 2 and args[1]:
raise args[1] raise args[1]
@ -125,7 +139,7 @@ class Script:
if self.original_namespace: if self.original_namespace:
self.namespace = self.original_namespace self.namespace = self.original_namespace
def localize_path(self, profile_path): def localize_path(self, profile_path :str) -> str:
if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'): if (url := urllib.parse.urlparse(profile_path)).scheme and url.scheme in ('https', 'http'):
if not self.converted_path: if not self.converted_path:
self.converted_path = f"/tmp/{os.path.basename(self.profile).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py" self.converted_path = f"/tmp/{os.path.basename(self.profile).replace('.py', '')}_{hashlib.md5(os.urandom(12)).hexdigest()}.py"
@ -138,7 +152,7 @@ class Script:
return profile_path return profile_path
@property @property
def path(self): def path(self) -> str:
parsed_url = urllib.parse.urlparse(self.profile) parsed_url = urllib.parse.urlparse(self.profile)
# The Profile was not a direct match on a remote URL # The Profile was not a direct match on a remote URL
@ -163,7 +177,7 @@ class Script:
else: else:
raise ProfileNotFound(f"Cannot handle scheme {parsed_url.scheme}") raise ProfileNotFound(f"Cannot handle scheme {parsed_url.scheme}")
def load_instructions(self, namespace=None): def load_instructions(self, namespace :Optional[str] = None) -> 'Script':
if namespace: if namespace:
self.namespace = namespace self.namespace = namespace
@ -173,7 +187,7 @@ class Script:
return self return self
def execute(self): def execute(self) -> ModuleType:
if self.namespace not in sys.modules or self.spec is None: if self.namespace not in sys.modules or self.spec is None:
self.load_instructions() self.load_instructions()
@ -183,25 +197,23 @@ class Script:
class Profile(Script): class Profile(Script):
def __init__(self, installer, path, args=None): def __init__(self, installer :Installer, path :str):
super(Profile, self).__init__(path, installer) super(Profile, self).__init__(path, installer)
if args is None:
args = {}
def __dump__(self, *args, **kwargs): def __dump__(self, *args :str, **kwargs :str) -> Dict[str, str]:
return {'path': self.path} return {'path': self.path}
def __repr__(self, *args, **kwargs): def __repr__(self, *args :str, **kwargs :str) -> str:
return f'Profile({os.path.basename(self.profile)})' return f'Profile({os.path.basename(self.profile)})'
def install(self): def install(self) -> ModuleType:
# Before installing, revert any temporary changes to the namespace. # Before installing, revert any temporary changes to the namespace.
# This ensures that the namespace during installation is the original initiation namespace. # This ensures that the namespace during installation is the original initiation namespace.
# (For instance awesome instead of aweosme.py or app-awesome.py) # (For instance awesome instead of aweosme.py or app-awesome.py)
self.namespace = self.original_namespace self.namespace = self.original_namespace
return self.execute() return self.execute()
def has_prep_function(self): def has_prep_function(self) -> bool:
with open(self.path, 'r') as source: with open(self.path, 'r') as source:
source_data = source.read() source_data = source.read()
@ -218,7 +230,7 @@ class Profile(Script):
return True return True
return False return False
def has_post_install(self): def has_post_install(self) -> bool:
with open(self.path, 'r') as source: with open(self.path, 'r') as source:
source_data = source.read() source_data = source.read()
@ -234,7 +246,7 @@ class Profile(Script):
if hasattr(imported, '_post_install'): if hasattr(imported, '_post_install'):
return True return True
def is_top_level_profile(self): def is_top_level_profile(self) -> bool:
with open(self.path, 'r') as source: with open(self.path, 'r') as source:
source_data = source.read() source_data = source.read()
@ -247,7 +259,7 @@ class Profile(Script):
# since developers like less code - omitting it should assume they want to present it. # since developers like less code - omitting it should assume they want to present it.
return True return True
def get_profile_description(self): def get_profile_description(self) -> str:
with open(self.path, 'r') as source: with open(self.path, 'r') as source:
source_data = source.read() source_data = source.read()
@ -282,11 +294,11 @@ class Profile(Script):
class Application(Profile): class Application(Profile):
def __repr__(self, *args, **kwargs): def __repr__(self, *args :str, **kwargs :str):
return f'Application({os.path.basename(self.profile)})' return f'Application({os.path.basename(self.profile)})'
@property @property
def path(self): def path(self) -> str:
parsed_url = urllib.parse.urlparse(self.profile) parsed_url = urllib.parse.urlparse(self.profile)
# The Profile was not a direct match on a remote URL # The Profile was not a direct match on a remote URL
@ -311,7 +323,7 @@ class Application(Profile):
else: else:
raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}") raise ProfileNotFound(f"Application cannot handle scheme {parsed_url.scheme}")
def install(self): def install(self) -> ModuleType:
# Before installing, revert any temporary changes to the namespace. # Before installing, revert any temporary changes to the namespace.
# This ensures that the namespace during installation is the original initiation namespace. # This ensures that the namespace during installation is the original initiation namespace.
# (For instance awesome instead of aweosme.py or app-awesome.py) # (For instance awesome instead of aweosme.py or app-awesome.py)

View File

@ -2,7 +2,7 @@ import os
from .general import SysCommand from .general import SysCommand
def service_state(service_name: str): def service_state(service_name: str) -> str:
if os.path.splitext(service_name)[1] != '.service': if os.path.splitext(service_name)[1] != '.service':
service_name += '.service' # Just to be safe service_name += '.service' # Just to be safe

View File

@ -1,5 +1,6 @@
import logging import logging
import time import time
from typing import Iterator
from .exceptions import SysCallError from .exceptions import SysCallError
from .general import SysCommand, SysCommandWorker, locate_binary from .general import SysCommand, SysCommandWorker, locate_binary
from .installer import Installer from .installer import Installer
@ -8,14 +9,14 @@ from .storage import storage
class Ini: class Ini:
def __init__(self, *args, **kwargs): def __init__(self, *args :str, **kwargs :str):
""" """
Limited INI handler for now. Limited INI handler for now.
Supports multiple keywords through dictionary list items. Supports multiple keywords through dictionary list items.
""" """
self.kwargs = kwargs self.kwargs = kwargs
def __str__(self): def __str__(self) -> str:
result = '' result = ''
first_row_done = False first_row_done = False
for top_level in self.kwargs: for top_level in self.kwargs:
@ -54,7 +55,7 @@ class Boot:
self.session = None self.session = None
self.ready = False self.ready = False
def __enter__(self): def __enter__(self) -> 'Boot':
if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance: if (existing_session := storage.get('active_boot', None)) and existing_session.instance != self.instance:
raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.") raise KeyError("Archinstall only supports booting up one instance, and a active session is already active and it is not this one.")
@ -81,7 +82,7 @@ class Boot:
storage['active_boot'] = self storage['active_boot'] = self
return self return self
def __exit__(self, *args, **kwargs): def __exit__(self, *args :str, **kwargs :str) -> None:
# b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync. # b''.join(sys_command('sync')) # No need to, since the underlying fs() object will call sync.
# TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager # TODO: https://stackoverflow.com/questions/28157929/how-to-safely-handle-an-exception-inside-a-context-manager
@ -98,24 +99,24 @@ class Boot:
else: else:
raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {shutdown}", exit_code=shutdown.exit_code) raise SysCallError(f"Could not shut down temporary boot of {self.instance}: {shutdown}", exit_code=shutdown.exit_code)
def __iter__(self): def __iter__(self) -> Iterator[str]:
if self.session: if self.session:
for value in self.session: for value in self.session:
yield value yield value
def __contains__(self, key: bytes): def __contains__(self, key: bytes) -> bool:
if self.session is None: if self.session is None:
return False return False
return key in self.session return key in self.session
def is_alive(self): def is_alive(self) -> bool:
if self.session is None: if self.session is None:
return False return False
return self.session.is_alive() return self.session.is_alive()
def SysCommand(self, cmd: list, *args, **kwargs): def SysCommand(self, cmd: list, *args, **kwargs) -> SysCommand:
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.
@ -125,7 +126,7 @@ class Boot:
return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs) return SysCommand(["systemd-run", f"--machine={self.container_name}", "--pty", *cmd], *args, **kwargs)
def SysCommandWorker(self, cmd: list, *args, **kwargs): def SysCommandWorker(self, cmd: list, *args, **kwargs) -> SysCommandWorker:
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])

View File

@ -1,3 +1,4 @@
from __future__ import annotations
import getpass import getpass
import ipaddress import ipaddress
import logging import logging
@ -7,11 +8,17 @@ import shutil
import signal import signal
import sys import sys
import time import time
from collections.abc import Iterable
from typing import List, Any, Optional, Dict, Union, TYPE_CHECKING
# https://stackoverflow.com/a/39757388/929999
if TYPE_CHECKING:
from .disk.partition import Partition
from .disk import BlockDevice, suggest_single_disk_layout, suggest_multi_disk_layout, valid_parted_position, all_disks from .disk import BlockDevice, suggest_single_disk_layout, suggest_multi_disk_layout, valid_parted_position, all_disks
from .exceptions import RequirementError, UserError, DiskError from .exceptions import RequirementError, UserError, DiskError
from .hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics from .hardware import AVAILABLE_GFX_DRIVERS, has_uefi, has_amd_graphics, has_intel_graphics, has_nvidia_graphics
from .locale_helpers import list_keyboard_languages, list_timezones from .locale_helpers import list_keyboard_languages, list_timezones, list_locales
from .networking import list_interfaces from .networking import list_interfaces
from .menu import Menu from .menu import Menu
from .output import log from .output import log
@ -21,22 +28,22 @@ from .mirrors import list_mirrors
# TODO: Some inconsistencies between the selection processes. # TODO: Some inconsistencies between the selection processes.
# Some return the keys from the options, some the values? # Some return the keys from the options, some the values?
from .. import fs_types from .. import fs_types, validate_package_list
# TODO: These can be removed after the move to simple_menu.py
def get_terminal_height(): def get_terminal_height() -> int:
return shutil.get_terminal_size().lines return shutil.get_terminal_size().lines
def get_terminal_width(): def get_terminal_width() -> int:
return shutil.get_terminal_size().columns return shutil.get_terminal_size().columns
def get_longest_option(options): def get_longest_option(options :List[Any]) -> int:
return max([len(x) for x in options]) return max([len(x) for x in options])
def check_for_correct_username(username): def check_for_correct_username(username :str) -> bool:
if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32: if re.match(r'^[a-z_][a-z0-9_-]*\$?$', username) and len(username) <= 32:
return True return True
log( log(
@ -47,14 +54,14 @@ def check_for_correct_username(username):
return False return False
def do_countdown(): def do_countdown() -> bool:
SIG_TRIGGER = False SIG_TRIGGER = False
def kill_handler(sig, frame): def kill_handler(sig :int, frame :Any) -> None:
print() print()
exit(0) exit(0)
def sig_handler(sig, frame): def sig_handler(sig :int, frame :Any) -> None:
global SIG_TRIGGER global SIG_TRIGGER
SIG_TRIGGER = True SIG_TRIGGER = True
signal.signal(signal.SIGINT, kill_handler) signal.signal(signal.SIGINT, kill_handler)
@ -79,12 +86,14 @@ def do_countdown():
sys.stdin.read() sys.stdin.read()
SIG_TRIGGER = False SIG_TRIGGER = False
signal.signal(signal.SIGINT, sig_handler) signal.signal(signal.SIGINT, sig_handler)
print() print()
signal.signal(signal.SIGINT, original_sigint_handler) signal.signal(signal.SIGINT, original_sigint_handler)
return True return True
def get_password(prompt="Enter a password: "): def get_password(prompt :str = "Enter a password: ") -> Optional[str]:
while passwd := getpass.getpass(prompt): while passwd := getpass.getpass(prompt):
passwd_verification = getpass.getpass(prompt='And one more time for verification: ') passwd_verification = getpass.getpass(prompt='And one more time for verification: ')
if passwd != passwd_verification: if passwd != passwd_verification:
@ -98,7 +107,7 @@ def get_password(prompt="Enter a password: "):
return None return None
def print_large_list(options, padding=5, margin_bottom=0, separator=': '): def print_large_list(options :List[str], padding :int = 5, margin_bottom :int = 0, separator :str = ': ') -> List[int]:
highest_index_number_length = len(str(len(options))) highest_index_number_length = len(str(len(options)))
longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding longest_line = highest_index_number_length + len(separator) + get_longest_option(options) + padding
spaces_without_option = longest_line - (len(separator) + highest_index_number_length) spaces_without_option = longest_line - (len(separator) + highest_index_number_length)
@ -136,6 +145,7 @@ def select_encrypted_partitions(block_devices :dict, password :str) -> dict:
# Users might want to single out a partition for non-encryption to share between dualboot etc. # Users might want to single out a partition for non-encryption to share between dualboot etc.
# TODO: This can be removed once we have simple_menu everywhere
class MiniCurses: class MiniCurses:
def __init__(self, width, height): def __init__(self, width, height):
self.width = width self.width = width
@ -255,11 +265,24 @@ class MiniCurses:
return response return response
def ask_for_swap(prompt='Would you like to use swap on zram? (Y/n): ', forced=False): def ask_for_swap(prompt='Would you like to use swap on zram?', forced=False):
return True if input(prompt).strip(' ').lower() not in ('n', 'no') else False choice = Menu(prompt, ['yes', 'no'], default_option='yes').run()
return False if choice == 'no' else True
def ask_for_superuser_account(prompt='Username for required superuser with sudo privileges: ', forced=False): def ask_ntp():
prompt = 'Would you like to use automatic time synchronization (NTP) with the default time servers?'
prompt += 'Hardware time and other post-configuration steps might be required in order for NTP to work. For more information, please check the Arch wiki'
choice = Menu(prompt, ['yes', 'no'], skip=False, default_option='yes').run()
return False if choice == 'no' else True
def ask_hostname():
hostname = input('Desired hostname for the installation: ').strip(' ')
return hostname
def ask_for_superuser_account(prompt :str = 'Username for required superuser with sudo privileges: ', forced :bool = False) -> Dict[str, Dict[str, str]]:
while 1: while 1:
new_user = input(prompt).strip(' ') new_user = input(prompt).strip(' ')
@ -277,7 +300,7 @@ def ask_for_superuser_account(prompt='Username for required superuser with sudo
return {new_user: {"!password": password}} return {new_user: {"!password": password}}
def ask_for_additional_users(prompt='Any additional users to install (leave blank for no users): '): def ask_for_additional_users(prompt :str = 'Any additional users to install (leave blank for no users): ') -> List[Dict[str, Dict[str, str]]]:
users = {} users = {}
superusers = {} superusers = {}
@ -297,13 +320,13 @@ def ask_for_additional_users(prompt='Any additional users to install (leave blan
return users, superusers return users, superusers
def ask_for_a_timezone(): def ask_for_a_timezone() -> str:
timezones = list_timezones() timezones = list_timezones()
default = 'UTC' default = 'UTC'
selected_tz = Menu( selected_tz = Menu(
f'Select a timezone or leave blank to use default "{default}"', f'Select a timezone or leave blank to use default "{default}"',
timezones, list(timezones),
skip=False, skip=False,
default_option=default default_option=default
).run() ).run()
@ -311,12 +334,12 @@ def ask_for_a_timezone():
return selected_tz return selected_tz
def ask_for_bootloader(advanced_options=False) -> str: def ask_for_bootloader(advanced_options :bool = False) -> str:
bootloader = "systemd-bootctl" if has_uefi() else "grub-install" bootloader = "systemd-bootctl" if has_uefi() else "grub-install"
if has_uefi(): if has_uefi():
if not advanced_options: if not advanced_options:
bootloader_choice = input("Would you like to use GRUB as a bootloader instead of systemd-boot? [y/N] ").lower() bootloader_choice = Menu('Would you like to use GRUB as a bootloader instead of systemd-boot?', ['yes', 'no'], default_option='no').run()
if bootloader_choice == "y": if bootloader_choice == "yes":
bootloader = "grub-install" bootloader = "grub-install"
else: else:
# We use the common names for the bootloader as the selection, and map it back to the expected values. # We use the common names for the bootloader as the selection, and map it back to the expected values.
@ -333,14 +356,46 @@ def ask_for_bootloader(advanced_options=False) -> str:
return bootloader return bootloader
def ask_for_audio_selection(desktop=True): def ask_for_audio_selection(desktop :bool = True) -> str:
audio = 'pipewire' if desktop else 'none' audio = 'pipewire' if desktop else 'none'
choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', 'none'] choices = ['pipewire', 'pulseaudio'] if desktop else ['pipewire', 'pulseaudio', 'none']
selected_audio = Menu(f'Choose an audio server or leave blank to use "{audio}"', choices, default_option=audio).run() selected_audio = Menu(
f'Choose an audio server',
choices,
default_option=audio,
skip=False
).run()
return selected_audio return selected_audio
def ask_to_configure_network(): # TODO: Remove? Moved?
def ask_additional_packages_to_install(packages :List[str] = None) -> List[str]:
# Additional packages (with some light weight error handling for invalid package names)
print(
"Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.")
print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.")
while True:
if packages is None:
packages = [p for p in input(
'Write additional packages to install (space separated, leave blank to skip): '
).split(' ') if len(p)]
if len(packages):
# Verify packages that were given
try:
log("Verifying that additional packages exist (this might take a few seconds)")
validate_package_list(packages)
break
except RequirementError as e:
log(e, fg='red')
else:
# no additional packages were selected, which we'll allow
break
return packages
def ask_to_configure_network() -> Dict[str, Any]:
# Optionally configure one network interface. # Optionally configure one network interface.
# while 1: # while 1:
# {MAC: Ifname} # {MAC: Ifname}
@ -350,7 +405,7 @@ def ask_to_configure_network():
**list_interfaces() **list_interfaces()
} }
nic = Menu('Select one network interface to configure', interfaces.values()).run() nic = Menu('Select one network interface to configure', list(interfaces.values())).run()
if nic and nic != 'Copy ISO network configuration to installation': if nic and nic != 'Copy ISO network configuration to installation':
if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)': if nic == 'Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)':
@ -435,7 +490,7 @@ def ask_for_main_filesystem_format(advanced_options=False):
return Menu('Select which filesystem your main partition should use', options, skip=False).run() return Menu('Select which filesystem your main partition should use', options, skip=False).run()
def current_partition_layout(partitions, with_idx=False): def current_partition_layout(partitions :List[Partition], with_idx :bool = False) -> Dict[str, Any]:
def do_padding(name, max_len): def do_padding(name, max_len):
spaces = abs(len(str(name)) - max_len) + 2 spaces = abs(len(str(name)) - max_len) + 2
pad_left = int(spaces / 2) pad_left = int(spaces / 2)
@ -479,7 +534,7 @@ def current_partition_layout(partitions, with_idx=False):
return f'\n\nCurrent partition layout:\n\n{current_layout}' return f'\n\nCurrent partition layout:\n\n{current_layout}'
def select_partition(title, partitions, multiple=False): def select_partition(title :str, partitions :List[Partition], multiple :bool = False) -> Union[int, List[int], None]:
partition_indexes = list(map(str, range(len(partitions)))) partition_indexes = list(map(str, range(len(partitions))))
partition = Menu(title, partition_indexes, multi=multiple).run() partition = Menu(title, partition_indexes, multi=multiple).run()
@ -491,47 +546,18 @@ def select_partition(title, partitions, multiple=False):
return None return None
def get_default_partition_layout(block_devices, advanced_options=False): def get_default_partition_layout(
block_devices :Union[BlockDevice, List[BlockDevice]],
advanced_options :bool = False
) -> Dict[str, Any]:
if len(block_devices) == 1: if len(block_devices) == 1:
return suggest_single_disk_layout(block_devices[0], advanced_options=advanced_options) return suggest_single_disk_layout(block_devices[0], advanced_options=advanced_options)
else: else:
return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options) return suggest_multi_disk_layout(block_devices, advanced_options=advanced_options)
def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict: def manage_new_and_existing_partitions(block_device :BlockDevice) -> Dict[str, Any]:
# if has_uefi():
# partition_type = 'gpt'
# else:
# partition_type = 'msdos'
# log(f"Selecting which partitions to re-use on {block_device}...", fg="yellow", level=logging.INFO)
# partitions = generic_multi_select(block_device.partitions.values(), "Select which partitions to re-use (the rest will be left alone): ", sort=True)
# partitions_to_wipe = generic_multi_select(partitions, "Which partitions do you wish to wipe (multiple can be selected): ", sort=True)
# mountpoints = {}
# struct = {
# "partitions" : []
# }
# for partition in partitions:
# mountpoint = input(f"Select a mountpoint (or skip) for {partition}: ").strip()
# part_struct = {}
# if mountpoint:
# part_struct['mountpoint'] = mountpoint
# if mountpoint == '/boot':
# part_struct['boot'] = True
# if has_uefi():
# part_struct['ESP'] = True
# elif mountpoint == '/' and
# if partition.uuid:
# part_struct['PARTUUID'] = partition.uuid
# if partition in partitions_to_wipe:
# part_struct['wipe'] = True
# struct['partitions'].append(part_struct)
# return struct
block_device_struct = { block_device_struct = {
"partitions": [partition.__dump__() for partition in block_device.partitions.values()] "partitions": [partition.__dump__() for partition in block_device.partitions.values()]
} }
@ -689,7 +715,7 @@ def manage_new_and_existing_partitions(block_device :BlockDevice) -> dict:
return block_device_struct return block_device_struct
def select_individual_blockdevice_usage(block_devices: list): def select_individual_blockdevice_usage(block_devices: list) -> Dict[str, Any]:
result = {} result = {}
for device in block_devices: for device in block_devices:
@ -700,7 +726,7 @@ def select_individual_blockdevice_usage(block_devices: list):
return result return result
def select_disk_layout(block_devices :list, advanced_options=False): def select_disk_layout(block_devices :list, advanced_options=False) -> Dict[str, Any]:
modes = [ modes = [
"Wipe all selected drives and use a best-effort default partition layout", "Wipe all selected drives and use a best-effort default partition layout",
"Select what to do with each individual drive (followed by partition usage)" "Select what to do with each individual drive (followed by partition usage)"
@ -714,7 +740,7 @@ def select_disk_layout(block_devices :list, advanced_options=False):
return select_individual_blockdevice_usage(block_devices) return select_individual_blockdevice_usage(block_devices)
def select_disk(dict_o_disks): def select_disk(dict_o_disks :Dict[str, BlockDevice]) -> BlockDevice:
""" """
Asks the user to select a harddrive from the `dict_o_disks` selection. Asks the user to select a harddrive from the `dict_o_disks` selection.
Usually this is combined with :ref:`archinstall.list_drives`. Usually this is combined with :ref:`archinstall.list_drives`.
@ -742,7 +768,7 @@ def select_disk(dict_o_disks):
raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.') raise DiskError('select_disk() requires a non-empty dictionary of disks to select from.')
def select_profile(): def select_profile() -> Optional[str]:
""" """
# Asks the user to select a profile from the available profiles. # Asks the user to select a profile from the available profiles.
# #
@ -762,7 +788,7 @@ def select_profile():
title = 'This is a list of pre-programmed profiles, ' \ title = 'This is a list of pre-programmed profiles, ' \
'they might make it easier to install things like desktop environments' 'they might make it easier to install things like desktop environments'
selection = Menu(title=title, options=options.keys()).run() selection = Menu(title=title, p_options=list(options.keys())).run()
if selection is not None: if selection is not None:
return options[selection] return options[selection]
@ -770,7 +796,7 @@ def select_profile():
return None return None
def select_language(): def select_language(default_value :str) -> str:
""" """
Asks the user to select a language Asks the user to select a language
Usually this is combined with :ref:`archinstall.list_keyboard_languages`. Usually this is combined with :ref:`archinstall.list_keyboard_languages`.
@ -784,11 +810,11 @@ def select_language():
# allows for searching anyways # allows for searching anyways
sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len) sorted_kb_lang = sorted(sorted(list(kb_lang)), key=len)
selected_lang = Menu('Select Keyboard layout', sorted_kb_lang, default_option='us', sort=False).run() selected_lang = Menu('Select Keyboard layout', sorted_kb_lang, default_option=default_value, sort=False).run()
return selected_lang return selected_lang
def select_mirror_regions(): def select_mirror_regions() -> Dict[str, Any]:
""" """
Asks the user to select a mirror or region Asks the user to select a mirror or region
Usually this is combined with :ref:`archinstall.list_mirrors`. Usually this is combined with :ref:`archinstall.list_mirrors`.
@ -800,7 +826,7 @@ def select_mirror_regions():
mirrors = list_mirrors() mirrors = list_mirrors()
selected_mirror = Menu( selected_mirror = Menu(
'Select one of the regions to download packages from', 'Select one of the regions to download packages from',
mirrors.keys(), list(mirrors.keys()),
multi=True multi=True
).run() ).run()
@ -810,7 +836,7 @@ def select_mirror_regions():
return {} return {}
def select_harddrives(): def select_harddrives() -> Optional[str]:
""" """
Asks the user to select one or multiple hard drives Asks the user to select one or multiple hard drives
@ -822,17 +848,17 @@ def select_harddrives():
selected_harddrive = Menu( selected_harddrive = Menu(
'Select one or more hard drives to use and configure', 'Select one or more hard drives to use and configure',
options.keys(), list(options.keys()),
multi=True multi=True
).run() ).run()
if selected_harddrive and len(selected_harddrive) > 0: if selected_harddrive and len(selected_harddrive) > 0:
return [options[i] for i in selected_harddrive] return [options[i] for i in selected_harddrive]
return None return []
def select_driver(options=AVAILABLE_GFX_DRIVERS): def select_driver(options :Dict[str, Any] = AVAILABLE_GFX_DRIVERS) -> str:
""" """
Some what convoluted function, whose job is simple. Some what convoluted function, whose job is simple.
Select a graphics driver from a pre-defined set of popular options. Select a graphics driver from a pre-defined set of popular options.
@ -866,7 +892,7 @@ def select_driver(options=AVAILABLE_GFX_DRIVERS):
raise RequirementError("Selecting drivers require a least one profile to be given as an option.") raise RequirementError("Selecting drivers require a least one profile to be given as an option.")
def select_kernel(): def select_kernel() -> List[str]:
""" """
Asks the user to select a kernel for system. Asks the user to select a kernel for system.
@ -886,3 +912,109 @@ def select_kernel():
).run() ).run()
return selected_kernels return selected_kernels
def select_locale_lang(default):
locales = list_locales()
locale_lang = set([locale.split()[0] for locale in locales])
selected_locale = Menu(
f'Choose which locale language to use',
locale_lang,
sort=True,
default_option=default
).run()
return selected_locale
def select_locale_enc(default):
locales = list_locales()
locale_enc = set([locale.split()[1] for locale in locales])
selected_locale = Menu(
f'Choose which locale encoding to use',
locale_enc,
sort=True,
default_option=default
).run()
return selected_locale
def generic_select(p_options :Union[list,dict],
input_text :str = "Select one of the values shown below: ",
allow_empty_input :bool = True,
options_output :bool = True, # function not available
sort :bool = False,
multi :bool = False,
default :Any = None) -> Any:
"""
A generic select function that does not output anything
other than the options and their indexes. As an example:
generic_select(["first", "second", "third option"])
> first
second
third option
When the user has entered the option correctly,
this function returns an item from list, a string, or None
Options can be any iterable.
Duplicate entries are not checked, but the results with them are unreliable. Which element to choose from the duplicates depends on the return of the index()
Default value if not on the list of options will be added as the first element
sort will be handled by Menu()
"""
# We check that the options are iterable. If not we abort. Else we copy them to lists
# it options is a dictionary we use the values as entries of the list
# if options is a string object, each character becomes an entry
# if options is a list, we implictily build a copy to mantain immutability
if not isinstance(p_options,Iterable):
log(f"Objects of type {type(p_options)} is not iterable, and are not supported at generic_select",fg="red")
log(f"invalid parameter at Menu() call was at <{sys._getframe(1).f_code.co_name}>",level=logging.WARNING)
raise RequirementError("generic_select() requires an iterable as option.")
if isinstance(p_options,dict):
options = list(p_options.values())
else:
options = list(p_options)
# check that the default value is in the list. If not it will become the first entry
if default and default not in options:
options.insert(0,default)
# one of the drawbacks of the new interface is that in only allows string like options, so we do a conversion
# also for the default value if it exists
soptions = list(map(str,options))
default_value = options[options.index(default)] if default else None
selected_option = Menu(
input_text,
soptions,
skip=allow_empty_input,
multi=multi,
default_option=default_value,
sort=sort
).run()
# we return the original objects, not the strings.
# options is the list with the original objects and soptions the list with the string values
# thru the map, we get from the value selected in soptions it index, and thu it the original object
if not selected_option:
return selected_option
elif isinstance(selected_option,list): # for multi True
selected_option = list(map(lambda x: options[soptions.index(x)],selected_option))
else: # for multi False
selected_option = options[soptions.index(selected_option)]
return selected_option
def generic_multi_select(p_options :Union[list,dict],
text :str = "Select one or more of the options below: ",
sort :bool = False,
default :Any = None,
allow_empty :bool = False) -> Any:
return generic_select(p_options,
input_text=text,
allow_empty_input=allow_empty,
sort=sort,
multi=True,
default=default)

View File

@ -12,7 +12,7 @@ Packages
======== ========
.. autofunction:: archinstall.find_package .. autofunction:: archinstall.find_package
Be
.. autofunction:: archinstall.find_packages .. autofunction:: archinstall.find_packages
Locale related Locale related
@ -68,10 +68,20 @@ Luks (Disk encryption)
Networking Networking
========== ==========
.. autofunction:: archinstall.getHwAddr .. autofunction:: archinstall.get_hw_addr
.. autofunction:: archinstall.list_interfaces .. autofunction:: archinstall.list_interfaces
.. autofunction:: archinstall.check_mirror_reachable
.. autofunction:: archinstall.enrich_iface_types
.. autofunction:: archinstall.get_interface_from_mac
.. autofunction:: archinstall.wireless_scan
.. autofunction:: archinstall.get_wireless_networks
General General
======= =======
@ -79,7 +89,9 @@ General
.. autofunction:: archinstall.locate_binary .. autofunction:: archinstall.locate_binary
.. autofunction:: archinstall.sys_command .. autofunction:: archinstall.SysCommand
.. autofunction:: archinstall.SysCommandWorker
Exceptions Exceptions
========== ==========

View File

@ -5,10 +5,10 @@ Discord
There's a discord channel which is frequented by some `contributors <https://github.com/archlinux/archinstall/graphs/contributors>`_. There's a discord channel which is frequented by some `contributors <https://github.com/archlinux/archinstall/graphs/contributors>`_.
To join the server, head over to `https://discord.gg/cqXU88y <https://discord.gg/cqXU88y>`_'s server and join in. | To join the server, head over to `https://discord.gg/cqXU88y <https://discord.gg/cqXU88y>`_ and join in.
There's not many rules other than common sense and to treat others with respect. | There's not many rules other than common sense and to treat others with respect. The general chat is for off-topic things as well.
There's the `@Party Animals` role if you want notifications of new releases which is posted in the `#Release Party` channel. There's the ``@Party Animals`` role if you want notifications of new releases which is posted in the ``#Release Party`` channel.
Another thing is the `@Contributors` role which you can get by writing `!verify` and verify that you're a contributor. Another thing is the ``@Contributors`` role can be activated by contributors by writing ``!verify`` and follow the verification process.
Hop in, I hope to see you there! : ) Hop in, we hope to see you there! : )

View File

@ -8,15 +8,17 @@ Issues and bugs should be reported over at `https://github.com/archlinux/archins
General questions, enhancements and security issues can be reported over there too. General questions, enhancements and security issues can be reported over there too.
For quick issues or if you need help, head over to the Discord server which has a help channel. For quick issues or if you need help, head over to the Discord server which has a help channel.
Submitting a help ticket Log files
======================== ---------
| When submitting a help ticket, please include the :code:`/var/log/archinstall/install.log`. | When submitting a help ticket, please include the :code:`/var/log/archinstall/install.log`.
| It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in. | It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in.
| There are additional worker files, these worker files contain individual command input and output. | There are additional log files under ``/var/log/archinstall/`` that can be useful.
| These worker files are located in :code:`~/.cache/archinstall/` and do not need to be submitted by default when submitting issues. | For instance the ``cmd_history.txt`` which contains a fully transparent list of all commands executed.
| Or ``cmd_output.txt`` which is a transcript and contains any output seen on the screen.
.. warning:: .. warning::
Worker log-files *may* contain sensitive information such as **passwords** and **private information**. Never submit these logs without going through them manually making sure they're good for submission. Or submit only parts of them which are relevant to the issue itself. We only try to guarantee that ``/var/log/archinstall/install.log`` is free from sensitive information.
Any other log should be pasted with **utmost care**!

View File

@ -1,18 +1,20 @@
python-archinstall Documentation archinstall Documentation
================================ =========================
| **python-archinstall** *(or, archinstall for short)* is a helper library to install Arch Linux and manage services, packages and other things. | **archinstall** is library which can be used to install Arch Linux.
| It comes packaged with different pre-configured installers, such as the :ref:`guided_installation` installer. | The library comes packaged with different pre-configured installers, such as the default :ref:`guided` installer.
| |
| A demo can be viewed here: `https://www.youtube.com/watch?v=9Xt7X_Iqg6E <https://www.youtube.com/watch?v=9Xt7X_Iqg6E>`_ which uses the default guided installer. | A demo of the :ref:`guided` installer can be seen here: `https://www.youtube.com/watch?v=9Xt7X_Iqg6E <https://www.youtube.com/watch?v=9Xt7X_Iqg6E>`_.
Some of the features of Archinstall are: Some of the features of Archinstall are:
* **No external dependencies or installation requirements.** Runs without any external requirements or installation processes. * **No external dependencies or installation requirements.** Runs without any external requirements or installation processes.
* **Single threaded and context friendly.** The library always executes calls in sequential order to ensure installation-steps don't overlap or execute in the wrong order. It also supports *(and uses)* context wrappers to ensure things such as `sync` are called so data and configurations aren't lost. * **Context friendly.** The library always executes calls in sequential order to ensure installation-steps don't overlap or execute in the wrong order. It also supports *(and uses)* context wrappers to ensure cleanup and final tasks such as ``mkinitcpio`` are called when needed.
* **Supports standalone executable** The library can be compiled into a single executable and run on any system with or without Python. This is ideal for live mediums that don't want to ship Python as a big dependency. * **Full transparancy** Logs and insights can be found at ``/var/log/archinstall`` both in the live ISO and the installed system.
* **Accessibility friendly** Archinstall works with ``espeakup`` and other accessibility tools thanks to the use of a TUI.
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
@ -29,16 +31,16 @@ Some of the features of Archinstall are:
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
:caption: Installing the library :caption: Archinstall as a library
installing/python installing/python
installing/binary examples/python
.. toctree:: .. toctree::
:maxdepth: 3 :maxdepth: 3
:caption: Using the library :caption: Archinstall as a binary
examples/python installing/binary
examples/binary examples/binary
.. ..
examples/scripting examples/scripting

View File

@ -1,53 +1,52 @@
.. _guided_installation: .. _guided:
.. autofunction:: guided_installation
Guided installation Guided installation
=================== ===================
This is the default scripted installation you'll encounter on the official Arch Linux Archinstall package as well as the unofficial ISO found on `https://archlinux.life <https://archlinux.life>`_. It will guide you through a very basic installation of Arch Linux. | This is the default script the Arch Linux `Archinstall package <https://archlinux.org/packages/extra/any/archinstall/>`_.
| It will guide you through a very basic installation of Arch Linux.
The installer has two pre-requisites:
* A Physical or Virtual machine to install on
* An active internet connection prior to running archinstall
.. warning::
A basic understanding of machines, ISO-files and command lines are needed.
Please read the official Arch Linux Wiki (`https://wiki.archlinux.org/ <https://wiki.archlinux.org/>`_) to learn more
.. note:: .. note::
There are some limitations with the installer, such as that it will not configure WiFi during the installation procedure. And it will not perform a post-installation network configuration either. So you need to read up on `Arch Linux networking <https://wiki.archlinux.org/index.php/Network_configuration>`_ to get that to work. There are other scripts and they can be invoked by executing `archinstall <script>` *(without .py)*. To see a complete list of scripts, see the source code directory `examples/ <https://github.com/archlinux/archinstall/tree/master/examples>`_
The installer has three pre-requisites:
* The latest version of `Arch Linux ISO <https://archlinux.org/download/>`_
* A physical or virtual machine to install on
* A `working internet connection <https://wiki.archlinux.org/title/installation_guide#Connect_to_the_internet>`_ prior to running archinstall
.. note::
A basic understanding of machines, ISO-files and command line arguments are needed.
Please read the official `Arch Linux Wiki <https://wiki.archlinux.org/>`_ to learn more about your future operating system.
.. warning::
The installer will not configure WiFi before the installation begins. You need to read up on `Arch Linux networking <https://wiki.archlinux.org/index.php/Network_configuration>`_ before you continue.
Running the guided installation Running the guided installation
------------------------------- -------------------------------
.. note:: To start the installer, run the following in the latest Arch Linux ISO:
Due to the package being quite new, it might be required to update the local package list before installing or continuing. Partial upgrades can cause issues, but the lack of dependencies should make this perfectly safe:
.. code::bash
# pacman -Syy
To install archinstall and subsequently the guided installer, simply do the following:
.. code-block:: sh .. code-block:: sh
pacman -S python-archinstall archinstall --script guided
And to run it, execute archinstall as a Python module:
.. code-block:: sh
python -m archinstall --script guided
| The ``--script guided`` argument is optional as it's the default behavior. | The ``--script guided`` argument is optional as it's the default behavior.
| But this will start the process of guiding you through an installation of a quite minimal Arch Linux experience. | But this will use our most guided installation and if you skip all the option steps it will install a minimal Arch Linux experience.
Installing directly from a configuration file Installing directly from a configuration file
-------------------------------------- ---------------------------------------------
| The guided installation also supports installing with pre-configured answers to all the guided steps.
| This can be a quick and convenient way to re-run one or several installations.
There are three different configuration files, all of which are optional.
* ``--config`` that deals with the general configuration of language and which profiles to use.
* ``--creds`` which takes any ``superuser``, ``user`` or ``root`` account data.
* ``--disk_layouts`` for defining the desired partition strategy on the selected ``"harddrives"`` in ``--config``.
.. note:: .. note::
Edit the following json according to your needs, You can always get the latest options with ``archinstall --dry-run``, but edit the following json according to your needs.
save this as a json file, and provide the local or remote path (URL) Save the configuration as a ``.json`` file. Archinstall can source it via a local or remote path (URL)
.. code-block:: json .. code-block:: json
@ -59,12 +58,12 @@ Installing directly from a configuration file
"chown -R devel:devel /home/devel/paru", "chown -R devel:devel /home/devel/paru",
"usermod -aG docker devel" "usermod -aG docker devel"
], ],
"!encryption-password": "supersecret",
"filesystem": "btrfs", "filesystem": "btrfs",
"gfx_driver": "VMware / VirtualBox (open-source)", "gfx_driver": "VMware / VirtualBox (open-source)",
"harddrive": { "harddrives": [
"path": "/dev/nvme0n1" "/dev/nvme0n1"
}, ],
"swap": true,
"hostname": "development-box", "hostname": "development-box",
"kernels": [ "kernels": [
"linux" "linux"
@ -72,217 +71,176 @@ Installing directly from a configuration file
"keyboard-language": "us", "keyboard-language": "us",
"mirror-region": "Worldwide", "mirror-region": "Worldwide",
"nic": { "nic": {
"NetworkManager": true "NetworkManager": true,
"nic": "Use NetworkManager (necessary to configure internet graphically in GNOME and KDE)"
}, },
"ntp": true, "ntp": true,
"packages": ["docker", "git", "wget", "zsh"], "packages": ["docker", "git", "wget", "zsh"],
"profile": "gnome", "profile": "gnome",
"services": ["docker"], "services": ["docker"],
"superusers": {
"devel": {
"!password": "devel"
}
},
"sys-encoding": "utf-8", "sys-encoding": "utf-8",
"sys-language": "en_US", "sys-language": "en_US",
"timezone": "US/Eastern", "timezone": "US/Eastern",
"users": {}
} }
To run it, execute archinstall as a Python module: To use it, assuming you put it on ``https://domain.lan/config.json``:
.. code-block:: sh .. code-block:: sh
python -m archinstall --config <local path or remote URL> archinstall --config https://domain.lan/config.json
Options for ``--config``
------------------------
*(To see which keys are required, scroll to the right in the above table.)*
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| Key | Values | Description | Required |
| | | | |
+======================+========================================================+=============================================================================================+===============================================+
| audio | pipewire/pulseaudio | Audioserver to be installed | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| bootloader | systemd-bootctl/grub-install | Bootloader to be installed *(grub being mandatory on BIOS machines)* | Yes |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| custom-commands | [ <command1>, <command2>, ...] | Custom commands to be run post install | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| gfx_driver | - "VMware / VirtualBox (open-source)" | Graphics Drivers to install | No |
| | - "Nvidia" | | |
| | - "Intel (open-source)" | | |
| | - "AMD / ATI (open-source)" | | |
| | - "All open-source (default)" | | |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| harddrives | [ <path of device>, <path of second device>, ... } | Multiple paths to block devices to be formatted | No[1] |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| hostname | any | Hostname of machine after installation. Default will be ``archinstall`` | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| kernels | [ "kernel1", "kernel2"] | List of kernels to install eg: linux, linux-lts, linux-zen etc | Atleast 1 |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| keyboard-language | Any valid layout given by ``localectl list-keymaps`` | eg: ``us``, ``de`` or ``de-latin1`` etc. Defaults to ``us`` | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| mirror-region | | {"<Region Name>": { "Mirror URL": True/False}, ..} | | Defaults to automatic selection. | No |
| | | "Worldwide" or "Sweden" | | Either takes a dictionary structure of region and a given set of mirrors. | |
| | | | Or just a region and archinstall will source any mirrors for that region automatically | |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| nic | | { NetworkManager: <boolean> } | | Takes three different kind of options. Copy, NetworkManager or a nic name. | No |
| | | { "eth0": {"address": "<ip>", "subnet": "255.0.0.0"}}| | Copy will copy the network configuration used in the live ISO. NetworkManager will | |
| | | "Copy ISO network configuration to installation" | | install and configure `NetworkManager <https://wiki.archlinux.org/title/NetworkManager>`_ | |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| ntp | <boolean> | Set to true to set-up ntp post install | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| packages | [ "package1", "package2", ..] | List of packages to install post-installation | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| profile | Name of the profile to install | Profiles are present in | No |
| | | `profiles/ <https://github.com/archlinux/archinstall/tree/master/profiles>`_, | |
| | | use the name of a profile to install it without the ``.py`` extension. | |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| services | [ "service1", "service2", ..] | Services to enable post-installation | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| sys-encoding | "utf-8" | Set to change system encoding post-install, ignored if --advanced flag is not passed | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| sys-language | "en_US" | Set to change system language post-install, ignored if --advanced flag is not passed | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
| timezone | Timezone to configure in installation | Timezone eg: UTC, Asia/Kolkata etc. Defaults to UTC | No |
+----------------------+--------------------------------------------------------+---------------------------------------------------------------------------------------------+-----------------------------------------------+
.. note::
[1] If no entires are found in ``harddrives``, archinstall guided installation will use whatever is mounted currently under ``/mnt/archinstall``.
Options for ``--creds``
-----------------------
| Creds is a separate configuration file to separate normal options from more sensitive data like passwords.
| Below is an example of how to set the root password and below that are description of other values that can be set.
.. code-block:: json
{
"!root-password" : "SecretSanta2022"
}
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| Key | Values/Description | Description | Required | | Key | Values | Description | Required |
| | | | | | | | | |
+======================+=====================================================+======================================================================================+===============================================+ +======================+=====================================================+======================================================================================+===============================================+
| audio | pipewire/pulseaudio | Audioserver to be installed | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| bootloader | systemd-bootctl/grub-install | Bootloader to be installed | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| custom-commands | [ <command1>, <command2>, ...] | Custom commands to be run post install | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No | | !encryption-password | any | Password to encrypt disk, not encrypted if password not provided | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| filesystem | ext4 / btrfs / fat32 etc. | Filesystem for root and home partitions | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| gfx_driver | - "VMware / VirtualBox (open-source)" | Graphics Drivers to install | No |
| | - "Nvidia" | | |
| | - "Intel (open-source)" | | |
| | - "AMD / ATI (open-source)" | | |
| | - "All open-source (default)" | | |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| harddrive | { "path": <path of device> } | Path of device to be used | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| hostname | any | Hostname of machine after installation | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| kernels | [ "kernel1", "kernel2"] | List of kernels to install eg: linux, linux-lts, linux-zen etc | Atleast 1 |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| keyboard-language | 2 letter code for your keyboard language | eg: us, de etc | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| mirror-region | {"<Region Name>": { "Mirror Name": True/False}, ..} | List of regions and mirrors to use | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| nic | { NetworkManager: <boolean>, nic: <nic name> } | | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| ntp | <boolean> | Set to true to set-up ntp post install | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| packages | [ "package1", "package2", ..] | List of packages to install post-installation | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| profile | Name of the profile to install | Profiles are present in profiles/, use the name of a profile to install it | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| !root-password | any | The root account password | No | | !root-password | any | The root account password | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| services | [ "service1", "service2", ..] | Services to enable post-installation | No | | !superusers | { "<username>": { "!password": "<password>"}, ..} | List of superuser credentials, see configuration for reference | Yes[1] |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| sys-encoding | "utf-8" | Set to change system encoding post-install, ignored if --advanced flag is not passed | No | | !users | { "<username>": { "!password": "<password>"}, ..} | List of regular user credentials, see configuration for reference | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+ +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| sys-language | "en_US" | Set to change system language post-install, ignored if --advanced flag is not passed | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| superusers | { "<username>": { "!password": "<password>"}, ..} | List of superuser credentials, see configuration for reference | Yes, if root account password is not provided |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| timezone | Timezone to configure in installation | Timezone eg: UTC, Asia/Kolkata etc. | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| users | { "<username>": { "!password": "<password>"}, ..} | List of regular user credentials, see configuration for reference | Yes, can be {} |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
(To see which keys are required, scroll to the right in the above table.)
Description individual steps
============================
Below is a description of each individual step in order.
keyboard languages
------------------
Default is :code:`us`.
| A short list of the most common layouts are presented.
| Entering :code:`?` and pressing enter enables a search mode where additional keyboard layouts can be found.
In search mode, you'll find things like:
* :code:`sv-latin1` for swedish layouts
Mirror region selection
-----------------------
Default is :code:`auto detect best mirror`
| Leaving this blank should enable the most appropriate mirror for you.
| But if you want to override and use only one selected region, you can enter one in this step.
As an example:
* :code:`Sweden` *(with a capital* :code:`S`) will only use mirrors from Sweden.
Selection of drive
------------------
There is no default for this step and it's a required step.
.. warning::
| The selected device will be wiped completely!
|
| Make sure you select a drive that will be used only by Arch Linux.
| *(Future versions of archinstall will support multiboot on the same drive and more complex partition setups)*
Select the appropriate drive by selecting it by number or full path.
Disk encryption
---------------
Selecting a disk encryption password enables disk encryption for the OS partition.
.. note:: .. note::
This step is highly recommended for most users, skipping this step comes with some risk and you are obligated to read up on why you would want to skip encryption before deciding to opt-out. [1] ``!superusers`` is optional only if ``!root-password`` was set. ``!superusers`` will be enforced otherwise and the minimum amount of superusers required will be set to 1.
.. warning:: Options for ``--disk_layouts``
This step does require at least 1GB of free RAM during boot in order to boot at all. Keep this in mind when creating virtual machines. It also only encrypts the OS partition - not the boot partition *(it's not full disk encryption)*. ------------------------------
Hostname
--------
Default is :code:`Archinstall`
The hostname in which the machine will identify itself on the local network.
This step is optional, but a default hostname of `Archinstall` will be set if none is selected.
.. _root_password:
Root password
-------------
.. warning::
| Setting a root password disables sudo permissions for additional users.
| It's there for **recommended to skip this step**!
This gives you the option to re-enable the :code:`root` account on the machine. By default, the :code:`root` account on Arch Linux is disabled and does not contain a password.
You are instead recommended to skip to the next step without any input.
Super User (sudo)
-----------------
.. warning::
This step only applies if you correctly skipped :ref:`the previous step <root_password>` which also makes this step mandatory.
If the previous step was skipped, and only if it is skipped.
This step enables you to create a :code:`sudo` enabled user with a password.
.. note:: .. note::
The sudo permission grants :code:`root`-like privileges to the account but is less prone to for instance guessing admin account attacks. You are also less likely to mess up system critical things by operating in normal user-mode and calling `sudo` to gain temporary administrative privileges. | The layout of ``--disk_layouts`` is a bit complicated.
| It's highly recommended that you generate it using ``--dry-run`` which will simulate an installation, without performing any damaging actions on your machine. *(no formatting is done)*
Pre-programmed profiles .. code-block:: json
-----------------------
You can optionally choose to install a pre-programmed profile. These profiles might make it easier for new users or beginners to achieve a traditional desktop environment as an example. {
"/dev/loop0": {
"partitions": [
{
"boot": true,
"encrypted": false,
"filesystem": {
"format": "fat32"
},
"format": true,
"mountpoint": "/boot",
"size": "513MB",
"start": "5MB",
"type": "primary"
},
{
"btrfs": {
"subvolumes": {
"@.snapshots": "/.snapshots",
"@home": "/home",
"@log": "/var/log",
"@pkgs": "/var/cache/pacman/pkg"
}
},
"encrypted": true,
"filesystem": {
"format": "btrfs"
},
"format": true,
"mountpoint": "/",
"size": "100%",
"start": "518MB",
"type": "primary"
}
],
"wipe": true
}
}
There is a list of profiles to choose from. If you are unsure of what any of these are, research the names that show up to understand what they are before you choose one. | The overall structure is that of ``{ "blockdevice-path" : ...}`` followed by options for that blockdevice.
| Each partition has it's own settings, and the formatting is executed in order *(top to bottom in the above example)*.
| Mountpoints is later mounted in order of path traversal, ``/`` before ``/home`` etc.
.. note:: +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| Some profiles might have sub-dependencies that will ask you to select additional profiles. | Key | Values | Description | Required |
| For instance the :code:`desktop` profile will create a secondary menu to select a graphical driver. That graphical driver might have additional dependencies if there are multiple driver vendors. | | | | |
| +======================+=====================================================+======================================================================================+===============================================+
| Simply follow the instructions on the screen to navigate through them. | filesystem | { "format": "ext4 / btrfs / fat32 etc." } | Filesystem for root and other partitions | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
Additional packages | boot | <bool> | Marks the partition as bootable | No |
------------------- +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| encrypted | <bool> | Mark the partition for encryption | No |
Some additional packages can be installed if need be. This step allows you to list *(space separated)* officially supported packages from the package database at `https://archlinux.org/packages/ <https://archlinux.org/packages/>`_. +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| mountpoint | /path | Relative to the inside of the installation, where should the partition be mounted | Yes |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
Network configuration | start | <size><B, MiB, GiB, %, etc> | The start position of the partition | Yes |
--------------------- +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| type | primary | Only used if MBR and BIOS is used. Marks what kind of partition it is. | No |
| This step is optional and allows for some basic configuration of your network. +----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
| There are two main options and two sub-options, the two main ones are: | btrfs | { "subvolumes": {"subvolume": "mountpoint"}} | Support for btrfs subvolumes for a given partition | No |
+----------------------+-----------------------------------------------------+--------------------------------------------------------------------------------------+-----------------------------------------------+
* Copy existing network configuration from the ISO you're working on
* Select **one** network interface to configure
| If copying the existing configuration is chosen, no further configuration is needed.
| The installer will copy any wireless *(based on* :code:`iwd`) configurations and :code:`systemd-networkd` configuration set up by the user or the default system configuration.
| If an interface was selected instead, a secondary option will be presented, allowing you to choose between two options:
* Automatic DHCP configuration of IP, DNS and Gateway
* Static IP configuration that will ask some further mandatory questions
Configuration verification
--------------------------
| Before the installer continues, and this is only valid for the **guided installation**, the chosen configuration will be printed on the screen and you have the option to verify it.
After which you can press :code:`Enter` in order to start the formatting and installation process.
.. warning::
After a 5 second countdown, the selected drive will be permanently erased and all data will be lost.
Post installation
-----------------
Once the installation is complete, green text should appear saying that it's safe to `reboot`, which is also the command you use to reboot.

View File

@ -7,50 +7,38 @@ Archinstall ships on `PyPi <https://pypi.org/>`_ as `archinstall <pypi.org/proje
But the library can be installed manually as well. But the library can be installed manually as well.
.. warning:: .. warning::
This is not required if you're running archinstall on a pre-built ISO. The installation is only required if you're creating your own scripted installations. These steps are not required if you want to use archinstall on the official Arch Linux ISO.
Using pacman Installing with pacman
------------ ----------------------
Archinstall is on the `official repositories <https://wiki.archlinux.org/index.php/Official_repositories>`_. Archinstall is on the `official repositories <https://wiki.archlinux.org/index.php/Official_repositories>`_.
And it will also install archinstall as a python library.
To install both the library and the archinstall script: To install both the library and the archinstall script:
.. code-block:: console .. code-block:: console
sudo pacman -S archinstall pacman -S archinstall
Or, to install just the library: Alternatively, you can install only the library and not the helper executable using the ``python-archinstall`` package.
.. code-block:: console Installing with PyPi
--------------------
sudo pacman -S python-archinstall
Using PyPi
----------
The basic concept of PyPi applies using `pip`. The basic concept of PyPi applies using `pip`.
Either as a global library:
.. code-block:: console .. code-block:: console
sudo pip install archinstall pip install archinstall
Or as a user module:
.. code-block:: console
pip --user install archinstall
Which will allow you to start using the library.
.. _installing.python.manual: .. _installing.python.manual:
Manual installation Install using source code
------------------- -------------------------
You can either download the github repo as a zip archive, or you can clone it. | You can also install using the source code.
We'll clone it here but both methods work the same. | For sake of simplicity we will use ``git clone`` in this example.
.. code-block:: console .. code-block:: console

View File

@ -57,133 +57,65 @@ def ask_user_questions():
Not until we're satisfied with what we want to install Not until we're satisfied with what we want to install
will we continue with the actual installation steps. will we continue with the actual installation steps.
""" """
if not archinstall.arguments.get('keyboard-layout', None):
archinstall.arguments['keyboard-layout'] = archinstall.select_language()
# Before continuing, set the preferred keyboard layout/language in the current terminal. global_menu = archinstall.GlobalMenu()
# This will just help the user with the next following questions. global_menu.enable('keyboard-layout')
if len(archinstall.arguments['keyboard-layout']):
archinstall.set_keyboard_language(archinstall.arguments['keyboard-layout']) if not archinstall.arguments.get('ntp', False):
archinstall.arguments['ntp'] = input("Would you like to use automatic time synchronization (NTP) with the default time servers? [Y/n]: ").strip().lower() in ('y', 'yes', '')
if archinstall.arguments['ntp']:
archinstall.log("Hardware time and other post-configuration steps might be required in order for NTP to work. For more information, please check the Arch wiki.", fg="yellow")
archinstall.SysCommand('timedatectl set-ntp true')
# Set which region to download packages from during the installation # Set which region to download packages from during the installation
if not archinstall.arguments.get('mirror-region', None): global_menu.enable('mirror-region')
archinstall.arguments['mirror-region'] = archinstall.select_mirror_regions()
if not archinstall.arguments.get('sys-language', None) and archinstall.arguments.get('advanced', False): if archinstall.arguments.get('advanced', False):
archinstall.arguments['sys-language'] = input("Enter a valid locale (language) for your OS, (Default: en_US): ").strip() global_menu.enable('sys-language', True)
archinstall.arguments['sys-encoding'] = input("Enter a valid system default encoding for your OS, (Default: utf-8): ").strip() global_menu.enable('sys-encoding', True)
archinstall.log("Keep in mind that if you want multiple locales, post configuration is required.", fg="yellow")
if not archinstall.arguments.get('sys-language', None):
archinstall.arguments['sys-language'] = 'en_US'
if not archinstall.arguments.get('sys-encoding', None):
archinstall.arguments['sys-encoding'] = 'utf-8'
# Ask which harddrives/block-devices we will install to # Ask which harddrives/block-devices we will install to
# and convert them into archinstall.BlockDevice() objects. # and convert them into archinstall.BlockDevice() objects.
if archinstall.arguments.get('harddrives', None) is None: global_menu.enable('harddrives')
archinstall.arguments['harddrives'] = archinstall.select_harddrives()
# we skip the customary .get('harddrives',None) 'cause we are pretty certain that at this point it contains at least none (behaviour has changed from previous version, where it had an empty list. Shouls be compatible with my code
if not archinstall.arguments['harddrives']:
archinstall.log("You decided to skip harddrive selection",fg="yellow",level=logging.INFO)
archinstall.log(f"and will use whatever drive-setup is mounted at {archinstall.storage['MOUNT_POINT']} (experimental)",fg="yellow",level=logging.INFO)
archinstall.log("WARNING: Archinstall won't check the suitability of this setup",fg="yellow",level=logging.INFO)
if input("Do you wish to continue ? [Y/n]").strip().lower() == 'n':
exit(1)
else:
if archinstall.storage.get('disk_layouts', None) is None:
archinstall.storage['disk_layouts'] = archinstall.select_disk_layout(archinstall.arguments['harddrives'], archinstall.arguments.get('advanced', False))
# Get disk encryption password (or skip if blank) global_menu.enable('disk_layouts')
if archinstall.arguments.get('!encryption-password', None) is None:
if passwd := archinstall.get_password(prompt='Enter disk encryption password (leave blank for no encryption): '):
archinstall.arguments['!encryption-password'] = passwd
if archinstall.arguments.get('!encryption-password', None): # Get disk encryption password (or skip if blank)
# If no partitions was marked as encrypted, but a password was supplied and we have some disks to format.. global_menu.enable('!encryption-password')
# Then we need to identify which partitions to encrypt. This will default to / (root).
if len(list(archinstall.encrypted_partitions(archinstall.storage['disk_layouts']))) == 0:
archinstall.storage['disk_layouts'] = archinstall.select_encrypted_partitions(archinstall.storage['disk_layouts'], archinstall.arguments['!encryption-password'])
# Ask which boot-loader to use (will only ask if we're in BIOS (non-efi) mode) # Ask which boot-loader to use (will only ask if we're in BIOS (non-efi) mode)
if not archinstall.arguments.get("bootloader", None): global_menu.enable('bootloader')
archinstall.arguments["bootloader"] = archinstall.ask_for_bootloader(archinstall.arguments.get('advanced', False))
if not archinstall.arguments.get('swap', None): global_menu.enable('swap')
archinstall.arguments['swap'] = archinstall.ask_for_swap()
# Get the hostname for the machine # Get the hostname for the machine
if not archinstall.arguments.get('hostname', None): global_menu.enable('hostname')
archinstall.arguments['hostname'] = input('Desired hostname for the installation: ').strip(' ')
# Ask for a root password (optional, but triggers requirement for super-user if skipped) # Ask for a root password (optional, but triggers requirement for super-user if skipped)
if not archinstall.arguments.get('!root-password', None): global_menu.enable('!root-password')
archinstall.arguments['!root-password'] = archinstall.get_password(prompt='Enter root password (leave blank to disable root & create superuser): ')
# Ask for additional users (super-user if root pw was not set) global_menu.enable('!superusers')
if not archinstall.arguments.get('!root-password', None) and not archinstall.arguments.get('!superusers', None): global_menu.enable('!users')
archinstall.arguments['!superusers'] = archinstall.ask_for_superuser_account('Create a required super-user with sudo privileges: ', forced=True)
if not archinstall.arguments.get('!users'):
users, superusers = archinstall.ask_for_additional_users('Enter a username to create an additional user (leave blank to skip & continue): ')
archinstall.arguments['!users'] = users
archinstall.arguments['!superusers'] = {**archinstall.arguments.get('!superusers', {}), **superusers}
# Ask for archinstall-specific profiles (such as desktop environments etc) # Ask for archinstall-specific profiles (such as desktop environments etc)
if not archinstall.arguments.get('profile', None): global_menu.enable('profile')
archinstall.arguments['profile'] = archinstall.select_profile()
# Check the potentially selected profiles preparations to get early checks if some additional questions are needed.
if archinstall.arguments['profile'] and archinstall.arguments['profile'].has_prep_function():
namespace = f"{archinstall.arguments['profile'].namespace}.py"
with archinstall.arguments['profile'].load_instructions(namespace=namespace) as imported:
if not imported._prep_function():
archinstall.log(' * Profile\'s preparation requirements was not fulfilled.', fg='red')
exit(1)
# Ask about audio server selection if one is not already set # Ask about audio server selection if one is not already set
if not archinstall.arguments.get('audio', None): global_menu.enable('audio')
# The argument to ask_for_audio_selection lets the library know if it's a desktop profile
archinstall.arguments['audio'] = archinstall.ask_for_audio_selection(archinstall.is_desktop_profile(archinstall.arguments['profile']))
# Ask for preferred kernel: # Ask for preferred kernel:
if not archinstall.arguments.get("kernels", None): global_menu.enable('kernels')
archinstall.arguments['kernels'] = archinstall.select_kernel()
# Additional packages (with some light weight error handling for invalid package names) global_menu.enable('packages')
print("Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.")
print("If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.")
while True:
if not archinstall.arguments.get('packages', None):
archinstall.arguments['packages'] = [package for package in input('Write additional packages to install (space separated, leave blank to skip): ').split(' ') if len(package)]
if len(archinstall.arguments['packages']):
# Verify packages that were given
try:
archinstall.log("Verifying that additional packages exist (this might take a few seconds)")
archinstall.validate_package_list(archinstall.arguments['packages'])
break
except archinstall.RequirementError as e:
archinstall.log(e, fg='red')
archinstall.arguments['packages'] = None # Clear the packages to trigger a new input question
else:
# no additional packages were selected, which we'll allow
break
# Ask or Call the helper function that asks the user to optionally configure a network. # Ask or Call the helper function that asks the user to optionally configure a network.
if not archinstall.arguments.get('nic', None): global_menu.enable('nic')
archinstall.arguments['nic'] = archinstall.ask_to_configure_network()
if not archinstall.arguments['nic']:
archinstall.log("No network configuration was selected. Network is going to be unavailable until configured manually!", fg="yellow")
if not archinstall.arguments.get('timezone', None): global_menu.enable('timezone')
archinstall.arguments['timezone'] = archinstall.ask_for_a_timezone()
if archinstall.arguments['timezone']: global_menu.enable('ntp')
if not archinstall.arguments.get('ntp', False):
archinstall.arguments['ntp'] = input("Would you like to use automatic time synchronization (NTP) with the default time servers? [Y/n]: ").strip().lower() in ('y', 'yes', '') global_menu.run()
if archinstall.arguments['ntp']:
archinstall.log("Hardware time and other post-configuration steps might be required in order for NTP to work. For more information, please check the Arch wiki.", fg="yellow")
def perform_filesystem_operations(): def perform_filesystem_operations():
@ -255,8 +187,8 @@ def perform_installation(mountpoint):
# Placing /boot check during installation because this will catch both re-use and wipe scenarios. # Placing /boot check during installation because this will catch both re-use and wipe scenarios.
for partition in installation.partitions: for partition in installation.partitions:
if partition.mountpoint == installation.target + '/boot': if partition.mountpoint == installation.target + '/boot':
if partition.size <= 0.25: # in GB if partition.size < 0.19: # ~200 MiB in GiB
raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 256MB and re-run the installation.") raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 200MiB and re-run the installation.")
# if len(mirrors): # if len(mirrors):
# Certain services might be running that affects the system during installation. # Certain services might be running that affects the system during installation.

View File

@ -4,7 +4,6 @@ import os
import pathlib import pathlib
import archinstall import archinstall
import glob
def load_mirror(): def load_mirror():
if archinstall.arguments.get('mirror-region', None) is not None: if archinstall.arguments.get('mirror-region', None) is not None:
@ -27,20 +26,6 @@ def load_harddrives():
archinstall.arguments['harddrives'] = [archinstall.BlockDevice(BlockDev) for BlockDev in archinstall.arguments['harddrives']] archinstall.arguments['harddrives'] = [archinstall.BlockDevice(BlockDev) for BlockDev in archinstall.arguments['harddrives']]
# Temporarily disabling keep_partitions if config file is loaded # Temporarily disabling keep_partitions if config file is loaded
def load_disk_layouts():
if archinstall.arguments.get('disk_layouts', None) is not None:
dl_path = pathlib.Path(archinstall.arguments['disk_layouts'])
if dl_path.exists(): # and str(dl_path).endswith('.json'):
try:
with open(dl_path) as fh:
archinstall.storage['disk_layouts'] = json.load(fh)
except Exception as e:
raise ValueError(f"--disk_layouts does not contain a valid JSON format: {e}")
else:
try:
archinstall.storage['disk_layouts'] = json.loads(archinstall.arguments['disk_layouts'])
except:
raise ValueError("--disk_layouts=<json> needs either a JSON file or a JSON string given with a valid disk layout.")
def ask_harddrives(): def ask_harddrives():
# Ask which harddrives/block-devices we will install to # Ask which harddrives/block-devices we will install to
@ -106,7 +91,6 @@ def load_config():
load_localization() load_localization()
load_gfxdriver() load_gfxdriver()
load_servers() load_servers()
load_disk_layouts()
def ask_user_questions(): def ask_user_questions():
""" """
@ -169,40 +153,6 @@ def perform_disk_operations():
with archinstall.Filesystem(drive, mode) as fs: with archinstall.Filesystem(drive, mode) as fs:
fs.load_layout(dl_disk) fs.load_layout(dl_disk)
def create_subvolume(installation_mountpoint, subvolume_location):
"""
This function uses btrfs to create a subvolume.
@installation: archinstall.Installer instance
@subvolume_location: a localized string or path inside the installation / or /boot for instance without specifying /mnt/boot
"""
if type(installation_mountpoint) == str:
installation_mountpoint_path = pathlib.Path(installation_mountpoint)
else:
installation_mountpoint_path = installation_mountpoint
# Set up the required physical structure
if type(subvolume_location) == str:
subvolume_location = pathlib.Path(subvolume_location)
target = installation_mountpoint_path / subvolume_location.relative_to(subvolume_location.anchor)
# Difference from mount_subvolume:
# We only check if the parent exists, since we'll run in to "target path already exists" otherwise
if not target.parent.exists():
target.parent.mkdir(parents=True)
if glob.glob(str(target / '*')):
raise archinstall.DiskError(f"Cannot create subvolume at {target} because it contains data (non-empty folder target)")
# Remove the target if it exists. It is nor incompatible to the previous
if target.exists():
target.rmdir()
archinstall.log(f"Creating a subvolume on {target}", level=logging.INFO)
if (cmd := archinstall.SysCommand(f"btrfs subvolume create {target}")).exit_code != 0:
raise archinstall.DiskError(f"Could not create a subvolume at {target}: {cmd}")
def perform_installation(mountpoint): def perform_installation(mountpoint):
""" """
Performs the installation steps on a block device. Performs the installation steps on a block device.
@ -220,6 +170,10 @@ def perform_installation(mountpoint):
if partition.mountpoint == installation.target + '/boot': if partition.mountpoint == installation.target + '/boot':
if partition.size <= 0.25: # in GB if partition.size <= 0.25: # in GB
raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 256MB and re-run the installation.") raise archinstall.DiskError(f"The selected /boot partition in use is not large enough to properly install a boot loader. Please resize it to at least 256MB and re-run the installation.")
# to generate a fstab directory holder. Avoids an error on exit and at the same time checks the procedure
target = pathlib.Path(f"{mountpoint}/etc/fstab")
if not target.parent.exists():
target.parent.mkdir(parents=True)
# 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)

View File

View File

@ -29,3 +29,11 @@ exclude = ["docs/*.html", "docs/_static","docs/*.png","docs/*.psd"]
[tool.flit.metadata.requires-extra] [tool.flit.metadata.requires-extra]
doc = ["sphinx"] doc = ["sphinx"]
[tool.mypy]
python_version = "3.10"
exclude = "tests"
[tool.bandit]
targets = ["ourkvm"]
exclude = ["/tests"]

164
schema.json Normal file
View File

@ -0,0 +1,164 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Archinstall Config",
"description": "A schema for the archinstall command config, more info over at https://archinstall.readthedocs.io/installing/guided.html#options-for-config",
"type": "object",
"properties": {
"audio": {
"description": "Audioserver to be installed",
"type": "string",
"enum": [
"pipewire",
"pulseaudio",
"none"
]
},
"bootloader": {
"description": "Bootloader to be installed",
"type": "string",
"enum": [
"systemd-bootctl",
"grub-install",
"efistub"
]
},
"custom-commands": {
"description": "Custom commands to be run post install",
"type": "array",
"items": {
"type": "string"
}
},
"gfx_driver": {
"description": "Graphics Drivers to install if a desktop profile is used, ignored otherwise.",
"type": "string",
"enum": [
"VMware / VirtualBox (open-source)",
"Nvidia",
"Intel (open-source)",
"AMD / ATI (open-source)",
"All open-source (default)"
]
},
"harddrives": {
"description": "Path of device to be used",
"type": "array",
"items": {
"type": "string"
}
},
"hostname": {
"description": "Hostname of machine after installation",
"type": "string"
},
"kernels": {
"description": "List of kernels to install eg: linux, linux-lts, linux-zen etc",
"type": "array",
"items": {
"type": "string",
"enum": [
"linux",
"linux-lts",
"linux-zen",
"linux-hardened"
]
}
},
"keyboard-language": {
"description": "eg: us, de, de-latin1 etc",
"type": "string"
},
"mirror-region": {
"description": "By default, it will autodetect the best region. Enter a region or a dictionary of regions and mirrors to use specific ones",
"type": "object"
},
"nic": {
"description": "Choose between NetworkManager, manual configuration, use systemd-networkd from the ISO or no configuration",
"type": "object",
"properties": {
"NetworkManager": {
"description": "<boolean>",
"type": "boolean"
},
"interface-name": {
"address": "ip address",
"subnet": "255.255.255.0",
"gateway": "ip address",
"dns": "ip address"
},
"nic": "Copy ISO network configuration to installation",
"nic": {}
}
},
"ntp": {
"description": "Set to true to set-up ntp post install",
"type": "boolean"
},
"packages": {
"description": "List of packages to install post-installation",
"type": "array",
"items": {
"type": "string"
}
},
"profile": {
"description": "Profiles are present in profiles/, use the name of a profile to install it",
"type": "string",
"enum": [
"awesome",
"budgie",
"cinnamon",
"cutefish",
"deepin",
"desktop",
"enlightenment",
"gnome",
"i3",
"kde",
"lxqt",
"mate",
"minimal",
"server",
"sway",
"xfce4",
"xorg"
]
},
"services": {
"description": "Services to enable post-installation",
"type": "array",
"items": {
"type": "string"
}
},
"sys-encoding": {
"description": "Set to change system encoding post-install, ignored if --advanced flag is not passed",
"type": "string"
},
"sys-language": {
"description": "Set to change system language post-install, ignored if --advanced flag is not passed",
"type": "string"
},
"timezone": {
"description": "Timezone eg: UTC, Asia/Kolkata etc.",
"type": "string"
}
},
"required": [
"bootloader",
"kernels",
"mirror-region",
],
"anyOf": [
{
"required": [
"!root-password"
]
},
{
"required": [
"!superusers"
]
}
]
}