linux-image-manager 2.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lim/__init__.py +3 -0
- lim/catalog.py +31 -0
- lim/cli.py +167 -0
- lim/config.py +18 -0
- lim/configuration/packages/general.txt +6 -0
- lim/configuration/packages/server/luks.txt +8 -0
- lim/data/__init__.py +0 -0
- lim/data/crypt.py +24 -0
- lim/data/sync.py +135 -0
- lim/device.py +130 -0
- lim/distributions.yml +67 -0
- lim/errors.py +2 -0
- lim/fsutil.py +32 -0
- lim/image/__init__.py +0 -0
- lim/image/backup.py +32 -0
- lim/image/choosers.py +111 -0
- lim/image/chroot.py +23 -0
- lim/image/encryption.py +117 -0
- lim/image/plan.py +31 -0
- lim/image/raspberry.py +236 -0
- lim/image/session.py +190 -0
- lim/image/setup.py +96 -0
- lim/image/transfer.py +140 -0
- lim/image/verify.py +100 -0
- lim/luks.py +80 -0
- lim/packages.py +18 -0
- lim/runner.py +134 -0
- lim/storage/__init__.py +0 -0
- lim/storage/common.py +38 -0
- lim/storage/raid1.py +68 -0
- lim/storage/single_drive.py +88 -0
- lim/system.py +35 -0
- lim/ui.py +62 -0
- linux_image_manager-2.1.0.dist-info/METADATA +142 -0
- linux_image_manager-2.1.0.dist-info/RECORD +39 -0
- linux_image_manager-2.1.0.dist-info/WHEEL +5 -0
- linux_image_manager-2.1.0.dist-info/entry_points.txt +2 -0
- linux_image_manager-2.1.0.dist-info/licenses/LICENSE.txt +674 -0
- linux_image_manager-2.1.0.dist-info/top_level.txt +1 -0
lim/image/choosers.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Interactive selection of the distribution image to install."""
|
|
2
|
+
|
|
3
|
+
from lim import catalog, runner, ui
|
|
4
|
+
from lim.errors import LimError
|
|
5
|
+
from lim.image.plan import ImagePlan
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def choose_arch(plan: ImagePlan) -> None:
|
|
9
|
+
version = ui.ask("Which Raspberry Pi will be used (e.g.: 1, 2, 3b, 3b+, 4...):")
|
|
10
|
+
entry = catalog.arch_rpi_images().get(version)
|
|
11
|
+
if entry is None:
|
|
12
|
+
raise LimError(f"Version {version} isn't supported.")
|
|
13
|
+
plan.raspberry_pi_version = version
|
|
14
|
+
plan.boot_size = "+500M"
|
|
15
|
+
plan.base_download_url = "http://os.archlinuxarm.org/os/"
|
|
16
|
+
plan.image_name = entry["image"]
|
|
17
|
+
plan.luks_memory_cost = entry["luks_memory_cost"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def choose_manjaro(plan: ImagePlan) -> None:
|
|
21
|
+
plan.boot_size = "+500M"
|
|
22
|
+
flavour = ui.ask("Which version(e.g.:architect,gnome) should be used:")
|
|
23
|
+
if flavour == "architect":
|
|
24
|
+
plan.image_checksum = "6b1c2fce12f244c1e32212767a9d3af2cf8263b2"
|
|
25
|
+
plan.base_download_url = (
|
|
26
|
+
"https://osdn.net/frs/redir.php?m=dotsrc&f=%2Fstorage%2Fg%2Fm%2Fma"
|
|
27
|
+
"%2Fmanjaro%2Farchitect%2F20.0%2F"
|
|
28
|
+
)
|
|
29
|
+
plan.image_name = "manjaro-architect-20.0-200426-linux56.iso"
|
|
30
|
+
elif flavour == "gnome":
|
|
31
|
+
release = ui.ask("Which release(e.g.:20,21,raspberrypi) should be used:")
|
|
32
|
+
entry = catalog.manjaro_gnome_releases().get(release)
|
|
33
|
+
if entry is None:
|
|
34
|
+
raise LimError(f"Gnome release {release} isn't supported.")
|
|
35
|
+
plan.image_checksum = entry.get("checksum")
|
|
36
|
+
plan.base_download_url = entry["url"]
|
|
37
|
+
plan.image_name = entry["image"]
|
|
38
|
+
plan.luks_memory_cost = entry.get("luks_memory_cost")
|
|
39
|
+
plan.raspberry_pi_version = entry.get("raspberry_pi_version")
|
|
40
|
+
else:
|
|
41
|
+
raise LimError(f"Manjaro version {flavour} isn't supported.")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def choose_moode(plan: ImagePlan) -> None:
|
|
45
|
+
plan.boot_size = "+200M"
|
|
46
|
+
plan.image_checksum = "185cbc9a4994534bb7a4bc2744c78197"
|
|
47
|
+
plan.base_download_url = (
|
|
48
|
+
"https://github.com/moode-player/moode/releases/download/r651prod/"
|
|
49
|
+
)
|
|
50
|
+
plan.image_name = "moode-r651-iso.zip"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def choose_retropie(plan: ImagePlan) -> None:
|
|
54
|
+
plan.boot_size = "+500M"
|
|
55
|
+
version = ui.ask("Which version(e.g.:1,2,3,4) should be used:")
|
|
56
|
+
entry = catalog.retropie_images().get(version)
|
|
57
|
+
if entry is None:
|
|
58
|
+
raise LimError(f"Version {version} isn't supported.")
|
|
59
|
+
plan.raspberry_pi_version = version
|
|
60
|
+
plan.base_download_url = (
|
|
61
|
+
"https://github.com/RetroPie/RetroPie-Setup/releases/download/4.8/"
|
|
62
|
+
)
|
|
63
|
+
plan.image_checksum = entry["checksum"]
|
|
64
|
+
plan.image_name = entry["image"]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def choose_torbox(plan: ImagePlan) -> None:
|
|
68
|
+
plan.base_download_url = "https://www.torbox.ch/data/"
|
|
69
|
+
plan.image_name = "torbox-20220102-v050.gz"
|
|
70
|
+
plan.image_checksum = (
|
|
71
|
+
"0E1BA7FFD14AAAE5F0462C8293D95B62C3BF1D9E726E26977BD04772C55680D3"
|
|
72
|
+
)
|
|
73
|
+
plan.boot_size = "+200M"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def choose_android_x86(plan: ImagePlan) -> None:
|
|
77
|
+
plan.base_download_url = (
|
|
78
|
+
"https://www.fosshub.com/Android-x86.html?dwl=android-x86_64-9.0-r2.iso"
|
|
79
|
+
)
|
|
80
|
+
plan.image_name = "android-x86_64-9.0-r2.iso"
|
|
81
|
+
plan.image_checksum = (
|
|
82
|
+
"f7eb8fc56f29ad5432335dc054183acf086c539f3990f0b6e9ff58bd6df4604e"
|
|
83
|
+
)
|
|
84
|
+
plan.boot_size = "+500M"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
DISTRIBUTION_CHOOSERS = {
|
|
88
|
+
"android-x86": choose_android_x86,
|
|
89
|
+
"torbox": choose_torbox,
|
|
90
|
+
"arch": choose_arch,
|
|
91
|
+
"manjaro": choose_manjaro,
|
|
92
|
+
"moode": choose_moode,
|
|
93
|
+
"retropie": choose_retropie,
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def choose_linux_image(plan: ImagePlan) -> None:
|
|
98
|
+
distribution = ui.ask(
|
|
99
|
+
"Which distribution should be used [arch,moode,retropie,manjaro,torbox...]?"
|
|
100
|
+
)
|
|
101
|
+
chooser = DISTRIBUTION_CHOOSERS.get(distribution)
|
|
102
|
+
if chooser is None:
|
|
103
|
+
raise LimError(f"Distribution {distribution} isn't supported.")
|
|
104
|
+
plan.distribution = distribution
|
|
105
|
+
chooser(plan)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def choose_local_image(plan: ImagePlan) -> None:
|
|
109
|
+
ui.info("Available images:")
|
|
110
|
+
runner.run(["ls", "-l", str(plan.image_folder)], check=False)
|
|
111
|
+
plan.image_name = ui.ask("Which image would you like to use?")
|
lim/image/chroot.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Mount an image and open an interactive shell inside it."""
|
|
2
|
+
|
|
3
|
+
from lim import device as device_module
|
|
4
|
+
from lim import runner, ui
|
|
5
|
+
from lim.image.session import ImageSession
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_chroot() -> None:
|
|
9
|
+
ui.info("Starting chroot...")
|
|
10
|
+
device = device_module.select_device()
|
|
11
|
+
session = ImageSession(device)
|
|
12
|
+
try:
|
|
13
|
+
session.make_working_folder()
|
|
14
|
+
session.make_mount_folders()
|
|
15
|
+
session.decrypt_root()
|
|
16
|
+
session.mount_partitions()
|
|
17
|
+
session.mount_chroot_binds()
|
|
18
|
+
session.copy_qemu()
|
|
19
|
+
session.copy_resolv_conf()
|
|
20
|
+
ui.info("Bash shell starts...")
|
|
21
|
+
runner.run(["chroot", str(session.root_mount_path), "/bin/bash"], sudo=True)
|
|
22
|
+
finally:
|
|
23
|
+
session.destructor()
|
lim/image/encryption.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Remote-unlockable LUKS boot configuration (dropbear + mkinitcpio).
|
|
2
|
+
|
|
3
|
+
See https://gist.github.com/gea0/4fc2be0cb7a74d0e7cc4322aed710d38 and
|
|
4
|
+
https://gist.github.com/EnigmaCurry/2f9bed46073da8e38057fe78a61e7994
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from lim import catalog, fsutil, packages, runner, ui
|
|
10
|
+
from lim.image.plan import ImagePlan
|
|
11
|
+
from lim.image.session import ImageSession, chroot_bash, install_packages
|
|
12
|
+
|
|
13
|
+
MKINITCPIO_HOOKS_PREFIX = (
|
|
14
|
+
"base udev autodetect microcode modconf kms keyboard keymap consolefont block"
|
|
15
|
+
)
|
|
16
|
+
MKINITCPIO_HOOKS_SUFFIX = "filesystems fsck"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _configure_mkinitcpio(plan: ImagePlan, root: Path) -> None:
|
|
20
|
+
# Concerning mkinitcpio warnings, see
|
|
21
|
+
# https://gist.github.com/imrvelj/c65cd5ca7f5505a65e59204f5a3f7a6d
|
|
22
|
+
mkinitcpio_path = root / "etc/mkinitcpio.conf"
|
|
23
|
+
ui.info(f"Configuring {mkinitcpio_path}...")
|
|
24
|
+
additional_modules = catalog.mkinitcpio_modules_by_rpi().get(
|
|
25
|
+
plan.raspberry_pi_version
|
|
26
|
+
)
|
|
27
|
+
if additional_modules is None:
|
|
28
|
+
ui.warning(f"Version {plan.raspberry_pi_version} isn't supported.")
|
|
29
|
+
additional_modules = ""
|
|
30
|
+
modules = f"g_cdc usb_f_acm usb_f_ecm {additional_modules} g_ether".replace(" ", " ")
|
|
31
|
+
fsutil.replace_in_file("MODULES=()", f"MODULES=({modules})", mkinitcpio_path)
|
|
32
|
+
fsutil.replace_in_file(
|
|
33
|
+
"BINARIES=()", "BINARIES=(/usr/lib/libgcc_s.so.1)", mkinitcpio_path
|
|
34
|
+
)
|
|
35
|
+
fsutil.replace_in_file(
|
|
36
|
+
f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} {MKINITCPIO_HOOKS_SUFFIX})",
|
|
37
|
+
f"HOOKS=({MKINITCPIO_HOOKS_PREFIX} sleep netconf dropbear encryptssh "
|
|
38
|
+
f"{MKINITCPIO_HOOKS_SUFFIX})",
|
|
39
|
+
mkinitcpio_path,
|
|
40
|
+
)
|
|
41
|
+
ui.info(f"Content of {mkinitcpio_path}:{mkinitcpio_path.read_text()}")
|
|
42
|
+
ui.info("Generating mkinitcpio...")
|
|
43
|
+
chroot_bash(root, "mkinitcpio -vP")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _register_encrypted_root(plan: ImagePlan, session: ImageSession, root: Path) -> None:
|
|
47
|
+
fstab_path = root / "etc/fstab"
|
|
48
|
+
fstab_line = (
|
|
49
|
+
f"UUID={session.root_partition_uuid} / {plan.root_filesystem}"
|
|
50
|
+
" defaults,noatime 0 1"
|
|
51
|
+
)
|
|
52
|
+
ui.info(f"Configuring {fstab_path}...")
|
|
53
|
+
fsutil.ensure_line_in_file(fstab_line, fstab_path)
|
|
54
|
+
ui.info(f"Content of {fstab_path}:{fstab_path.read_text()}")
|
|
55
|
+
|
|
56
|
+
crypttab_path = root / "etc/crypttab"
|
|
57
|
+
crypttab_line = (
|
|
58
|
+
f"{session.root_mapper_name} UUID={session.root_partition_uuid} none luks"
|
|
59
|
+
)
|
|
60
|
+
ui.info(f"Configuring {crypttab_path}...")
|
|
61
|
+
fsutil.ensure_line_in_file(crypttab_line, crypttab_path)
|
|
62
|
+
ui.info(f"Content of {crypttab_path}:{crypttab_path.read_text()}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _configure_bootloader(plan: ImagePlan, session: ImageSession, root: Path) -> None:
|
|
66
|
+
cryptdevice = (
|
|
67
|
+
f"cryptdevice=UUID={session.root_partition_uuid}:{session.root_mapper_name} "
|
|
68
|
+
f"root={session.root_mapper_path}"
|
|
69
|
+
)
|
|
70
|
+
boot_txt_path = session.boot_mount_path / "boot.txt"
|
|
71
|
+
if boot_txt_path.is_file():
|
|
72
|
+
ui.info(f"Configuring {boot_txt_path}...")
|
|
73
|
+
hostname = (root / "etc/hostname").read_text().strip()
|
|
74
|
+
fsutil.replace_in_file(
|
|
75
|
+
"part uuid ${devtype} ${devnum}:2 uuid", "", boot_txt_path
|
|
76
|
+
)
|
|
77
|
+
fsutil.replace_in_file(
|
|
78
|
+
"setenv bootargs console=ttyS1,115200 console=tty0 root=PARTUUID=${uuid} "
|
|
79
|
+
'rw rootwait smsc95xx.macaddr="${usbethaddr}"',
|
|
80
|
+
f"setenv bootargs console=ttyS1,115200 console=tty0 "
|
|
81
|
+
f"ip=::::{hostname}:eth0:dhcp {cryptdevice} rw rootwait "
|
|
82
|
+
# Concerning issues with network adapter names, see
|
|
83
|
+
# https://forum.iobroker.net/topic/40542/raspberry-pi4-kein-eth0-mehr/16
|
|
84
|
+
'smsc95xx.macaddr="${usbethaddr}" net.ifnames=0 biosdevname=0',
|
|
85
|
+
boot_txt_path,
|
|
86
|
+
)
|
|
87
|
+
ui.info(f"Content of {boot_txt_path}:{boot_txt_path.read_text()}")
|
|
88
|
+
ui.info("Generating...")
|
|
89
|
+
chroot_bash(root, "cd /boot/ && ./mkscr || exit 1")
|
|
90
|
+
else:
|
|
91
|
+
cmdline_txt_path = session.boot_mount_path / "cmdline.txt"
|
|
92
|
+
ui.info(f"Configuring {cmdline_txt_path}...")
|
|
93
|
+
fsutil.replace_in_file(
|
|
94
|
+
"root=/dev/mmcblk0p2",
|
|
95
|
+
f"{cryptdevice} rootfstype={plan.root_filesystem}",
|
|
96
|
+
cmdline_txt_path,
|
|
97
|
+
)
|
|
98
|
+
ui.info(f"Content of {cmdline_txt_path}:{cmdline_txt_path.read_text()}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def configure_encryption(
|
|
102
|
+
plan: ImagePlan, session: ImageSession, authorized_keys: Path
|
|
103
|
+
) -> None:
|
|
104
|
+
root = session.root_mount_path
|
|
105
|
+
ui.info("Setup encryption...")
|
|
106
|
+
ui.info("Installing neccessary software...")
|
|
107
|
+
install_packages(
|
|
108
|
+
plan.distribution, root, " ".join(packages.get_packages("server/luks"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
dropbear_root_key_path = root / "etc/dropbear/root_key"
|
|
112
|
+
ui.info(f"Adding {authorized_keys} to dropbear...")
|
|
113
|
+
runner.run(["cp", "-v", str(authorized_keys), str(dropbear_root_key_path)], sudo=True)
|
|
114
|
+
|
|
115
|
+
_configure_mkinitcpio(plan, root)
|
|
116
|
+
_register_encrypted_root(plan, session, root)
|
|
117
|
+
_configure_bootloader(plan, session, root)
|
lim/image/plan.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""The image setup plan collected from the user's answers."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
DEFAULT_BOOT_SIZE = "+500M"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class ImagePlan:
|
|
11
|
+
operation_system: str = "linux"
|
|
12
|
+
distribution: str | None = None
|
|
13
|
+
base_download_url: str | None = None
|
|
14
|
+
image_name: str | None = None
|
|
15
|
+
image_checksum: str | None = None
|
|
16
|
+
boot_size: str = ""
|
|
17
|
+
luks_memory_cost: str | None = None
|
|
18
|
+
raspberry_pi_version: str | None = None
|
|
19
|
+
encrypt_system: bool = False
|
|
20
|
+
root_filesystem: str = ""
|
|
21
|
+
image_folder: Path = field(default_factory=Path)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def download_url(self) -> str | None:
|
|
25
|
+
if self.base_download_url is None or self.image_name is None:
|
|
26
|
+
return None
|
|
27
|
+
return f"{self.base_download_url}{self.image_name}"
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def image_path(self) -> Path:
|
|
31
|
+
return self.image_folder / self.image_name
|
lim/image/raspberry.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Raspberry-Pi-specific configuration of a freshly transferred image."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from lim import device as device_module
|
|
6
|
+
from lim import fsutil, runner, ui
|
|
7
|
+
from lim.errors import LimError
|
|
8
|
+
from lim.image.encryption import configure_encryption
|
|
9
|
+
from lim.image.plan import ImagePlan
|
|
10
|
+
from lim.image.session import ImageSession, chroot_bash, install_packages
|
|
11
|
+
|
|
12
|
+
ADMINISTRATOR_USERNAME = "administrator"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def configure_sudoers(root_mount_path: Path, username: str) -> None:
|
|
16
|
+
sudo_config_dir = root_mount_path / "etc/sudoers.d"
|
|
17
|
+
sudo_config_file = sudo_config_dir / username
|
|
18
|
+
sudo_config_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
try:
|
|
20
|
+
sudo_config_file.write_text(f"{username} ALL=(ALL:ALL) ALL\n")
|
|
21
|
+
sudo_config_file.chmod(0o440)
|
|
22
|
+
except OSError as exc:
|
|
23
|
+
raise LimError(f"Failed to create sudoers file for {username}: {exc}") from exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def configure_ssh_key(
|
|
27
|
+
public_key_path: str, target_ssh_folder: Path, authorized_keys: Path
|
|
28
|
+
) -> None:
|
|
29
|
+
source = Path(public_key_path)
|
|
30
|
+
if not source.is_file():
|
|
31
|
+
raise LimError(
|
|
32
|
+
f'The ssh key "{public_key_path}" can\'t be copied to '
|
|
33
|
+
f'"{authorized_keys}" because it doesn\'t exist.'
|
|
34
|
+
)
|
|
35
|
+
ui.info("Copy ssh key to target...")
|
|
36
|
+
target_ssh_folder.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
authorized_keys.write_text(source.read_text())
|
|
38
|
+
ui.info(f"{authorized_keys} contains the following: {authorized_keys.read_text()}")
|
|
39
|
+
ui.info("Set permissions with chmod...")
|
|
40
|
+
target_ssh_folder.chmod(0o700)
|
|
41
|
+
authorized_keys.chmod(0o600)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _ensure_image_mounted(session: ImageSession) -> None:
|
|
45
|
+
ui.info("Start regular mounting procedure...")
|
|
46
|
+
if device_module.is_mounted(session.boot_partition_path):
|
|
47
|
+
ui.info(f"{session.boot_partition_path} is allready mounted...")
|
|
48
|
+
elif device_module.is_mounted(session.root_mapper_path):
|
|
49
|
+
ui.info(f"{session.root_mapper_path} is allready mounted...")
|
|
50
|
+
else:
|
|
51
|
+
session.decrypt_root()
|
|
52
|
+
session.mount_partitions()
|
|
53
|
+
if session.boot_partition_uuid is None:
|
|
54
|
+
# Partitions were mounted by an earlier run; fetch what mount_partitions
|
|
55
|
+
# would have provided so fstab/crypttab never see a None UUID.
|
|
56
|
+
session.read_partition_uuids()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _seed_boot_uuid(session: ImageSession) -> None:
|
|
60
|
+
fstab_path = session.root_mount_path / "etc/fstab"
|
|
61
|
+
if "/dev/mmcblk0p1" not in fstab_path.read_text():
|
|
62
|
+
# PARTUUID=/LABEL= based images (e.g. RetroPie, Manjaro ARM) have
|
|
63
|
+
# nothing to seed — that is a valid state, not an error.
|
|
64
|
+
ui.info(f"{fstab_path} does not reference /dev/mmcblk0p1. Skipping UUID seeding.")
|
|
65
|
+
return
|
|
66
|
+
ui.info(f"Seeding UUID to {fstab_path} to avoid path conflicts...")
|
|
67
|
+
fsutil.replace_in_file(
|
|
68
|
+
"/dev/mmcblk0p1", f"UUID={session.boot_partition_uuid}", fstab_path
|
|
69
|
+
)
|
|
70
|
+
ui.info(f"Content of {fstab_path}:{fstab_path.read_text()}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _select_target_user(root: Path) -> tuple[str, str]:
|
|
74
|
+
"""Return (default username, target username), renaming the home on request."""
|
|
75
|
+
target_home_path = root / "home"
|
|
76
|
+
home_entries = sorted(path.name for path in target_home_path.iterdir())
|
|
77
|
+
if not home_entries:
|
|
78
|
+
raise LimError(f"No home directory found under {target_home_path}.")
|
|
79
|
+
default_username = home_entries[0]
|
|
80
|
+
rename = default_username != ADMINISTRATOR_USERNAME and ui.confirm(
|
|
81
|
+
f"Should the {default_username} be renamed to {ADMINISTRATOR_USERNAME}? "
|
|
82
|
+
)
|
|
83
|
+
if not rename:
|
|
84
|
+
return default_username, default_username
|
|
85
|
+
ui.info(
|
|
86
|
+
f"Rename home directory from {target_home_path / default_username} "
|
|
87
|
+
f"to {target_home_path / ADMINISTRATOR_USERNAME}..."
|
|
88
|
+
)
|
|
89
|
+
runner.run(
|
|
90
|
+
[
|
|
91
|
+
"mv",
|
|
92
|
+
"-v",
|
|
93
|
+
str(target_home_path / default_username),
|
|
94
|
+
str(target_home_path / ADMINISTRATOR_USERNAME),
|
|
95
|
+
],
|
|
96
|
+
sudo=True,
|
|
97
|
+
error_msg="Failed to rename home directory",
|
|
98
|
+
)
|
|
99
|
+
return default_username, ADMINISTRATOR_USERNAME
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _change_passwords(root: Path, target_username: str) -> None:
|
|
103
|
+
password = ui.ask(
|
|
104
|
+
f"Type in new password for user root and {target_username} (leave empty to skip): "
|
|
105
|
+
)
|
|
106
|
+
if not password:
|
|
107
|
+
ui.info("No password change requested, skipped password change...")
|
|
108
|
+
return
|
|
109
|
+
repeated = ui.ask(f'Repeat new password for "{target_username}": ')
|
|
110
|
+
if password != repeated:
|
|
111
|
+
raise LimError("Passwords didn't match.")
|
|
112
|
+
ui.info("Changing passwords on target system...")
|
|
113
|
+
chroot_bash(
|
|
114
|
+
root,
|
|
115
|
+
f"( echo '{password}'; echo '{password}' ) | passwd {target_username}\n"
|
|
116
|
+
f"( echo '{password}'; echo '{password}' ) | passwd\n",
|
|
117
|
+
error_msg="Failed to change password.",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _configure_hostname(root: Path) -> None:
|
|
122
|
+
hostname_path = root / "etc/hostname"
|
|
123
|
+
hostname = ui.ask("Type in the hostname (leave empty to skip): ")
|
|
124
|
+
if hostname:
|
|
125
|
+
hostname_path.write_text(hostname + "\n")
|
|
126
|
+
else:
|
|
127
|
+
hostname = hostname_path.read_text().strip()
|
|
128
|
+
ui.info("No hostname change requested, skipped hostname change...")
|
|
129
|
+
ui.info(f"Used hostname is: {hostname}")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _update_system(plan: ImagePlan, root: Path) -> None:
|
|
133
|
+
if not ui.confirm("Should the system be updated?"):
|
|
134
|
+
return
|
|
135
|
+
ui.info("Updating system...")
|
|
136
|
+
if plan.distribution in ("arch", "manjaro"):
|
|
137
|
+
chroot_bash(root, "pacman --noconfirm -Syyu")
|
|
138
|
+
elif plan.distribution in ("moode", "retropie"):
|
|
139
|
+
chroot_bash(root, "yes | apt update\nyes | apt upgrade\n")
|
|
140
|
+
else:
|
|
141
|
+
ui.warning(
|
|
142
|
+
f'System update for operation system "{plan.distribution}" '
|
|
143
|
+
"is not supported yet. Skipped."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _run_retropie_procedures(session: ImageSession, public_key_path: str) -> None:
|
|
148
|
+
if public_key_path:
|
|
149
|
+
(session.boot_mount_path / "ssh").write_text("\n")
|
|
150
|
+
if not ui.confirm("Should the RetroFlag specific procedures be executed?"):
|
|
151
|
+
return
|
|
152
|
+
ui.info("Executing RetroFlag specific procedures...")
|
|
153
|
+
chroot_bash(
|
|
154
|
+
session.root_mount_path,
|
|
155
|
+
'wget -O - "https://raw.githubusercontent.com/RetroFlag/'
|
|
156
|
+
'retroflag-picase/master/install_gpi.sh" | bash\n',
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _configure_users_and_keys(session: ImageSession) -> tuple[str, Path]:
|
|
161
|
+
"""Handle user rename, sudo rights and SSH key; return (public key path, authorized_keys)."""
|
|
162
|
+
root = session.root_mount_path
|
|
163
|
+
ui.info("Define target paths...")
|
|
164
|
+
default_username, target_username = _select_target_user(root)
|
|
165
|
+
renamed = default_username != target_username
|
|
166
|
+
target_user_ssh_folder = root / "home" / target_username / ".ssh"
|
|
167
|
+
target_authorized_keys = target_user_ssh_folder / "authorized_keys"
|
|
168
|
+
|
|
169
|
+
if ui.confirm(f"Should the {target_username} have sudo rights? "):
|
|
170
|
+
configure_sudoers(root, target_username)
|
|
171
|
+
|
|
172
|
+
public_key_path = ui.ask(
|
|
173
|
+
"Enter the path to the SSH key to be added to the image (default: none):"
|
|
174
|
+
)
|
|
175
|
+
if public_key_path:
|
|
176
|
+
configure_ssh_key(public_key_path, target_user_ssh_folder, target_authorized_keys)
|
|
177
|
+
else:
|
|
178
|
+
ui.info("Skipped SSH-key copying..")
|
|
179
|
+
|
|
180
|
+
ui.info("Start chroot procedures...")
|
|
181
|
+
session.mount_chroot_binds()
|
|
182
|
+
session.copy_qemu()
|
|
183
|
+
session.copy_resolv_conf()
|
|
184
|
+
|
|
185
|
+
chroot_user_home = f"/home/{target_username}/"
|
|
186
|
+
if renamed:
|
|
187
|
+
ui.info("Delete old user and create new user")
|
|
188
|
+
chroot_bash(
|
|
189
|
+
root,
|
|
190
|
+
f"userdel -r {default_username}\n"
|
|
191
|
+
f"useradd -m -d {chroot_user_home} -s /bin/bash {target_username}\n"
|
|
192
|
+
f"chown -R {target_username}:{target_username} {chroot_user_home}\n",
|
|
193
|
+
error_msg="Failed to delete old user and create new user",
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if public_key_path:
|
|
197
|
+
ui.info("Chroot to set ownership...")
|
|
198
|
+
chroot_bash(
|
|
199
|
+
root,
|
|
200
|
+
f"chown -vR {target_username}:{target_username} {chroot_user_home}.ssh\n",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
_change_passwords(root, target_username)
|
|
204
|
+
return public_key_path, target_authorized_keys
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def configure_raspberry_image(plan: ImagePlan, session: ImageSession) -> None:
|
|
208
|
+
_ensure_image_mounted(session)
|
|
209
|
+
root = session.root_mount_path
|
|
210
|
+
_seed_boot_uuid(session)
|
|
211
|
+
|
|
212
|
+
public_key_path, target_authorized_keys = _configure_users_and_keys(session)
|
|
213
|
+
|
|
214
|
+
_configure_hostname(root)
|
|
215
|
+
|
|
216
|
+
if plan.distribution in ("arch", "manjaro"):
|
|
217
|
+
ui.info("Populating keys...")
|
|
218
|
+
chroot_bash(
|
|
219
|
+
root,
|
|
220
|
+
"yes | pacman-key --init\nyes | pacman-key --populate archlinuxarm\n",
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
_update_system(plan, root)
|
|
224
|
+
|
|
225
|
+
ui.info(f"Installing software for filesystem {plan.root_filesystem}...")
|
|
226
|
+
if plan.root_filesystem == "btrfs":
|
|
227
|
+
install_packages(plan.distribution, root, "btrfs-progs")
|
|
228
|
+
else:
|
|
229
|
+
ui.info("Skipped.")
|
|
230
|
+
|
|
231
|
+
if plan.encrypt_system:
|
|
232
|
+
configure_encryption(plan, session, target_authorized_keys)
|
|
233
|
+
|
|
234
|
+
ui.info("Running system specific procedures...")
|
|
235
|
+
if plan.distribution == "retropie":
|
|
236
|
+
_run_retropie_procedures(session, public_key_path)
|