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/runner.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Thin wrapper around subprocess for all external commands.
|
|
2
|
+
|
|
3
|
+
Every module calls external tools through this module so tests can
|
|
4
|
+
replace ``run``/``output``/``succeeds``/``pipeline`` with fakes.
|
|
5
|
+
|
|
6
|
+
Missing binaries never surface as tracebacks: actions (``check=True``,
|
|
7
|
+
``pipeline``) abort with a clear LimError, tolerated calls
|
|
8
|
+
(``check=False``, ``succeeds``) degrade to a warning plus shell-style
|
|
9
|
+
exit code 127.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
from lim import ui
|
|
17
|
+
from lim.errors import LimError
|
|
18
|
+
|
|
19
|
+
COMMAND_NOT_FOUND = 127
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _with_sudo(cmd: list[str], *, sudo: bool) -> list[str]:
|
|
23
|
+
parts = [str(part) for part in cmd]
|
|
24
|
+
if sudo and os.geteuid() != 0:
|
|
25
|
+
return ["sudo", *parts]
|
|
26
|
+
return parts
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _not_found_message(cmd: list[str]) -> str:
|
|
30
|
+
return f"Command not found: {cmd[0]} โ please install it."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def run(
|
|
34
|
+
cmd: list[str],
|
|
35
|
+
*,
|
|
36
|
+
sudo: bool = False,
|
|
37
|
+
input_text: str | None = None,
|
|
38
|
+
check: bool = True,
|
|
39
|
+
error_msg: str | None = None,
|
|
40
|
+
) -> subprocess.CompletedProcess:
|
|
41
|
+
"""Run a command inheriting stdio so progress output stays visible."""
|
|
42
|
+
cmd = _with_sudo(cmd, sudo=sudo)
|
|
43
|
+
ui.info(f"Running: {shlex.join(cmd)}")
|
|
44
|
+
try:
|
|
45
|
+
result = subprocess.run(cmd, input=input_text, text=True, check=False)
|
|
46
|
+
except FileNotFoundError:
|
|
47
|
+
if check:
|
|
48
|
+
raise LimError(error_msg or _not_found_message(cmd)) from None
|
|
49
|
+
ui.warning(_not_found_message(cmd))
|
|
50
|
+
return subprocess.CompletedProcess(cmd, COMMAND_NOT_FOUND)
|
|
51
|
+
if check and result.returncode != 0:
|
|
52
|
+
raise LimError(
|
|
53
|
+
error_msg
|
|
54
|
+
or f"Command failed with code {result.returncode}: {shlex.join(cmd)}"
|
|
55
|
+
)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def output(
|
|
60
|
+
cmd: list[str],
|
|
61
|
+
*,
|
|
62
|
+
sudo: bool = False,
|
|
63
|
+
check: bool = True,
|
|
64
|
+
error_msg: str | None = None,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Run a command and return its stripped stdout."""
|
|
67
|
+
cmd = _with_sudo(cmd, sudo=sudo)
|
|
68
|
+
try:
|
|
69
|
+
result = subprocess.run(cmd, text=True, capture_output=True, check=False)
|
|
70
|
+
except FileNotFoundError:
|
|
71
|
+
if check:
|
|
72
|
+
raise LimError(error_msg or _not_found_message(cmd)) from None
|
|
73
|
+
ui.warning(_not_found_message(cmd))
|
|
74
|
+
return ""
|
|
75
|
+
if check and result.returncode != 0:
|
|
76
|
+
raise LimError(
|
|
77
|
+
error_msg
|
|
78
|
+
or f"Command failed with code {result.returncode}: {shlex.join(cmd)}"
|
|
79
|
+
+ (f"\n{result.stderr.strip()}" if result.stderr else "")
|
|
80
|
+
)
|
|
81
|
+
return result.stdout.strip()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def succeeds(cmd: list[str], *, sudo: bool = False) -> bool:
|
|
85
|
+
"""Run a command silently, report whether it exited with 0."""
|
|
86
|
+
cmd = _with_sudo(cmd, sudo=sudo)
|
|
87
|
+
try:
|
|
88
|
+
return subprocess.run(cmd, capture_output=True, check=False).returncode == 0
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
ui.warning(_not_found_message(cmd))
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def pipeline(
|
|
95
|
+
*cmds: list[str],
|
|
96
|
+
sudo_last: bool = False,
|
|
97
|
+
error_msg: str | None = None,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Run commands as a shell-style pipeline (cmd1 | cmd2 | ...)."""
|
|
100
|
+
prepared = [
|
|
101
|
+
_with_sudo(cmd, sudo=sudo_last and index == len(cmds) - 1)
|
|
102
|
+
for index, cmd in enumerate(cmds)
|
|
103
|
+
]
|
|
104
|
+
pretty = " | ".join(shlex.join(cmd) for cmd in prepared)
|
|
105
|
+
ui.info(f"Running: {pretty}")
|
|
106
|
+
processes: list[subprocess.Popen] = []
|
|
107
|
+
previous_stdout = None
|
|
108
|
+
try:
|
|
109
|
+
for index, cmd in enumerate(prepared):
|
|
110
|
+
last = index == len(prepared) - 1
|
|
111
|
+
process = subprocess.Popen(
|
|
112
|
+
cmd,
|
|
113
|
+
stdin=previous_stdout,
|
|
114
|
+
stdout=None if last else subprocess.PIPE,
|
|
115
|
+
)
|
|
116
|
+
if previous_stdout is not None:
|
|
117
|
+
previous_stdout.close()
|
|
118
|
+
previous_stdout = process.stdout
|
|
119
|
+
processes.append(process)
|
|
120
|
+
except FileNotFoundError as exc:
|
|
121
|
+
if previous_stdout is not None:
|
|
122
|
+
previous_stdout.close()
|
|
123
|
+
for process in processes:
|
|
124
|
+
process.kill()
|
|
125
|
+
process.wait()
|
|
126
|
+
raise LimError(f"Command not found: {exc.filename} โ please install it.") from None
|
|
127
|
+
for process in processes:
|
|
128
|
+
process.wait()
|
|
129
|
+
if any(process.returncode != 0 for process in processes):
|
|
130
|
+
raise LimError(error_msg or f"Pipeline failed: {pretty}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def sync_disks() -> None:
|
|
134
|
+
run(["sync"])
|
lim/storage/__init__.py
ADDED
|
File without changes
|
lim/storage/common.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Shared path derivation for encrypted storage setups."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from lim import device as device_module
|
|
6
|
+
from lim import ui
|
|
7
|
+
from lim.device import Device
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class StorageTarget:
|
|
12
|
+
"""An encrypted drive plus all derived mapper/mount/partition paths."""
|
|
13
|
+
|
|
14
|
+
device: Device
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def mapper_name(self) -> str:
|
|
18
|
+
return f"encrypteddrive-{self.device.name}"
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def mapper_path(self) -> str:
|
|
22
|
+
return f"/dev/mapper/{self.mapper_name}"
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def mount_path(self) -> str:
|
|
26
|
+
return f"/media/{self.mapper_name}"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def partition_path(self) -> str:
|
|
30
|
+
return self.device.partition(1)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def select_storage_target() -> StorageTarget:
|
|
34
|
+
target = StorageTarget(device_module.select_device())
|
|
35
|
+
ui.info(f"mapper name set to : {target.mapper_name}")
|
|
36
|
+
ui.info(f"mapper path set to : {target.mapper_path}")
|
|
37
|
+
ui.info(f"mount path set to : {target.mount_path}")
|
|
38
|
+
return target
|
lim/storage/raid1.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Encrypted Btrfs RAID1 across two drives.
|
|
2
|
+
|
|
3
|
+
See https://balaskas.gr/btrfs/raid1.html and
|
|
4
|
+
https://mutschler.eu/linux/install-guides/ubuntu-btrfs-raid1/
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from lim import luks, runner, ui
|
|
8
|
+
from lim.storage.common import StorageTarget, select_storage_target
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _select_pair() -> tuple[StorageTarget, StorageTarget]:
|
|
12
|
+
ui.info("RAID1 partition 1...")
|
|
13
|
+
first = select_storage_target()
|
|
14
|
+
ui.info("RAID1 partition 2...")
|
|
15
|
+
second = select_storage_target()
|
|
16
|
+
return first, second
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def setup() -> None:
|
|
20
|
+
first, second = _select_pair()
|
|
21
|
+
|
|
22
|
+
ui.info(f"Encrypting {first.device.path}...")
|
|
23
|
+
runner.run(["cryptsetup", "luksFormat", first.device.path], sudo=True)
|
|
24
|
+
ui.info(f"Encrypting {second.device.path}...")
|
|
25
|
+
runner.run(["cryptsetup", "luksFormat", second.device.path], sudo=True)
|
|
26
|
+
|
|
27
|
+
runner.run(["cryptsetup", "luksOpen", first.device.path, first.mapper_name], sudo=True)
|
|
28
|
+
runner.run(
|
|
29
|
+
["cryptsetup", "luksOpen", second.device.path, second.mapper_name], sudo=True
|
|
30
|
+
)
|
|
31
|
+
runner.run(["cryptsetup", "status", first.mapper_path], sudo=True)
|
|
32
|
+
runner.run(["cryptsetup", "status", second.mapper_path], sudo=True)
|
|
33
|
+
|
|
34
|
+
runner.run(
|
|
35
|
+
[
|
|
36
|
+
"mkfs.btrfs",
|
|
37
|
+
"-m",
|
|
38
|
+
"raid1",
|
|
39
|
+
"-d",
|
|
40
|
+
"raid1",
|
|
41
|
+
first.mapper_path,
|
|
42
|
+
second.mapper_path,
|
|
43
|
+
],
|
|
44
|
+
sudo=True,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
ui.success("Encryption successfull :)")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _show_luks_devices() -> None:
|
|
51
|
+
for name in runner.output(["lsblk", "-dno", "NAME"], check=False).split():
|
|
52
|
+
if runner.succeeds(["cryptsetup", "isLuks", f"/dev/{name}"], sudo=True):
|
|
53
|
+
ui.info(f"/dev/{name} is a LUKS encrypted storage device.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def mount_on_boot() -> None:
|
|
57
|
+
ui.info("Activate Automount raid1 encrypted storages...")
|
|
58
|
+
_show_luks_devices()
|
|
59
|
+
|
|
60
|
+
first, second = _select_pair()
|
|
61
|
+
|
|
62
|
+
luks.create_luks_key_and_update_crypttab(first.mapper_name, first.device.path)
|
|
63
|
+
ui.info(f'Creating mount folder under "{first.mount_path}"...')
|
|
64
|
+
runner.run(["mkdir", "-vp", first.mount_path], sudo=True)
|
|
65
|
+
luks.create_luks_key_and_update_crypttab(second.mapper_name, second.device.path)
|
|
66
|
+
luks.update_fstab(first.mapper_path, first.mount_path)
|
|
67
|
+
|
|
68
|
+
ui.success("Installation finished. Please restart :)")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Single-drive LUKS + Btrfs storage: setup, mount, umount, mount-on-boot."""
|
|
2
|
+
|
|
3
|
+
from lim import device as device_module
|
|
4
|
+
from lim import luks, runner, system, ui
|
|
5
|
+
from lim.storage.common import select_storage_target
|
|
6
|
+
|
|
7
|
+
# fdisk answer sequences; empty lines accept the defaults.
|
|
8
|
+
CREATE_GPT_TABLE_INPUT = "g\nw\n"
|
|
9
|
+
CREATE_PARTITION_INPUT = "n\n\n\n\np\nw\n"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def setup() -> None:
|
|
13
|
+
print("Setups disk encryption")
|
|
14
|
+
target = select_storage_target()
|
|
15
|
+
|
|
16
|
+
device_module.overwrite_device(target.device)
|
|
17
|
+
|
|
18
|
+
ui.info("Creating new GPT partition table...")
|
|
19
|
+
runner.run(
|
|
20
|
+
["fdisk", "--wipe", "always", target.device.path],
|
|
21
|
+
input_text=CREATE_GPT_TABLE_INPUT,
|
|
22
|
+
sudo=True,
|
|
23
|
+
)
|
|
24
|
+
ui.info("Creating partition table...")
|
|
25
|
+
runner.run(
|
|
26
|
+
["fdisk", "--wipe", "always", target.device.path],
|
|
27
|
+
input_text=CREATE_PARTITION_INPUT,
|
|
28
|
+
sudo=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
ui.info(f"Encrypt {target.device.path}...")
|
|
32
|
+
runner.run(["cryptsetup", "-v", "-y", "luksFormat", target.partition_path], sudo=True)
|
|
33
|
+
|
|
34
|
+
ui.info("Unlock partition...")
|
|
35
|
+
runner.run(
|
|
36
|
+
["cryptsetup", "luksOpen", target.partition_path, target.mapper_name], sudo=True
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
ui.info("Create btrfs file system...")
|
|
40
|
+
runner.run(["mkfs.btrfs", target.mapper_path], sudo=True)
|
|
41
|
+
|
|
42
|
+
ui.info(f'Creating mount folder under "{target.mount_path}"...')
|
|
43
|
+
runner.run(["mkdir", "-p", target.mount_path], sudo=True)
|
|
44
|
+
|
|
45
|
+
ui.info("Mount partition...")
|
|
46
|
+
runner.run(["mount", target.mapper_path, target.mount_path], sudo=True)
|
|
47
|
+
|
|
48
|
+
user = system.real_user()
|
|
49
|
+
ui.info("Own partition by user...")
|
|
50
|
+
runner.run(["chown", "-R", f"{user}:{user}", target.mount_path], sudo=True)
|
|
51
|
+
|
|
52
|
+
ui.success("Encryption successfull :)")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def mount() -> None:
|
|
56
|
+
print("Mounts encrypted storages")
|
|
57
|
+
target = select_storage_target()
|
|
58
|
+
|
|
59
|
+
ui.info("Unlock partition...")
|
|
60
|
+
runner.run(
|
|
61
|
+
["cryptsetup", "luksOpen", target.partition_path, target.mapper_name], sudo=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
ui.info("Mount partition...")
|
|
65
|
+
runner.run(["mount", target.mapper_path, target.mount_path], sudo=True)
|
|
66
|
+
|
|
67
|
+
ui.success("Mounting successfull :)")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def umount() -> None:
|
|
71
|
+
print("Unmount encrypted storages")
|
|
72
|
+
target = select_storage_target()
|
|
73
|
+
|
|
74
|
+
ui.info(f"Unmount {target.mapper_path}...")
|
|
75
|
+
runner.run(["umount", target.mapper_path], sudo=True)
|
|
76
|
+
runner.run(["cryptsetup", "luksClose", target.mapper_name], sudo=True)
|
|
77
|
+
|
|
78
|
+
ui.success("Successfull :)")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def mount_on_boot() -> None:
|
|
82
|
+
print("Automount encrypted storages")
|
|
83
|
+
target = select_storage_target()
|
|
84
|
+
|
|
85
|
+
luks.create_luks_key_and_update_crypttab(target.mapper_name, target.partition_path)
|
|
86
|
+
luks.update_fstab(target.mapper_path, target.mount_path)
|
|
87
|
+
|
|
88
|
+
ui.success("Installation finished. Please restart :)")
|
lim/system.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Process-level helpers: privilege handling and user resolution."""
|
|
2
|
+
|
|
3
|
+
import getpass
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from lim import ui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def is_root() -> bool:
|
|
12
|
+
return os.geteuid() == 0
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ensure_root() -> None:
|
|
16
|
+
"""Re-execute the current command with sudo when not running as root."""
|
|
17
|
+
if is_root():
|
|
18
|
+
return
|
|
19
|
+
ui.info("Root privileges required. Re-executing with sudo...")
|
|
20
|
+
script = str(Path(sys.argv[0]).resolve())
|
|
21
|
+
# Deliberate privilege escalation: replace this process with sudo.
|
|
22
|
+
os.execvp("sudo", ["sudo", sys.executable, script, *sys.argv[1:]]) # noqa: S606
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def real_user() -> str:
|
|
26
|
+
"""Return the invoking user, even when running under sudo."""
|
|
27
|
+
return os.environ.get("SUDO_USER") or getpass.getuser()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def real_home() -> Path:
|
|
31
|
+
"""Home directory of the invoking user, even when running under sudo."""
|
|
32
|
+
sudo_user = os.environ.get("SUDO_USER")
|
|
33
|
+
if sudo_user:
|
|
34
|
+
return Path("/home") / sudo_user
|
|
35
|
+
return Path.home()
|
lim/ui.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Colored console messages and interactive prompts."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
_USE_COLOR = sys.stdout.isatty()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _color(code: str) -> str:
|
|
9
|
+
return f"\033[{code}m" if _USE_COLOR else ""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
COLOR_RED = _color("31")
|
|
13
|
+
COLOR_GREEN = _color("32")
|
|
14
|
+
COLOR_YELLOW = _color("33")
|
|
15
|
+
COLOR_BLUE = _color("34")
|
|
16
|
+
COLOR_MAGENTA = _color("35")
|
|
17
|
+
COLOR_CYAN = _color("36")
|
|
18
|
+
COLOR_WHITE = _color("37")
|
|
19
|
+
COLOR_RESET = _color("0")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def message(color: str, tag: str, text: str) -> None:
|
|
23
|
+
print(f"{color}[{tag}]:{COLOR_RESET} {text}")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def question(text: str) -> None:
|
|
27
|
+
message(COLOR_MAGENTA, "QUESTION", text)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def info(text: str) -> None:
|
|
31
|
+
message(COLOR_BLUE, "INFO", text)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def warning(text: str) -> None:
|
|
35
|
+
message(COLOR_YELLOW, "WARNING", text)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def success(text: str) -> None:
|
|
39
|
+
message(COLOR_GREEN, "SUCCESS", text)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def error(text: str) -> None:
|
|
43
|
+
message(COLOR_RED, "ERROR", text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def ask(prompt: str) -> str:
|
|
47
|
+
question(prompt)
|
|
48
|
+
return input().strip()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def confirm(prompt: str) -> bool:
|
|
52
|
+
return ask(f"{prompt}(y/N)") == "y"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def header() -> None:
|
|
56
|
+
print(
|
|
57
|
+
f"\n{COLOR_YELLOW}The\n"
|
|
58
|
+
"LINUX IMAGE MANAGER\n"
|
|
59
|
+
"is an administration tool designed from and for Kevin Veen-Birkenbach.\n\n"
|
|
60
|
+
"Licensed under GNU GENERAL PUBLIC LICENSE Version 3"
|
|
61
|
+
f"{COLOR_RESET}\n"
|
|
62
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linux-image-manager
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: CLI (`lim`) for downloading, verifying and flashing Linux images, LUKS/Btrfs encrypted storage (single drive and RAID1), image backups and chroot maintenance
|
|
5
|
+
Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
6
|
+
Maintainer-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
7
|
+
License-Expression: GPL-3.0-only
|
|
8
|
+
Project-URL: Homepage, https://veen.world
|
|
9
|
+
Project-URL: Repository, https://github.com/kevinveenbirkenbach/linux-image-manager
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE.txt
|
|
16
|
+
Requires-Dist: PyYAML>=6
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Linux Image Manager ๐ฅ๏ธ๐ ๏ธ
|
|
20
|
+
|
|
21
|
+
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
|
22
|
+
|
|
23
|
+
[](./LICENSE.txt) [](https://github.com/kevinveenbirkenbach/linux-image-manager/stargazers)
|
|
24
|
+
|
|
25
|
+
Linux Image Manager (lim) is a Python tool for downloading, configuring, and managing Linux images. Whether you're setting up encrypted storage, configuring a virtual Btrfs RAID1, performing backups, or chrooting into an image, this tool makes Linux image administration simple and efficient. ๐
|
|
26
|
+
|
|
27
|
+
## Features โจ
|
|
28
|
+
|
|
29
|
+
- **Image Download & Setup:** Automatically download, verify (checksum + GPG signature) and prepare Linux distributions.
|
|
30
|
+
- **Encrypted Storage:** Configure LUKS encryption for secure image management.
|
|
31
|
+
- **Virtual RAID1:** Easily set up virtual Btrfs RAID1 for data redundancy.
|
|
32
|
+
- **Backup & Restore:** Create image backups from devices using dd.
|
|
33
|
+
- **Chroot Environment:** Easily enter a chroot shell to maintain or modify Linux images.
|
|
34
|
+
- **Data Import/Export:** Sync personal data into an encfs-encrypted store and back.
|
|
35
|
+
- **Automated Procedures:** Simplify partitioning, formatting, mounting, and more.
|
|
36
|
+
|
|
37
|
+
## Installation ๐ฆ
|
|
38
|
+
|
|
39
|
+
Install Linux Image Manager quickly using [Kevin's Package Manager](https://github.com/kevinveenbirkenbach/package-manager) under the alias `lim`. Just run:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
package-manager install lim
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This command makes Linux Image Manager globally available as `lim` in your terminal. The `lim` alias points to the **main.py** entry point.
|
|
46
|
+
|
|
47
|
+
There are no Python dependencies beyond the standard library. The commands call the usual system tools (`cryptsetup`, `fdisk`, `dd`, `rsync`, `wget`, `gpg`, `encfs`, `pv`, `bsdtar`, ...), so those need to be installed for the command you use.
|
|
48
|
+
|
|
49
|
+
## Usage โ๏ธ
|
|
50
|
+
|
|
51
|
+
`lim` provides a unified interface for all image and storage operations. Commands that need root privileges re-execute themselves with `sudo` automatically. The built-in `--help` option displays detailed usage information.
|
|
52
|
+
|
|
53
|
+
### Available Command Types
|
|
54
|
+
|
|
55
|
+
| `--type` | Description |
|
|
56
|
+
|---------------|-----------------------------------------------------------------------------|
|
|
57
|
+
| `image` | Download, verify and transfer a Linux image to a device, incl. optional LUKS encryption and Raspberry Pi configuration. |
|
|
58
|
+
| `single` | Set up LUKS encryption with Btrfs on a single drive. |
|
|
59
|
+
| `raid1` | Set up an encrypted virtual Btrfs RAID1 across two drives. |
|
|
60
|
+
| `backup` | Create an image backup from a memory device using dd. |
|
|
61
|
+
| `chroot` | Mount an image and open a shell inside it. |
|
|
62
|
+
| `mount` | Unlock and mount an encrypted drive. |
|
|
63
|
+
| `umount` | Unmount an encrypted drive and close the mapper. |
|
|
64
|
+
| `single-boot` | Register a single encrypted drive for automount on boot (keyfile, crypttab, fstab). |
|
|
65
|
+
| `raid1-boot` | Register an encrypted RAID1 for automount on boot. |
|
|
66
|
+
| `unlock` | Decrypt the encfs data store. |
|
|
67
|
+
| `lock` | Lock the encfs data store. |
|
|
68
|
+
| `import` | Import personal data from the system into the encrypted store. |
|
|
69
|
+
| `export` | Export personal data from the encrypted store back to the system. |
|
|
70
|
+
|
|
71
|
+
### Command-Line Options
|
|
72
|
+
|
|
73
|
+
- **`--type`** *(required)*: Choose the command to execute (see table above).
|
|
74
|
+
- **`--auto-confirm`** *(optional)*: Bypass the confirmation prompt before execution.
|
|
75
|
+
- **`--help`** *(optional)*: Display detailed help information.
|
|
76
|
+
|
|
77
|
+
### Example Commands
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Display help
|
|
81
|
+
lim --help
|
|
82
|
+
|
|
83
|
+
# Execute the Linux image setup
|
|
84
|
+
lim --type image
|
|
85
|
+
|
|
86
|
+
# Run the single drive encryption setup without a confirmation prompt
|
|
87
|
+
lim --type single --auto-confirm
|
|
88
|
+
|
|
89
|
+
# Set up an encrypted RAID1
|
|
90
|
+
lim --type raid1
|
|
91
|
+
|
|
92
|
+
# Back up a memory device to an image file
|
|
93
|
+
lim --type backup
|
|
94
|
+
|
|
95
|
+
# Enter a chroot environment for a Linux image
|
|
96
|
+
lim --type chroot
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Project Structure ๐๏ธ
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
main.py # entry point (the `lim` alias)
|
|
103
|
+
lim/
|
|
104
|
+
cli.py # argument parsing and command dispatch
|
|
105
|
+
catalog.py # read-only access to distributions.yml
|
|
106
|
+
device.py # block device selection, dd, blkid helpers
|
|
107
|
+
luks.py # LUKS keyfiles, crypttab/fstab bookkeeping
|
|
108
|
+
runner.py # subprocess wrapper used by all modules
|
|
109
|
+
storage/ # single drive and RAID1 encryption setups
|
|
110
|
+
image/ # image setup, backup, chroot, verification
|
|
111
|
+
data/ # encfs lock/unlock and data import/export
|
|
112
|
+
distributions.yml # single point of truth for the image catalog
|
|
113
|
+
configuration/ # package collections used during image setup
|
|
114
|
+
tests/unit/ # unit tests (all external commands mocked)
|
|
115
|
+
tests/lint/ # architecture guards (e.g. max file length)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Configuration & Customization ๐ง
|
|
119
|
+
|
|
120
|
+
Customize your environment in the `lim/configuration/` folder:
|
|
121
|
+
- **General Packages:** Contains common packages for all setup scripts.
|
|
122
|
+
- **Server LUKS Packages:** Contains packages needed for setting up LUKS encryption on servers.
|
|
123
|
+
|
|
124
|
+
## Development & Tests ๐งช
|
|
125
|
+
|
|
126
|
+
The test suite mocks all external commands, so it runs safely on any machine:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
pytest
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## License ๐
|
|
133
|
+
|
|
134
|
+
This project is licensed under the GNU General Public License Version 3. See the [LICENSE.txt](./LICENSE.txt) file for details.
|
|
135
|
+
|
|
136
|
+
## Contact & Support ๐ฌ
|
|
137
|
+
|
|
138
|
+
- **Author:** Kevin Veen-Birkenbach
|
|
139
|
+
- **Email:** [kevin@veen.world](mailto:kevin@veen.world)
|
|
140
|
+
- **Website:** [https://www.veen.world/](https://www.veen.world/)
|
|
141
|
+
|
|
142
|
+
Feel free to contribute, report issues, or get in touch. Happy Linux managing! ๐
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
lim/__init__.py,sha256=C8ZE1GmhxFQzIa6WP8EWS89H52g-8yIDMfPaQngPVkE,113
|
|
2
|
+
lim/catalog.py,sha256=DASr3KdjN_Xy7dlsobGnmLBGd8Ppqv1oaSn6Abhn3LY,681
|
|
3
|
+
lim/cli.py,sha256=e92mJeKLsO314fSscg3kioMtyccTLn1GZIQZfGkBPjQ,5131
|
|
4
|
+
lim/config.py,sha256=eEhYGRHNw5ttpiVn7966rLdAm4dwpcu9XF5R__IUikE,630
|
|
5
|
+
lim/device.py,sha256=vzjWocxMCbJl5bpU_tSUw55X46ZhrF5JnqT_X7lfksI,4380
|
|
6
|
+
lim/distributions.yml,sha256=_yL050W7YNFbpaaZ_9rIrGyc1eLqbnxDzMoOH6lMvDI,2178
|
|
7
|
+
lim/errors.py,sha256=0TuxNae3m-Y7VOjb714MrbtsBeiwKtp6Cpdoj6i-XNQ,86
|
|
8
|
+
lim/fsutil.py,sha256=2VT4tA5pj6GM4B_bZbCgZVTblERNDo9MrgGcKR55uRQ,1058
|
|
9
|
+
lim/luks.py,sha256=wpG7vwIW_T0UgHbV_5RTHuxEn7ONjer1LaTpDIPQA1Y,2699
|
|
10
|
+
lim/packages.py,sha256=DlsRCFgf6qifgHFTQpeCrVJW0Ly5sJpVr8V9yylTSO0,636
|
|
11
|
+
lim/runner.py,sha256=KByLjepsYMTRE92DQngsPOuJgORitLxWBcWYPUHKLC8,4278
|
|
12
|
+
lim/system.py,sha256=DAMrv47KGombVp5R73nSPZw7EbKA7bBteZ5LdVPQKns,995
|
|
13
|
+
lim/ui.py,sha256=4uqIOupwT7PdApqnWAPhEAC9OZBkKE2Fa6iRI3NI4rg,1312
|
|
14
|
+
lim/configuration/packages/general.txt,sha256=gdU29G5IKcPA-3XS82J5hRNPZwiRRAHbjcB5ASp2xhg,74
|
|
15
|
+
lim/configuration/packages/server/luks.txt,sha256=i-fkCLHO4XfA5PgeTMwcY1xsm49V0Lq75tKsOXZlrjE,132
|
|
16
|
+
lim/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
|
+
lim/data/crypt.py,sha256=hZltw-tktAllFPYF0UW_yec9NCrSfZ0mSTBlZEw0d5g,881
|
|
18
|
+
lim/data/sync.py,sha256=xLbHCceOY4caUlvdqWGN6TQ2HSrBihM9Rg07nG6aeA8,4648
|
|
19
|
+
lim/image/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
lim/image/backup.py,sha256=t5ImNWS9lBOCSomeNe_CaB1uztcGlakK4rnBCS06TVM,996
|
|
21
|
+
lim/image/choosers.py,sha256=bjscrLQ0Fyy6AvpMpYtM4UDRkVEMzyL9_-UQP5wqgcI,4036
|
|
22
|
+
lim/image/chroot.py,sha256=evqagFlKW1h5DzjegKTbjM4kNGbIYbHDRzdbbrb-Eb0,730
|
|
23
|
+
lim/image/encryption.py,sha256=M5p3POygI-YV69VdUXPLM0iHUHU7QwBhla2C8MZqKtU,4903
|
|
24
|
+
lim/image/plan.py,sha256=yKtIuz2g5njMswXu4DriAhYqi1NGUx2w3XTQH5Qb26Q,908
|
|
25
|
+
lim/image/raspberry.py,sha256=GszoYkzFv0C6deNENbjPtte5-1CvJwxQblX5CGc2nZQ,9024
|
|
26
|
+
lim/image/session.py,sha256=CdS5E3aJlWNzCDrfoIhEpd8Iejy8wXxHuS5Qvs6fNZs,7671
|
|
27
|
+
lim/image/setup.py,sha256=zW7y8ngGT41T2fOR8ubuMC7cT8xm7lcV7R1i6S0x8gk,3296
|
|
28
|
+
lim/image/transfer.py,sha256=sSsWbVvgUhfuu5glBfFLp8VGYf0ephQzADmKMDWCFzM,5089
|
|
29
|
+
lim/image/verify.py,sha256=6nyDFeLDv5Em8F2ebBs44XJn9ShIXBTvg3ua7CFCvPo,3802
|
|
30
|
+
lim/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
|
+
lim/storage/common.py,sha256=Z-zODKO2TT-_XUuk4J3tj02_o4p0RhHj1auyirjmv64,1006
|
|
32
|
+
lim/storage/raid1.py,sha256=LJWXxuNquKfwdg-4NYapsc9K4C7G6Kh0hKqg4bnR8Vg,2259
|
|
33
|
+
lim/storage/single_drive.py,sha256=FdT2h_uhbcm8i9G6901p-dkhp1u5sWZ26EXCq1F8fwo,2747
|
|
34
|
+
linux_image_manager-2.1.0.dist-info/licenses/LICENSE.txt,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
35
|
+
linux_image_manager-2.1.0.dist-info/METADATA,sha256=eY69GFQWLCbNQ8XaZwJ1gxuVVexP3tSq9I8307remdU,7245
|
|
36
|
+
linux_image_manager-2.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
37
|
+
linux_image_manager-2.1.0.dist-info/entry_points.txt,sha256=26af-MdIDdAPbJpU_efWahw6dNRoYilAIhZheY6GCNQ,37
|
|
38
|
+
linux_image_manager-2.1.0.dist-info/top_level.txt,sha256=3pzdvJsQP8C5_0PC98XH_QvY5J4rMGXg6UtVWvjKrzc,4
|
|
39
|
+
linux_image_manager-2.1.0.dist-info/RECORD,,
|