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/vm_hook.py ADDED
@@ -0,0 +1,256 @@
1
+ """Generate per-VM PVE hookscripts (qm hookscripts)."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from kento.defaults import VM_CONFIG_FILE, load_config
7
+ from kento.errors import StateError, SubprocessError
8
+
9
+ _STORAGE_CFG = Path("/etc/pve/storage.cfg")
10
+
11
+
12
+ def generate_vm_hook(container_dir: Path, layers: str, name: str,
13
+ state_dir: Path) -> str:
14
+ """Return a hookscript with baked-in paths for a PVE VM.
15
+
16
+ The script receives VMID as $1 and phase as $2 from qm.
17
+ """
18
+ return f"""#!/bin/sh
19
+ set -eu
20
+
21
+ VMID="$1"
22
+ PHASE="$2"
23
+
24
+ NAME="{name}"
25
+ CONTAINER_DIR="{container_dir}"
26
+ STATE_DIR="{state_dir}"
27
+ LAYERS="{layers}"
28
+
29
+ case "$PHASE" in
30
+ pre-start)
31
+ # 1. Validate memory consistency
32
+ QM_CONF="/etc/pve/qemu-server/${{VMID}}.conf"
33
+ if [ -f "$QM_CONF" ]; then
34
+ CONF_MEM=$(sed -n 's/^memory: *//p' "$QM_CONF")
35
+ ARGS_MEM=$(sed -n 's/.*size=\\([0-9]*\\)M.*/\\1/p' "$QM_CONF")
36
+ if [ -n "$CONF_MEM" ] && [ -n "$ARGS_MEM" ]; then
37
+ if [ "$CONF_MEM" != "$ARGS_MEM" ]; then
38
+ echo "kento-hook: error: memory mismatch — memory: ${{CONF_MEM}}M but memfd size=${{ARGS_MEM}}M in args." >&2
39
+ echo "kento-hook: run: kento vm scrub $NAME to regenerate config with matching values." >&2
40
+ exit 1
41
+ fi
42
+ fi
43
+ fi
44
+
45
+ # 2. Validate layer paths
46
+ IFS=:
47
+ for dir in $LAYERS; do
48
+ if [ ! -d "$dir" ]; then
49
+ echo "kento-hook: error: layer path missing: $dir" >&2
50
+ echo "kento-hook: image may have changed. Run: kento vm scrub $NAME" >&2
51
+ exit 1
52
+ fi
53
+ done
54
+ unset IFS
55
+
56
+ # 3. Mount overlayfs
57
+ ROOTFS="$CONTAINER_DIR/rootfs"
58
+ mkdir -p "$STATE_DIR/upper" "$STATE_DIR/work" "$ROOTFS"
59
+ export LIBMOUNT_FORCE_MOUNT2=always
60
+ mount -t overlay overlay \\
61
+ -o "lowerdir=$LAYERS,upperdir=$STATE_DIR/upper,workdir=$STATE_DIR/work" \\
62
+ "$ROOTFS"
63
+
64
+ # 4. Validate kernel and initramfs
65
+ if [ ! -f "$ROOTFS/boot/vmlinuz" ]; then
66
+ umount "$ROOTFS" 2>/dev/null || true
67
+ echo "kento-hook: error: kernel not found at $ROOTFS/boot/vmlinuz" >&2
68
+ exit 1
69
+ fi
70
+ if [ ! -f "$ROOTFS/boot/initramfs.img" ]; then
71
+ umount "$ROOTFS" 2>/dev/null || true
72
+ echo "kento-hook: error: initramfs not found at $ROOTFS/boot/initramfs.img" >&2
73
+ exit 1
74
+ fi
75
+
76
+ # 5. Inject guest-side config (hostname/network/tz/env/ssh-key) into
77
+ # the mounted rootfs before virtiofsd starts. A failing inject is a
78
+ # start failure — don't boot a misconfigured VM. set -eu at the top
79
+ # causes the hookscript to abort on non-zero exit, which PVE treats
80
+ # as a VM start failure.
81
+ sh "$CONTAINER_DIR/kento-inject.sh" "$ROOTFS" "$CONTAINER_DIR"
82
+
83
+ # 6. Find and start virtiofsd
84
+ VIRTIOFSD=""
85
+ for p in virtiofsd /usr/libexec/virtiofsd /usr/lib/qemu/virtiofsd /usr/lib/virtiofsd /usr/bin/virtiofsd; do
86
+ if command -v "$p" >/dev/null 2>&1 || [ -x "$p" ]; then
87
+ VIRTIOFSD="$p"
88
+ break
89
+ fi
90
+ done
91
+ if [ -z "$VIRTIOFSD" ]; then
92
+ umount "$ROOTFS" 2>/dev/null || true
93
+ echo "kento-hook: error: virtiofsd not found" >&2
94
+ exit 1
95
+ fi
96
+
97
+ SOCKET="$CONTAINER_DIR/virtiofsd.sock"
98
+ setsid $VIRTIOFSD --socket-path="$SOCKET" --shared-dir="$ROOTFS" --cache=auto \
99
+ </dev/null >"$CONTAINER_DIR/virtiofsd.log" 2>&1 &
100
+ VFS_PID=$!
101
+ echo "$VFS_PID" > "$CONTAINER_DIR/kento-virtiofsd-pid"
102
+
103
+ # 7. Wait for socket (5s timeout)
104
+ TRIES=50
105
+ while [ $TRIES -gt 0 ]; do
106
+ [ -e "$SOCKET" ] && break
107
+ sleep 0.1
108
+ TRIES=$((TRIES - 1))
109
+ done
110
+ if [ ! -e "$SOCKET" ]; then
111
+ kill "$VFS_PID" 2>/dev/null || true
112
+ umount "$ROOTFS" 2>/dev/null || true
113
+ echo "kento-hook: error: virtiofsd socket did not appear at $SOCKET" >&2
114
+ exit 1
115
+ fi
116
+ ;;
117
+
118
+ post-stop)
119
+ # 1. Kill virtiofsd
120
+ PID_FILE="$CONTAINER_DIR/kento-virtiofsd-pid"
121
+ if [ -f "$PID_FILE" ]; then
122
+ VFS_PID=$(cat "$PID_FILE")
123
+ # Guard against an empty/garbage pid file: a non-numeric VFS_PID
124
+ # would make "[ -d /proc/$VFS_PID ]" test "/proc/" (always true)
125
+ # and stall the post-stop hook for 5s. Blank it unless numeric.
126
+ case "$VFS_PID" in
127
+ ''|*[!0-9]*) VFS_PID='' ;;
128
+ esac
129
+ if [ -n "$VFS_PID" ] && [ -d "/proc/$VFS_PID" ]; then
130
+ kill "$VFS_PID" 2>/dev/null || true
131
+ # Wait up to 5s for exit
132
+ TRIES=50
133
+ while [ $TRIES -gt 0 ] && [ -d "/proc/$VFS_PID" ]; do
134
+ sleep 0.1
135
+ TRIES=$((TRIES - 1))
136
+ done
137
+ # Force kill if still alive
138
+ if [ -d "/proc/$VFS_PID" ]; then
139
+ kill -9 "$VFS_PID" 2>/dev/null || true
140
+ fi
141
+ fi
142
+ rm -f "$PID_FILE"
143
+ fi
144
+
145
+ # 2. Unmount overlayfs
146
+ ROOTFS="$CONTAINER_DIR/rootfs"
147
+ mountpoint -q "$ROOTFS" 2>/dev/null && umount "$ROOTFS" || true
148
+
149
+ # 3. Clean up socket
150
+ rm -f "$CONTAINER_DIR/virtiofsd.sock"
151
+ ;;
152
+ esac
153
+ """
154
+
155
+
156
+ def generate_snippets_wrapper(hook_path: str) -> str:
157
+ """Return a thin wrapper script that forwards to the real hook."""
158
+ return f"""#!/bin/sh
159
+ exec "{hook_path}" "$@"
160
+ """
161
+
162
+
163
+ def write_vm_hook(container_dir: Path, layers: str, name: str,
164
+ state_dir: Path) -> Path:
165
+ """Generate and write the VM hook script into the container directory."""
166
+ hook_path = container_dir / "kento-hook"
167
+ hook_path.write_text(generate_vm_hook(container_dir, layers, name, state_dir))
168
+ hook_path.chmod(0o755)
169
+ return hook_path
170
+
171
+
172
+ def find_snippets_dir() -> tuple[Path, str]:
173
+ """Find the PVE snippets directory and storage name.
174
+
175
+ Returns (snippets_path, storage_name).
176
+ """
177
+ config = load_config(VM_CONFIG_FILE)
178
+ storage_name = config.get("snippets_storage")
179
+
180
+ if not storage_name:
181
+ first_dir_storage = None
182
+ first_dir_content = None
183
+ if _STORAGE_CFG.is_file():
184
+ current_storage = None
185
+ current_type = None
186
+ for line in _STORAGE_CFG.read_text().splitlines():
187
+ stripped = line.strip()
188
+ if stripped.startswith(("dir:", "nfs:", "cifs:", "glusterfs:",
189
+ "zfspool:", "btrfs:", "lvmthin:", "lvm:")):
190
+ current_type = stripped.split(":")[0]
191
+ current_storage = stripped.split(":", 1)[1].strip().split()[0]
192
+ elif stripped.startswith("content") and current_storage:
193
+ content = stripped.split(None, 1)[1] if len(stripped.split(None, 1)) > 1 else ""
194
+ if "snippets" in content.split(","):
195
+ storage_name = current_storage
196
+ break
197
+ if first_dir_storage is None and current_type == "dir":
198
+ first_dir_storage = current_storage
199
+ first_dir_content = content.strip()
200
+
201
+ if not storage_name:
202
+ msg = "no PVE storage has 'snippets' in its content types."
203
+ if first_dir_storage and first_dir_content:
204
+ msg += (f"\n\nEnable snippets on your '{first_dir_storage}' storage:\n"
205
+ f" pvesm set {first_dir_storage} --content "
206
+ f"{first_dir_content},snippets")
207
+ else:
208
+ msg += ("\n\nEnable snippets on a storage (e.g. 'local'):\n"
209
+ " pvesm set local --content iso,vztmpl,backup,snippets")
210
+ msg += (f"\n\nOr set a specific storage in /etc/kento/vm.conf:\n"
211
+ f" snippets_storage = <name>")
212
+ raise StateError(msg)
213
+
214
+ # Resolve the filesystem path via pvesm
215
+ result = subprocess.run(
216
+ ["pvesm", "path", f"{storage_name}:snippets/probe"],
217
+ capture_output=True, text=True,
218
+ )
219
+ if result.returncode != 0:
220
+ raise SubprocessError(
221
+ f"failed to resolve snippets path for storage '{storage_name}': "
222
+ f"{result.stderr.strip()}",
223
+ cmd=["pvesm", "path", f"{storage_name}:snippets/probe"],
224
+ returncode=result.returncode,
225
+ )
226
+
227
+ # pvesm path returns something like /var/lib/vz/snippets/probe
228
+ # Strip the filename to get the directory
229
+ snippets_path = Path(result.stdout.strip()).parent
230
+ return snippets_path, storage_name
231
+
232
+
233
+ def write_snippets_wrapper(vmid: int, hook_path: Path, *,
234
+ snippets_dir: Path | None = None,
235
+ storage_name: str | None = None) -> str:
236
+ """Write a snippets wrapper and return the PVE storage reference.
237
+
238
+ Returns e.g. "local:snippets/kento-vm-100.sh"
239
+ """
240
+ if snippets_dir is None or storage_name is None:
241
+ snippets_dir, storage_name = find_snippets_dir()
242
+ wrapper_name = f"kento-vm-{vmid}.sh"
243
+ wrapper_path = snippets_dir / wrapper_name
244
+ wrapper_path.write_text(generate_snippets_wrapper(str(hook_path)))
245
+ wrapper_path.chmod(0o755)
246
+ return f"{storage_name}:snippets/{wrapper_name}"
247
+
248
+
249
+ def delete_snippets_wrapper(vmid: int) -> None:
250
+ """Delete the snippets wrapper for a given VMID."""
251
+ try:
252
+ snippets_dir, _ = find_snippets_dir()
253
+ wrapper = snippets_dir / f"kento-vm-{vmid}.sh"
254
+ wrapper.unlink(missing_ok=True)
255
+ except StateError:
256
+ pass # No snippets storage = nothing to clean up
@@ -0,0 +1,8 @@
1
+ Metadata-Version: 2.4
2
+ Name: kento-core
3
+ Version: 1.6.0.dev1
4
+ Summary: Kento core library — compose OCI images into LXC/VM system containers (importable engine)
5
+ License-Expression: GPL-3.0-only
6
+ Requires-Python: >=3.11
7
+ License-File: LICENSE.md
8
+ Dynamic: license-file
@@ -0,0 +1,34 @@
1
+ kento/__init__.py,sha256=YcBS6rMrMGSJOljQyeXGtcWt_YGVp84FIQvSMhRgFN0,17440
2
+ kento/attach.py,sha256=Dp2xOvooWUSGRafi3ji0cB_PUPzfCHbnkGseNKrviz4,6779
3
+ kento/cloudinit.py,sha256=czC0Nk0PKHIIM6shn49SsUcdD-1-uKMkzGl-y-BBBfY,7427
4
+ kento/create.py,sha256=xjq7uSMHTwvFyeFFhRf9NoKP1UP5ekIl1oKxRl1Pr5g,56318
5
+ kento/defaults.py,sha256=IYXgklBNl1Bi3fp9GoeYXBpmdAf7_9d1PQyu8mdHdMw,7220
6
+ kento/destroy.py,sha256=0j-R9-ghv-FRlnfSevix9ZhSeXLq4NeLJZVAMINLaDA,5496
7
+ kento/diagnose.py,sha256=mbZTtdlrDg0vsitdaAlZqA1K1pEJdjsD-KXwCHNwdw0,21478
8
+ kento/errors.py,sha256=eVjHhPEvg5BSR_yYUD09jCKMos1HSz7Hs3nPHfcHFxo,1448
9
+ kento/exec_cmd.py,sha256=t1Q13tTFoCmKFPjaU26liGGbeb73GefzGXpOfnUH0R4,1659
10
+ kento/hook.py,sha256=GspBk7SaQUuyAoXZDf-vEJJZSO0ulBfJfFLrDdxcmvY,1035
11
+ kento/hook.sh,sha256=4_t3IMvL15V3mejmZ8aWLyUfeEEOK8cCovakX3d-K0c,27463
12
+ kento/images.py,sha256=0sz2RpvIGlujhCtNyZTLbk4aYMPd9RFZWPGRGoOx-ks,7963
13
+ kento/info.py,sha256=R-3o9pQV8nHzIx9zaEM0iv-KM7zddYYeiaiTFNe1E94,9962
14
+ kento/inject.py,sha256=ChUhUrkXRG9Cj_3tzjG9pvrmjyUdgwIeAJ6YUDAskpc,973
15
+ kento/inject.sh,sha256=Gv389XggJfmrGcxBnHMbzH4uwCoIj3MdBWlrPN8u9lw,10971
16
+ kento/layers.py,sha256=zbhvRK8sOzMuXSo9--kv7a2vVFj8J7LD5iJg_AWeqQc,2367
17
+ kento/list.py,sha256=UhmwofwHp5xJkTJZ5nW2txUDaUWSzT-HpSVz8JI0e5g,6142
18
+ kento/locking.py,sha256=bXdcnZtF40kwH7F38RwVfprPiHGa3jYHYwEnKGt4M68,2032
19
+ kento/logs.py,sha256=q8aU8Ne9xEybc0x-q3YIAMBrfdyVRFjW0nNV3X4NJOo,1524
20
+ kento/lxc_hook.py,sha256=6LGudwN47MXBfUx6W165-uOkpVlwD25xegLODOl9CLU,2224
21
+ kento/pve.py,sha256=DfBJVwj3mgJQkx8nUf51UK5YYs7Lq3QPWDkqbvnCe-s,25770
22
+ kento/reset.py,sha256=FvfPGB3BsiZq5xgR7YyXip6ZtFOaMQv2vZcfrL5NqGA,9470
23
+ kento/set_cmd.py,sha256=mB5eWjsJTsh50ceqhHpKx6dtmiC-kkMDMp6beFuP2mc,42713
24
+ kento/start.py,sha256=mBnz6joLpKCtWzw2SsVapYsSeBvnJLJyxhr4fJ9E5mg,2267
25
+ kento/stop.py,sha256=lU2_nEvcbVKrbghBi6uBMwyzhSSkm-gR5MRRje2zxW4,8003
26
+ kento/subprocess_util.py,sha256=V44cpaMZlQ6fszQcLIZIBKG-jQdPvzUy3bFmqDF7rS8,2770
27
+ kento/suspend.py,sha256=laPvgY9pYjO0zndYXDlWgyuEitcDO0ERq0YvdSnnzJs,7373
28
+ kento/vm.py,sha256=MoHlrYhM7BrTb3GkhZRP2jK1lARZHjNy9itOU30tZo0,20238
29
+ kento/vm_hook.py,sha256=Rv2GQRyecXAQdZDzMX5YRsMpM3SKYZrbXvr8kNGNr5I,9797
30
+ kento_core-1.6.0.dev1.dist-info/licenses/LICENSE.md,sha256=myGniuZJOzblT1ZUaSVj9hNBLxPw3J80w6xujEYC_4c,34904
31
+ kento_core-1.6.0.dev1.dist-info/METADATA,sha256=qkdhEQYmns-fKXraDxZCHIuFwzg8YeDR-m9emSsEDKE,264
32
+ kento_core-1.6.0.dev1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
33
+ kento_core-1.6.0.dev1.dist-info/top_level.txt,sha256=jUOZ34N2uIdurIGNjatioce-AvnLtwDKspYSO7jk2lo,6
34
+ kento_core-1.6.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+