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/cloudinit.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Cloud-init NoCloud seed generation for kento containers."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def detect_cloudinit(layers: str) -> bool:
|
|
9
|
+
"""Check if any layer in the colon-separated layer paths has cloud-init.
|
|
10
|
+
|
|
11
|
+
Layer paths are podman's overlay diff directories — each layer only
|
|
12
|
+
holds files that changed at that layer. cloud-init might be installed
|
|
13
|
+
in any layer, so we scan all of them for a broad set of markers.
|
|
14
|
+
"""
|
|
15
|
+
markers = [
|
|
16
|
+
"usr/bin/cloud-init",
|
|
17
|
+
"usr/sbin/cloud-init",
|
|
18
|
+
"etc/cloud/cloud.cfg",
|
|
19
|
+
"lib/systemd/system/cloud-init.service",
|
|
20
|
+
"usr/lib/systemd/system/cloud-init.service",
|
|
21
|
+
]
|
|
22
|
+
for layer_path in layers.split(":"):
|
|
23
|
+
layer = Path(layer_path)
|
|
24
|
+
for marker in markers:
|
|
25
|
+
if (layer / marker).exists():
|
|
26
|
+
return True
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_meta_data(name: str, instance_id: str) -> str:
|
|
31
|
+
"""Generate NoCloud meta-data content."""
|
|
32
|
+
return f"instance-id: {instance_id}\nlocal-hostname: {name}\n"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generate_user_data(*, timezone: str | None = None,
|
|
36
|
+
ssh_keys: str | None = None,
|
|
37
|
+
ssh_key_user: str = "root",
|
|
38
|
+
ssh_host_key_dir: Path | None = None,
|
|
39
|
+
env: list[str] | None = None) -> str:
|
|
40
|
+
"""Generate NoCloud user-data content.
|
|
41
|
+
|
|
42
|
+
ssh_keys is the concatenated authorized_keys content.
|
|
43
|
+
ssh_host_key_dir is the path to the container's ssh-host-keys/ dir.
|
|
44
|
+
"""
|
|
45
|
+
lines = ["#cloud-config"]
|
|
46
|
+
|
|
47
|
+
if timezone:
|
|
48
|
+
lines.append(f"timezone: {timezone}")
|
|
49
|
+
|
|
50
|
+
# SSH authorized keys
|
|
51
|
+
if ssh_keys:
|
|
52
|
+
keys = [k.strip() for k in ssh_keys.strip().splitlines()
|
|
53
|
+
if k.strip() and not k.strip().startswith("#")]
|
|
54
|
+
if keys:
|
|
55
|
+
if ssh_key_user == "root":
|
|
56
|
+
lines.append("ssh_authorized_keys:")
|
|
57
|
+
for key in keys:
|
|
58
|
+
lines.append(f" - {key}")
|
|
59
|
+
else:
|
|
60
|
+
lines.append("users:")
|
|
61
|
+
lines.append(f" - name: {ssh_key_user}")
|
|
62
|
+
lines.append(" ssh_authorized_keys:")
|
|
63
|
+
for key in keys:
|
|
64
|
+
lines.append(f" - {key}")
|
|
65
|
+
|
|
66
|
+
# SSH host keys
|
|
67
|
+
if ssh_host_key_dir and ssh_host_key_dir.is_dir():
|
|
68
|
+
host_key_lines = _generate_ssh_keys_section(ssh_host_key_dir)
|
|
69
|
+
if host_key_lines:
|
|
70
|
+
lines.append("ssh_keys:")
|
|
71
|
+
lines.extend(host_key_lines)
|
|
72
|
+
|
|
73
|
+
# Environment variables via write_files
|
|
74
|
+
if env:
|
|
75
|
+
lines.append("write_files:")
|
|
76
|
+
lines.append(" - path: /etc/environment")
|
|
77
|
+
lines.append(" content: |")
|
|
78
|
+
for e in env:
|
|
79
|
+
lines.append(f" {e}")
|
|
80
|
+
|
|
81
|
+
lines.append("")
|
|
82
|
+
return "\n".join(lines)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _generate_ssh_keys_section(key_dir: Path) -> list[str]:
|
|
86
|
+
"""Generate the ssh_keys cloud-config section from host key files."""
|
|
87
|
+
lines = []
|
|
88
|
+
for key_type in ("rsa", "ecdsa", "ed25519"):
|
|
89
|
+
priv_file = key_dir / f"ssh_host_{key_type}_key"
|
|
90
|
+
pub_file = key_dir / f"ssh_host_{key_type}_key.pub"
|
|
91
|
+
if priv_file.is_file():
|
|
92
|
+
priv_content = priv_file.read_text()
|
|
93
|
+
lines.append(f" {key_type}_private: |")
|
|
94
|
+
for line in priv_content.splitlines():
|
|
95
|
+
lines.append(f" {line}")
|
|
96
|
+
if pub_file.is_file():
|
|
97
|
+
pub_content = pub_file.read_text().strip()
|
|
98
|
+
lines.append(f" {key_type}_public: {pub_content}")
|
|
99
|
+
return lines
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def generate_network_config(*, ip: str | None = None,
|
|
103
|
+
gateway: str | None = None,
|
|
104
|
+
dns: str | None = None,
|
|
105
|
+
searchdomain: str | None = None) -> str | None:
|
|
106
|
+
"""Generate NoCloud network-config (v2 format). Returns None if no network config needed."""
|
|
107
|
+
if not ip and not dns and not searchdomain:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
lines = ["network:", " version: 2", " ethernets:", " all:",
|
|
111
|
+
" match:", ' name: "*"']
|
|
112
|
+
|
|
113
|
+
if ip:
|
|
114
|
+
lines.append(" addresses:")
|
|
115
|
+
lines.append(f" - {ip}")
|
|
116
|
+
if gateway:
|
|
117
|
+
lines.append(" routes:")
|
|
118
|
+
lines.append(" - to: default")
|
|
119
|
+
lines.append(f" via: {gateway}")
|
|
120
|
+
else:
|
|
121
|
+
lines.append(" dhcp4: true")
|
|
122
|
+
|
|
123
|
+
if dns or searchdomain:
|
|
124
|
+
lines.append(" nameservers:")
|
|
125
|
+
if dns:
|
|
126
|
+
lines.append(" addresses:")
|
|
127
|
+
lines.append(f" - {dns}")
|
|
128
|
+
if searchdomain:
|
|
129
|
+
lines.append(" search:")
|
|
130
|
+
lines.append(f" - {searchdomain}")
|
|
131
|
+
|
|
132
|
+
lines.append("")
|
|
133
|
+
return "\n".join(lines)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def compute_instance_id(name: str, *, ip: str | None = None,
|
|
137
|
+
gateway: str | None = None,
|
|
138
|
+
dns: str | None = None,
|
|
139
|
+
searchdomain: str | None = None,
|
|
140
|
+
timezone: str | None = None,
|
|
141
|
+
env: list[str] | None = None,
|
|
142
|
+
ssh_key_user: str = "root",
|
|
143
|
+
has_ssh_keys: bool = False,
|
|
144
|
+
has_ssh_host_keys: bool = False) -> str:
|
|
145
|
+
"""Compute a content-hash instance-id from configuration.
|
|
146
|
+
|
|
147
|
+
When config changes, the hash changes, causing cloud-init to re-run.
|
|
148
|
+
"""
|
|
149
|
+
config = {
|
|
150
|
+
"name": name,
|
|
151
|
+
"ip": ip,
|
|
152
|
+
"gateway": gateway,
|
|
153
|
+
"dns": dns,
|
|
154
|
+
"searchdomain": searchdomain,
|
|
155
|
+
"timezone": timezone,
|
|
156
|
+
"env": sorted(env) if env else None,
|
|
157
|
+
"ssh_key_user": ssh_key_user,
|
|
158
|
+
"has_ssh_keys": has_ssh_keys,
|
|
159
|
+
"has_ssh_host_keys": has_ssh_host_keys,
|
|
160
|
+
}
|
|
161
|
+
canonical = json.dumps(config, sort_keys=True, separators=(",", ":"))
|
|
162
|
+
h = hashlib.sha256(canonical.encode()).hexdigest()[:16]
|
|
163
|
+
return f"kento-{h}"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def write_seed(container_dir: Path, *, name: str,
|
|
167
|
+
ip: str | None = None, gateway: str | None = None,
|
|
168
|
+
dns: str | None = None, searchdomain: str | None = None,
|
|
169
|
+
timezone: str | None = None, env: list[str] | None = None,
|
|
170
|
+
ssh_keys: str | None = None, ssh_key_user: str = "root",
|
|
171
|
+
ssh_host_key_dir: Path | None = None) -> None:
|
|
172
|
+
"""Generate and write NoCloud seed files to container_dir/cloud-seed/."""
|
|
173
|
+
seed_dir = container_dir / "cloud-seed"
|
|
174
|
+
seed_dir.mkdir(parents=True, exist_ok=True)
|
|
175
|
+
|
|
176
|
+
# Compute instance-id
|
|
177
|
+
iid = compute_instance_id(
|
|
178
|
+
name, ip=ip, gateway=gateway, dns=dns, searchdomain=searchdomain,
|
|
179
|
+
timezone=timezone, env=env, ssh_key_user=ssh_key_user,
|
|
180
|
+
has_ssh_keys=bool(ssh_keys),
|
|
181
|
+
has_ssh_host_keys=bool(ssh_host_key_dir and ssh_host_key_dir.is_dir()),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# meta-data
|
|
185
|
+
(seed_dir / "meta-data").write_text(generate_meta_data(name, iid))
|
|
186
|
+
|
|
187
|
+
# user-data
|
|
188
|
+
user_data = generate_user_data(
|
|
189
|
+
timezone=timezone, ssh_keys=ssh_keys, ssh_key_user=ssh_key_user,
|
|
190
|
+
ssh_host_key_dir=ssh_host_key_dir, env=env,
|
|
191
|
+
)
|
|
192
|
+
(seed_dir / "user-data").write_text(user_data)
|
|
193
|
+
|
|
194
|
+
# network-config (optional)
|
|
195
|
+
net_config = generate_network_config(ip=ip, gateway=gateway, dns=dns,
|
|
196
|
+
searchdomain=searchdomain)
|
|
197
|
+
if net_config:
|
|
198
|
+
(seed_dir / "network-config").write_text(net_config)
|
|
199
|
+
else:
|
|
200
|
+
# Remove stale network-config if it existed from a prior scrub
|
|
201
|
+
(seed_dir / "network-config").unlink(missing_ok=True)
|