kento-core 1.6.0.dev1__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.
kento/defaults.py ADDED
@@ -0,0 +1,207 @@
1
+ """Kento default configuration values."""
2
+
3
+ from pathlib import Path
4
+
5
+ # --- LXC defaults ---
6
+ LXC_TTY = 2
7
+ LXC_MOUNT_AUTO = "proc:mixed sys:mixed cgroup:mixed"
8
+ LXC_MOUNT_AUTO_NESTING = "proc:rw sys:rw cgroup:rw"
9
+ LXC_NESTING = False
10
+
11
+ # --- VM defaults ---
12
+ VM_MEMORY = 512 # MB
13
+ VM_CORES = 1
14
+ VM_KVM = True
15
+ VM_MACHINE = "q35"
16
+ VM_SERIAL = "ttyS0"
17
+ VM_DISPLAY = False # -nographic
18
+
19
+ # --- Pass-through denylists (v1.2.0 Phase B) ---
20
+ # Substrings that, if present anywhere in a --qemu-arg value, get rejected.
21
+ # Kept short on purpose: pass-through is an escape hatch, so err on the side
22
+ # of permitting. These specifically name flags kento already emits in vm.py
23
+ # / pve.py generate_qm_args — re-emitting them would either duplicate or
24
+ # conflict with the kento-managed version.
25
+ # -kernel / -initrd : kento owns these (boot from image-provided kernel;
26
+ # future --kernel/--initrd will be dedicated flags).
27
+ # virtiofs / rootfs (inside an arg): kento's virtiofs share — a second
28
+ # -device or -drive naming either would collide with the mount tag.
29
+ # memory-backend-memfd / memfd-size : kento generates this and scrub
30
+ # resyncs its size= to PVE's memory: field.
31
+ # -chardev / -serial : reserved for v1.4.0 VM interactive (serial socket).
32
+ QEMU_ARG_DENYLIST = (
33
+ "-kernel",
34
+ "-initrd",
35
+ "virtiofs",
36
+ "rootfs",
37
+ "memory-backend-memfd",
38
+ "memfd-size",
39
+ "-chardev",
40
+ "-serial",
41
+ )
42
+
43
+ # Substrings that, if present in a --pve-arg value, get rejected.
44
+ # Target only kento-managed keys that would silently clobber generated
45
+ # config: rootfs path, mp0 mount (reserved for virtiofs-equivalent future
46
+ # work), arch, hostname. Everything else (tags, onboot, unprivileged,
47
+ # features, lxc.* raw keys, etc.) is fair game.
48
+ PVE_ARG_DENYLIST = (
49
+ "rootfs:",
50
+ "mp0:",
51
+ "lxc.rootfs.path",
52
+ "arch:",
53
+ "hostname:",
54
+ )
55
+
56
+ # Substrings that, if present in a --lxc-arg value, get rejected. These are
57
+ # the keys generate_config() (create.py) emits structurally for plain-LXC's
58
+ # native config, plus the two cgroup lines `kento set` manages. Re-emitting
59
+ # any of them via pass-through would either duplicate or clobber the
60
+ # kento-managed line — the very wiring (rootfs, hooks, network, apparmor,
61
+ # mount/tty, resource limits) that makes the instance boot.
62
+ # lxc.uts.name : container name (kento owns it).
63
+ # lxc.rootfs.path : overlay rootfs dir (kento owns it).
64
+ # lxc.hook. : pre-start/post-stop/start-host/version hooks.
65
+ # lxc.net. : the veth/none NIC wiring (--network owns it).
66
+ # lxc.mount.auto : the proc/sys/cgroup auto-mounts.
67
+ # lxc.tty.max : kento default.
68
+ # lxc.apparmor. : profile/allow_nesting/allow_incomplete.
69
+ # lxc.cgroup2.memory.max : kento manages via --memory / `kento set`.
70
+ # lxc.cgroup2.cpu.max : kento manages via --cores / `kento set`.
71
+ # Everything else (lxc.mount.entry, lxc.environment, lxc.cgroup2.* other
72
+ # than the two above, lxc.idmap, lxc.cap.*, etc.) is fair game.
73
+ LXC_ARG_DENYLIST = (
74
+ "lxc.uts.name",
75
+ "lxc.rootfs.path",
76
+ "lxc.hook.",
77
+ "lxc.net.",
78
+ "lxc.mount.auto",
79
+ "lxc.tty.max",
80
+ "lxc.apparmor.",
81
+ "lxc.cgroup2.memory.max",
82
+ "lxc.cgroup2.cpu.max",
83
+ )
84
+
85
+
86
+ # --- Config file paths ---
87
+ CONFIG_DIR = Path("/etc/kento")
88
+ LXC_CONFIG_FILE = CONFIG_DIR / "lxc.conf"
89
+ VM_CONFIG_FILE = CONFIG_DIR / "vm.conf"
90
+
91
+ # --- Type parsers ---
92
+ _BOOL_TRUE = {"true", "yes", "1", "on"}
93
+ _BOOL_FALSE = {"false", "no", "0", "off"}
94
+
95
+
96
+ def _parse_bool(value: str) -> bool:
97
+ low = value.strip().lower()
98
+ if low in _BOOL_TRUE:
99
+ return True
100
+ if low in _BOOL_FALSE:
101
+ return False
102
+ raise ValueError(f"invalid boolean: {value!r}")
103
+
104
+
105
+ def load_config(path: Path) -> dict[str, str]:
106
+ """Read a key=value config file.
107
+
108
+ Skips comments (lines starting with #) and blank lines.
109
+ Returns empty dict if the file doesn't exist.
110
+ """
111
+ if not path.is_file():
112
+ return {}
113
+ result: dict[str, str] = {}
114
+ text = path.read_text()
115
+ for line in text.splitlines():
116
+ stripped = line.strip()
117
+ if not stripped or stripped.startswith("#"):
118
+ continue
119
+ if "=" not in stripped:
120
+ continue
121
+ key, _, value = stripped.partition("=")
122
+ result[key.strip()] = value.strip()
123
+ return result
124
+
125
+
126
+ def get_vm_defaults() -> dict[str, object]:
127
+ """Return VM defaults, overridden by values from VM_CONFIG_FILE."""
128
+ defaults: dict[str, object] = {
129
+ "memory": VM_MEMORY,
130
+ "cores": VM_CORES,
131
+ "kvm": VM_KVM,
132
+ "machine": VM_MACHINE,
133
+ "serial": VM_SERIAL,
134
+ "display": VM_DISPLAY,
135
+ }
136
+ overrides = load_config(VM_CONFIG_FILE)
137
+ if "memory" in overrides:
138
+ defaults["memory"] = int(overrides["memory"])
139
+ if "cores" in overrides:
140
+ defaults["cores"] = int(overrides["cores"])
141
+ if "kvm" in overrides:
142
+ defaults["kvm"] = _parse_bool(overrides["kvm"])
143
+ if "machine" in overrides:
144
+ defaults["machine"] = overrides["machine"]
145
+ if "serial" in overrides:
146
+ defaults["serial"] = overrides["serial"]
147
+ if "display" in overrides:
148
+ defaults["display"] = _parse_bool(overrides["display"])
149
+ return defaults
150
+
151
+
152
+ def get_lxc_defaults() -> dict[str, object]:
153
+ """Return LXC defaults, overridden by values from LXC_CONFIG_FILE."""
154
+ defaults: dict[str, object] = {
155
+ "tty": LXC_TTY,
156
+ "mount_auto": LXC_MOUNT_AUTO,
157
+ "mount_auto_nesting": LXC_MOUNT_AUTO_NESTING,
158
+ "nesting": LXC_NESTING,
159
+ }
160
+ overrides = load_config(LXC_CONFIG_FILE)
161
+ if "tty" in overrides:
162
+ defaults["tty"] = int(overrides["tty"])
163
+ if "mount_auto" in overrides:
164
+ defaults["mount_auto"] = overrides["mount_auto"]
165
+ if "mount_auto_nesting" in overrides:
166
+ defaults["mount_auto_nesting"] = overrides["mount_auto_nesting"]
167
+ if "nesting" in overrides:
168
+ defaults["nesting"] = _parse_bool(overrides["nesting"])
169
+ return defaults
170
+
171
+
172
+ _LXC_CONF_HEADER = """\
173
+ # Kento LXC defaults
174
+ # Uncomment and edit to override hardcoded defaults.
175
+ # Changes take effect on next container create.
176
+ """
177
+
178
+ _VM_CONF_HEADER = """\
179
+ # Kento VM defaults
180
+ # Uncomment and edit to override hardcoded defaults.
181
+ # Changes take effect on next VM create.
182
+ """
183
+
184
+
185
+ def ensure_config_files() -> None:
186
+ """Create default config files if they don't already exist."""
187
+ CONFIG_DIR.mkdir(parents=True, exist_ok=True)
188
+
189
+ if not LXC_CONFIG_FILE.exists():
190
+ lines = [_LXC_CONF_HEADER]
191
+ lines.append(f"# tty = {LXC_TTY}")
192
+ lines.append(f"# mount_auto = {LXC_MOUNT_AUTO}")
193
+ lines.append(f"# mount_auto_nesting = {LXC_MOUNT_AUTO_NESTING}")
194
+ lines.append(f"# nesting = {LXC_NESTING}")
195
+ lines.append("")
196
+ LXC_CONFIG_FILE.write_text("\n".join(lines))
197
+
198
+ if not VM_CONFIG_FILE.exists():
199
+ lines = [_VM_CONF_HEADER]
200
+ lines.append(f"# memory = {VM_MEMORY}")
201
+ lines.append(f"# cores = {VM_CORES}")
202
+ lines.append(f"# kvm = {VM_KVM}")
203
+ lines.append(f"# machine = {VM_MACHINE}")
204
+ lines.append(f"# serial = {VM_SERIAL}")
205
+ lines.append(f"# display = {VM_DISPLAY}")
206
+ lines.append("")
207
+ VM_CONFIG_FILE.write_text("\n".join(lines))
kento/destroy.py ADDED
@@ -0,0 +1,131 @@
1
+ """Remove a kento-managed instance."""
2
+
3
+ import logging
4
+ import shutil
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from kento import is_running, read_mode, require_root, resolve_container
9
+ from kento.errors import StateError
10
+
11
+ logger = logging.getLogger("kento")
12
+
13
+
14
+ def destroy(name: str, force: bool = False, *, container_dir: Path | None = None, mode: str | None = None) -> None:
15
+ require_root()
16
+
17
+ if container_dir is None:
18
+ container_dir = resolve_container(name)
19
+ container_id = container_dir.name
20
+
21
+ if mode is None:
22
+ # Detect mode (default lxc for containers created before mode tracking)
23
+ mode = read_mode(container_dir)
24
+
25
+ # Read state dir before we delete anything
26
+ state_file = container_dir / "kento-state"
27
+ state_dir = Path(state_file.read_text().strip()) if state_file.is_file() else container_dir
28
+
29
+ # Check if running
30
+ running = is_running(container_dir, mode)
31
+
32
+ if running and not force:
33
+ raise StateError(
34
+ f"instance {name} is running. "
35
+ f"Use 'kento lxc destroy -f {name}' or 'kento vm destroy -f {name}' to force removal."
36
+ )
37
+
38
+ if running:
39
+ logger.info("Stopping...")
40
+ try:
41
+ if mode == "vm":
42
+ from kento.vm import stop_vm
43
+ stop_vm(container_dir, force=True)
44
+ elif mode == "pve-vm":
45
+ vmid = (container_dir / "kento-vmid").read_text().strip()
46
+ subprocess.run(["qm", "stop", vmid], check=True, capture_output=True)
47
+ elif mode == "pve":
48
+ subprocess.run(["pct", "stop", container_id], check=True, capture_output=True)
49
+ else:
50
+ subprocess.run(["lxc-stop", "-n", container_id], check=True, capture_output=True)
51
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
52
+ # destroy() only runs the stop-before-remove path when called with -f
53
+ # (the non-force path errors above). Log and continue to cleanup so
54
+ # a partially-wedged instance can still be removed.
55
+ if isinstance(e, subprocess.CalledProcessError):
56
+ stderr = (e.stderr or b"").decode("utf-8", "replace").strip()
57
+ logger.warning("stop failed (exit %s); proceeding with cleanup: %s",
58
+ e.returncode, stderr)
59
+ else:
60
+ logger.warning("stop tool not found (%s); proceeding with cleanup.", e.filename)
61
+
62
+ # Unmount rootfs if still mounted. Use the busy-mount-hardened helper:
63
+ # under -f we want a wedged instance (qm timeout, leaked virtiofsd, etc.)
64
+ # to still succeed via fuser-kill + lazy umount fallback rather than hang
65
+ # the destroy. Without -f we keep strict semantics — fail loudly.
66
+ rootfs = container_dir / "rootfs"
67
+ if subprocess.run(["mountpoint", "-q", str(rootfs)],
68
+ capture_output=True).returncode == 0:
69
+ from kento.vm import _umount_with_retry
70
+ if not _umount_with_retry(rootfs, force=force):
71
+ raise StateError(f"failed to unmount {rootfs}. Is the container still running?")
72
+
73
+ # Release OCI image mount
74
+ from kento.layers import _podman_cmd
75
+ image = (container_dir / "kento-image").read_text().strip()
76
+ subprocess.run(
77
+ [*_podman_cmd(), "image", "unmount", image],
78
+ capture_output=True,
79
+ )
80
+
81
+ # Read vmid before deletion (needed for pve-vm cleanup)
82
+ vmid_str = None
83
+ if mode == "pve-vm":
84
+ vmid_file = container_dir / "kento-vmid"
85
+ vmid_str = vmid_file.read_text().strip() if vmid_file.is_file() else None
86
+
87
+ # Clean up platform-specific config BEFORE removing container_dir. Mirror
88
+ # the best-effort stop handler above: under -f a pmxcfs I/O error, a corrupt
89
+ # kento-vmid (int() ValueError), or any deletion failure must NOT skip the
90
+ # final rmtree and leave an orphan dir. Warn and continue so cleanup still
91
+ # reaches the rmtree below.
92
+ try:
93
+ if mode == "pve":
94
+ from kento.pve import delete_pve_config
95
+ from kento.lxc_hook import delete_lxc_snippets_wrapper
96
+ delete_pve_config(int(container_id))
97
+ delete_lxc_snippets_wrapper(int(container_id))
98
+ elif mode == "pve-vm" and vmid_str:
99
+ from kento.pve import delete_qm_config
100
+ from kento.vm_hook import delete_snippets_wrapper
101
+ try:
102
+ vmid = int(vmid_str)
103
+ except ValueError:
104
+ vmid = None
105
+ logger.warning("corrupt kento-vmid (%r); skipping qm config "
106
+ "cleanup, proceeding with removal.", vmid_str)
107
+ if vmid is not None:
108
+ delete_qm_config(vmid)
109
+ delete_snippets_wrapper(vmid)
110
+ except Exception as e:
111
+ if not force:
112
+ raise
113
+ logger.warning("platform config cleanup failed (%s); proceeding with removal.", e)
114
+
115
+ try:
116
+ from kento.layers import remove_image_hold
117
+ name_file = container_dir / "kento-name"
118
+ hold_name = name_file.read_text().strip() if name_file.is_file() else name
119
+ remove_image_hold(hold_name)
120
+ except Exception as e:
121
+ if not force:
122
+ raise
123
+ logger.warning("image-hold removal failed (%s); proceeding with removal.", e)
124
+
125
+ # Remove state dir if separate from container_dir
126
+ if state_dir != container_dir and state_dir.is_dir():
127
+ shutil.rmtree(state_dir)
128
+
129
+ shutil.rmtree(container_dir)
130
+
131
+ logger.info("Removed: %s", name)