Files
linux-image-manager/lim/cli.py
Kevin Veen-Birkenbach ccdef065df refactor!: port shell scripts to Python package
Bash scripts were untestable and duplicated device/LUKS/mount logic;
the lim/ package centralizes it behind one subprocess wrapper and a
YAML image catalog (single point of truth).

BREAKING CHANGE: scripts/*.sh removed. Use `lim --type <cmd>`; new
types mount/umount/single-boot/raid1-boot/lock/unlock/import/export
replace direct script calls. --extra is deprecated and ignored.

- distributions.yml + lim/catalog.py hold the image catalog (PyYAML)
- pytest suite: 102 tests with mocked subprocess (tests/unit) and a
  250-line max file-length guard (tests/lint)
- ruff strict (select ALL), GitHub Actions CI, Dependabot; Travis gone
- Makefile: install (symlink ~/.local/bin/lim) and test targets
- fixes over bash: SUDO_USER-aware chown, mmcblk/nvme partition paths,
  sha512 checksum support, whole-pipeline failure detection, blkid
  UUID fallback for pre-mounted images, conditional fstab seeding for
  PARTUUID/LABEL images, clean errors for missing binaries
2026-07-14 11:37:19 +02:00

168 lines
5.0 KiB
Python

"""Command line interface: `lim --type <command>`."""
import argparse
import sys
from collections.abc import Callable
from dataclasses import dataclass
from lim import system, ui
from lim.data import crypt, sync
from lim.errors import LimError
from lim.image import backup, chroot
from lim.image import setup as image_setup
from lim.storage import raid1, single_drive
@dataclass(frozen=True)
class Command:
func: Callable[[], None]
description: str
needs_root: bool
COMMANDS: dict[str, Command] = {
"image": Command(
image_setup.run_setup,
"Linux Image Setup:\n"
" - Creates partitions and formats them.\n"
" - Transfers the Linux image file to the device.\n"
" - Configures boot and root partitions.",
needs_root=True,
),
"single": Command(
single_drive.setup,
"Single Drive Encryption Setup:\n"
" - Sets up disk encryption using LUKS on one drive.\n"
" - Configures a Btrfs file system for secure storage.",
needs_root=True,
),
"raid1": Command(
raid1.setup,
"RAID1 Encryption Setup:\n"
" - Configures a virtual RAID1 with two drives.\n"
" - Uses LUKS encryption and a Btrfs RAID1 file system for redundancy.",
needs_root=True,
),
"backup": Command(
backup.run_backup,
"Backup Image Setup:\n"
" - Creates an image backup from a memory device to a file.\n"
" - Uses dd to transfer the image from the specified device to an image file.",
needs_root=True,
),
"chroot": Command(
chroot.run_chroot,
"Chroot Environment Setup:\n"
" - Mounts partitions and configures the chroot environment for a Linux image.\n"
" - Provides a shell within the Linux image for system maintenance.",
needs_root=True,
),
"mount": Command(
single_drive.mount,
"Mount Encrypted Storage:\n"
" - Unlocks a LUKS partition and mounts it.",
needs_root=True,
),
"umount": Command(
single_drive.umount,
"Unmount Encrypted Storage:\n"
" - Unmounts a LUKS partition and closes the mapper.",
needs_root=True,
),
"single-boot": Command(
single_drive.mount_on_boot,
"Single Drive Automount Setup:\n"
" - Creates a LUKS keyfile and registers it in crypttab and fstab.",
needs_root=True,
),
"raid1-boot": Command(
raid1.mount_on_boot,
"RAID1 Automount Setup:\n"
" - Creates LUKS keyfiles for both drives and registers them in crypttab/fstab.",
needs_root=True,
),
"unlock": Command(
crypt.unlock,
"Unlock Data:\n"
" - Decrypts the encfs data store into the decrypted folder.",
needs_root=False,
),
"lock": Command(
crypt.lock,
"Lock Data:\n"
" - Unmounts the decrypted encfs folder.",
needs_root=False,
),
"import": Command(
sync.import_from_system,
"Import Data:\n"
" - Copies personal data from the system into the encrypted store.",
needs_root=False,
),
"export": Command(
sync.export_to_system,
"Export Data:\n"
" - Copies personal data from the encrypted store back to the system.",
needs_root=False,
),
}
def build_parser() -> argparse.ArgumentParser:
epilog_lines = ["Available script types:"]
epilog_lines += [
f" {name:<12} - {command.description.splitlines()[0].rstrip(':')}"
for name, command in COMMANDS.items()
]
parser = argparse.ArgumentParser(
prog="lim",
description="Linux Image Manager — manages Linux images and encrypted storage.",
epilog="\n".join(epilog_lines),
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"--type",
required=True,
choices=list(COMMANDS.keys()),
help="Select the command to execute.",
)
parser.add_argument(
"--auto-confirm",
action="store_true",
help="Automatically confirm execution without prompting the user.",
)
parser.add_argument(
"--extra",
nargs=argparse.REMAINDER,
default=[],
help="Deprecated, ignored. Former extra parameters for the shell scripts.",
)
return parser
def main(argv: list[str] | None = None) -> None:
args = build_parser().parse_args(argv)
command = COMMANDS[args.type]
if args.extra:
ui.warning("--extra is deprecated and ignored.")
if command.needs_root:
system.ensure_root()
ui.header()
ui.info(f"Selected command: {args.type}")
ui.info("Description:")
print(command.description)
try:
if not args.auto_confirm:
input("Press Enter to execute or Ctrl+C to cancel...")
command.func()
except LimError as exc:
ui.error(f"{exc} -> Leaving program.")
sys.exit(1)
except KeyboardInterrupt:
print()
ui.error("Execution aborted by user.")
sys.exit(1)