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/__init__.py +461 -0
- kento/attach.py +188 -0
- kento/cloudinit.py +201 -0
- kento/create.py +1205 -0
- kento/defaults.py +207 -0
- kento/destroy.py +131 -0
- kento/diagnose.py +548 -0
- kento/errors.py +48 -0
- kento/exec_cmd.py +50 -0
- kento/hook.py +28 -0
- kento/hook.sh +525 -0
- kento/images.py +210 -0
- kento/info.py +274 -0
- kento/inject.py +28 -0
- kento/inject.sh +282 -0
- kento/layers.py +81 -0
- kento/list.py +146 -0
- kento/locking.py +64 -0
- kento/logs.py +48 -0
- kento/lxc_hook.py +61 -0
- kento/pve.py +619 -0
- kento/reset.py +212 -0
- kento/set_cmd.py +1036 -0
- kento/start.py +65 -0
- kento/stop.py +210 -0
- kento/subprocess_util.py +76 -0
- kento/suspend.py +196 -0
- kento/vm.py +504 -0
- kento/vm_hook.py +256 -0
- kento_core-1.6.0.dev1.dist-info/METADATA +8 -0
- kento_core-1.6.0.dev1.dist-info/RECORD +34 -0
- kento_core-1.6.0.dev1.dist-info/WHEEL +5 -0
- kento_core-1.6.0.dev1.dist-info/licenses/LICENSE.md +594 -0
- kento_core-1.6.0.dev1.dist-info/top_level.txt +1 -0
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,,
|