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/reset.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Scrub a kento-managed instance back to clean OCI state."""
|
|
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
|
+
from kento.hook import write_hook
|
|
11
|
+
from kento.layers import ensure_image_hold, resolve_layers
|
|
12
|
+
from kento.vm_hook import write_vm_hook
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("kento")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _safe_clear_dir(path: Path) -> None:
|
|
18
|
+
# Crash-safe clear: rename target out of the way, create a fresh
|
|
19
|
+
# empty dir, then remove the old copy. A crash mid-sequence leaves
|
|
20
|
+
# either ``path.old`` (next scrub sweeps it) or an empty ``path``;
|
|
21
|
+
# we never sit in a ``rmtree completed, mkdir not yet`` window where
|
|
22
|
+
# the overlayfs mount point is gone.
|
|
23
|
+
stale = path.with_name(path.name + ".old")
|
|
24
|
+
if stale.exists():
|
|
25
|
+
shutil.rmtree(stale)
|
|
26
|
+
if path.exists():
|
|
27
|
+
path.rename(stale)
|
|
28
|
+
path.mkdir(parents=True)
|
|
29
|
+
if stale.exists():
|
|
30
|
+
shutil.rmtree(stale)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def reset(name: str, *, container_dir: Path | None = None, mode: str | None = None) -> None:
|
|
34
|
+
require_root()
|
|
35
|
+
|
|
36
|
+
if container_dir is None:
|
|
37
|
+
container_dir = resolve_container(name)
|
|
38
|
+
|
|
39
|
+
if mode is None:
|
|
40
|
+
# Detect mode (default lxc for containers created before mode tracking)
|
|
41
|
+
mode = read_mode(container_dir)
|
|
42
|
+
|
|
43
|
+
# Refuse if running
|
|
44
|
+
if is_running(container_dir, mode):
|
|
45
|
+
raise StateError(f"instance is running. Stop it first: kento stop {name}")
|
|
46
|
+
|
|
47
|
+
# Re-resolve layers from image BEFORE touching the writable layer. If the
|
|
48
|
+
# backing image is gone from the store, resolve_layers() raises
|
|
49
|
+
# ImageNotFoundError — we want that to abort with zero side effects, not
|
|
50
|
+
# after _safe_clear_dir() has already wiped upper/work, leaving a
|
|
51
|
+
# half-scrubbed instance with stale kento-layers/hook.
|
|
52
|
+
image = (container_dir / "kento-image").read_text().strip()
|
|
53
|
+
layers = resolve_layers(image)
|
|
54
|
+
|
|
55
|
+
# Read state dir
|
|
56
|
+
state_file = container_dir / "kento-state"
|
|
57
|
+
state_dir = Path(state_file.read_text().strip()) if state_file.is_file() else container_dir
|
|
58
|
+
|
|
59
|
+
# Unmount rootfs if mounted
|
|
60
|
+
rootfs = container_dir / "rootfs"
|
|
61
|
+
if subprocess.run(["mountpoint", "-q", str(rootfs)],
|
|
62
|
+
capture_output=True).returncode == 0:
|
|
63
|
+
result = subprocess.run(["umount", str(rootfs)])
|
|
64
|
+
if result.returncode != 0:
|
|
65
|
+
raise StateError(f"failed to unmount {rootfs}. Is the container still running?")
|
|
66
|
+
|
|
67
|
+
# Unprivileged cleanup: unmount idmapped bind mounts in reverse order
|
|
68
|
+
# and remove the $STATE_DIR/idmap directory. This handles stale mounts
|
|
69
|
+
# left behind by a crashed unprivileged start so that the next start
|
|
70
|
+
# isn't blocked by the bind idempotency guard finding stale mounts that
|
|
71
|
+
# point at the wrong (old) overlay configuration.
|
|
72
|
+
idmap_dir = state_dir / "idmap"
|
|
73
|
+
if idmap_dir.exists():
|
|
74
|
+
# Collect subdirectories in sorted order, unmount in reverse.
|
|
75
|
+
idmap_subdirs = sorted(idmap_dir.iterdir())
|
|
76
|
+
for d in reversed(idmap_subdirs):
|
|
77
|
+
if d.is_dir():
|
|
78
|
+
subprocess.run(["umount", str(d)], capture_output=True)
|
|
79
|
+
shutil.rmtree(idmap_dir, ignore_errors=True)
|
|
80
|
+
|
|
81
|
+
# Clear writable layer (crash-safe: rename then rm, not rm then mkdir)
|
|
82
|
+
_safe_clear_dir(state_dir / "upper")
|
|
83
|
+
_safe_clear_dir(state_dir / "work")
|
|
84
|
+
|
|
85
|
+
# Clean up stale port forwarding state (safety net)
|
|
86
|
+
portfwd_active = container_dir / "kento-portfwd-active"
|
|
87
|
+
portfwd_active.unlink(missing_ok=True)
|
|
88
|
+
|
|
89
|
+
# Re-inject guest config from kento metadata
|
|
90
|
+
from kento.create import (_inject_network_config, _inject_hostname,
|
|
91
|
+
_inject_timezone, _inject_env)
|
|
92
|
+
|
|
93
|
+
# Hostname
|
|
94
|
+
name_file = container_dir / "kento-name"
|
|
95
|
+
if name_file.is_file():
|
|
96
|
+
_inject_hostname(state_dir, name_file.read_text().strip())
|
|
97
|
+
|
|
98
|
+
# Network (static IP + searchdomain)
|
|
99
|
+
net_file = container_dir / "kento-net"
|
|
100
|
+
if net_file.is_file():
|
|
101
|
+
net_cfg = {}
|
|
102
|
+
for line in net_file.read_text().strip().splitlines():
|
|
103
|
+
k, v = line.split("=", 1)
|
|
104
|
+
net_cfg[k] = v
|
|
105
|
+
if "ip" in net_cfg:
|
|
106
|
+
_inject_network_config(state_dir, net_cfg["ip"],
|
|
107
|
+
net_cfg.get("gateway"), net_cfg.get("dns"),
|
|
108
|
+
net_cfg.get("searchdomain"), mode=mode)
|
|
109
|
+
elif net_cfg.get("dns") or net_cfg.get("searchdomain"):
|
|
110
|
+
resolved_dir = state_dir / "upper" / "etc" / "systemd" / "resolved.conf.d"
|
|
111
|
+
resolved_dir.mkdir(parents=True, exist_ok=True)
|
|
112
|
+
lines = ["[Resolve]"]
|
|
113
|
+
if net_cfg.get("dns"):
|
|
114
|
+
lines.append(f"DNS={net_cfg['dns']}")
|
|
115
|
+
if net_cfg.get("searchdomain"):
|
|
116
|
+
lines.append(f"Domains={net_cfg['searchdomain']}")
|
|
117
|
+
lines.append("")
|
|
118
|
+
(resolved_dir / "90-kento.conf").write_text("\n".join(lines))
|
|
119
|
+
|
|
120
|
+
# Timezone
|
|
121
|
+
tz_file = container_dir / "kento-tz"
|
|
122
|
+
if tz_file.is_file():
|
|
123
|
+
_inject_timezone(state_dir, tz_file.read_text().strip())
|
|
124
|
+
|
|
125
|
+
# Environment variables
|
|
126
|
+
env_file = container_dir / "kento-env"
|
|
127
|
+
if env_file.is_file():
|
|
128
|
+
_inject_env(state_dir, env_file.read_text().strip().splitlines())
|
|
129
|
+
|
|
130
|
+
# Regenerate cloud-init seed if in cloudinit mode
|
|
131
|
+
config_mode_file = container_dir / "kento-config-mode"
|
|
132
|
+
if config_mode_file.is_file() and config_mode_file.read_text().strip() == "cloudinit":
|
|
133
|
+
from kento.cloudinit import write_seed
|
|
134
|
+
# Gather config from metadata files
|
|
135
|
+
net_file_ci = container_dir / "kento-net"
|
|
136
|
+
net_cfg_ci = {}
|
|
137
|
+
if net_file_ci.is_file():
|
|
138
|
+
for line in net_file_ci.read_text().strip().splitlines():
|
|
139
|
+
k, v = line.split("=", 1)
|
|
140
|
+
net_cfg_ci[k] = v
|
|
141
|
+
tz_file_ci = container_dir / "kento-tz"
|
|
142
|
+
env_file_ci = container_dir / "kento-env"
|
|
143
|
+
ci_ssh_keys = None
|
|
144
|
+
auth_keys_file = container_dir / "kento-authorized-keys"
|
|
145
|
+
if auth_keys_file.is_file():
|
|
146
|
+
ci_ssh_keys = auth_keys_file.read_text()
|
|
147
|
+
ssh_user_file = container_dir / "kento-ssh-user"
|
|
148
|
+
ci_ssh_user = ssh_user_file.read_text().strip() if ssh_user_file.is_file() else "root"
|
|
149
|
+
ci_host_key_dir = container_dir / "ssh-host-keys"
|
|
150
|
+
name_file_ci = container_dir / "kento-name"
|
|
151
|
+
write_seed(
|
|
152
|
+
container_dir,
|
|
153
|
+
name=name_file_ci.read_text().strip() if name_file_ci.is_file() else name,
|
|
154
|
+
ip=net_cfg_ci.get("ip"),
|
|
155
|
+
gateway=net_cfg_ci.get("gateway"),
|
|
156
|
+
dns=net_cfg_ci.get("dns"),
|
|
157
|
+
searchdomain=net_cfg_ci.get("searchdomain"),
|
|
158
|
+
timezone=tz_file_ci.read_text().strip() if tz_file_ci.is_file() else None,
|
|
159
|
+
env=env_file_ci.read_text().strip().splitlines() if env_file_ci.is_file() else None,
|
|
160
|
+
ssh_keys=ci_ssh_keys, ssh_key_user=ci_ssh_user,
|
|
161
|
+
ssh_host_key_dir=ci_host_key_dir if ci_host_key_dir.is_dir() else None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Persist the layers resolved up-front (before the writable layer was cleared).
|
|
165
|
+
(container_dir / "kento-layers").write_text(layers + "\n")
|
|
166
|
+
|
|
167
|
+
# Backfill the image-hold container if it's missing (self-heals guests
|
|
168
|
+
# created before the hold mechanism existed). Store-level, mode-agnostic.
|
|
169
|
+
hold_name = (container_dir / "kento-name").read_text().strip() \
|
|
170
|
+
if (container_dir / "kento-name").is_file() else name
|
|
171
|
+
ensure_image_hold(image, hold_name)
|
|
172
|
+
|
|
173
|
+
# Regenerate the mode-appropriate hook so fresh image paths land in it.
|
|
174
|
+
# Plain VM mode has no hook (QEMU starts from Python), but pve-vm does —
|
|
175
|
+
# and its hook shape (VMID $1 / PHASE $2) is incompatible with the LXC
|
|
176
|
+
# hook shape (uses $3 for hook type), so using the wrong writer here
|
|
177
|
+
# silently breaks `qm start` after scrub.
|
|
178
|
+
if mode == "pve-vm":
|
|
179
|
+
write_vm_hook(container_dir, layers, name, state_dir)
|
|
180
|
+
# Re-read memory: from the PVE qm config and rewrite the
|
|
181
|
+
# kento-managed args: line so the embedded memfd size= stays in
|
|
182
|
+
# sync. `qm set --memory N` updates memory: but leaves args:
|
|
183
|
+
# untouched, so without this the pre-start validator aborts on
|
|
184
|
+
# every start after a memory change.
|
|
185
|
+
vmid_file = container_dir / "kento-vmid"
|
|
186
|
+
if vmid_file.is_file():
|
|
187
|
+
from kento.pve import sync_qm_args_to_memory
|
|
188
|
+
try:
|
|
189
|
+
vmid = int(vmid_file.read_text().strip())
|
|
190
|
+
except ValueError:
|
|
191
|
+
vmid = None
|
|
192
|
+
if vmid is not None:
|
|
193
|
+
sync_qm_args_to_memory(vmid, container_dir)
|
|
194
|
+
elif mode != "vm":
|
|
195
|
+
write_hook(container_dir, layers, name, state_dir)
|
|
196
|
+
|
|
197
|
+
# PVE-LXC: regenerate snippets wrapper when the container has
|
|
198
|
+
# port/memory/cores metadata. The wrapper path is derived from the
|
|
199
|
+
# VMID (container_dir.name) and the kento-hook path, so this is
|
|
200
|
+
# idempotent — we rewrite the same file. Done for consistency in
|
|
201
|
+
# case the kento-hook path ever changes across upgrades.
|
|
202
|
+
if mode == "pve":
|
|
203
|
+
if any((container_dir / f).is_file()
|
|
204
|
+
for f in ("kento-port", "kento-memory", "kento-cores")):
|
|
205
|
+
from kento.lxc_hook import write_lxc_snippets_wrapper
|
|
206
|
+
write_lxc_snippets_wrapper(
|
|
207
|
+
int(container_dir.name),
|
|
208
|
+
container_dir / "kento-hook",
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
logger.info("Scrubbed: %s", name)
|
|
212
|
+
logger.info(" Writable layer cleared, layers re-resolved from image.")
|