Cloud-образы, клонирование VM и шаблоны были добавлены
Каталог официальных cloud-образов Ubuntu/Debian (x86_64 и aarch64) со скачиванием в фоне, страница /cloud-images, создание VM из cloud-образа с настройкой пользователя, пароля и SSH-ключа через cloud-init seed, клонирование VM через virt-clone и флаг шаблона с блокировкой запуска. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Каталог cloud-образов и генерация cloud-init seed для быстрых VM."""
|
||||
import json
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
CLOUD_IMAGES_DIR = Path("/var/lib/virtuality/cloud-images")
|
||||
|
||||
# Официальные cloud-образы. Для каждого дистрибутива — обе архитектуры,
|
||||
# чтобы Raspberry/Orange Pi качали arm64 без ручного выбора URL.
|
||||
CATALOG: list[dict[str, str]] = [
|
||||
{
|
||||
"key": "ubuntu-24.04-x86_64",
|
||||
"label": "Ubuntu Server 24.04 LTS",
|
||||
"arch": "x86_64",
|
||||
"filename": "ubuntu-24.04-x86_64.qcow2",
|
||||
"url": "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-amd64.img",
|
||||
"default_user": "ubuntu",
|
||||
},
|
||||
{
|
||||
"key": "ubuntu-24.04-aarch64",
|
||||
"label": "Ubuntu Server 24.04 LTS",
|
||||
"arch": "aarch64",
|
||||
"filename": "ubuntu-24.04-aarch64.qcow2",
|
||||
"url": "https://cloud-images.ubuntu.com/noble/current/noble-server-cloudimg-arm64.img",
|
||||
"default_user": "ubuntu",
|
||||
},
|
||||
{
|
||||
"key": "ubuntu-22.04-x86_64",
|
||||
"label": "Ubuntu Server 22.04 LTS",
|
||||
"arch": "x86_64",
|
||||
"filename": "ubuntu-22.04-x86_64.qcow2",
|
||||
"url": "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-amd64.img",
|
||||
"default_user": "ubuntu",
|
||||
},
|
||||
{
|
||||
"key": "ubuntu-22.04-aarch64",
|
||||
"label": "Ubuntu Server 22.04 LTS",
|
||||
"arch": "aarch64",
|
||||
"filename": "ubuntu-22.04-aarch64.qcow2",
|
||||
"url": "https://cloud-images.ubuntu.com/jammy/current/jammy-server-cloudimg-arm64.img",
|
||||
"default_user": "ubuntu",
|
||||
},
|
||||
{
|
||||
"key": "debian-12-x86_64",
|
||||
"label": "Debian 12 Bookworm",
|
||||
"arch": "x86_64",
|
||||
"filename": "debian-12-x86_64.qcow2",
|
||||
"url": "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-amd64.qcow2",
|
||||
"default_user": "debian",
|
||||
},
|
||||
{
|
||||
"key": "debian-12-aarch64",
|
||||
"label": "Debian 12 Bookworm",
|
||||
"arch": "aarch64",
|
||||
"filename": "debian-12-aarch64.qcow2",
|
||||
"url": "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-arm64.qcow2",
|
||||
"default_user": "debian",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def catalog_entry(key: str) -> dict[str, str] | None:
|
||||
for entry in CATALOG:
|
||||
if entry["key"] == key:
|
||||
return entry
|
||||
return None
|
||||
|
||||
|
||||
def catalog_for_arch(host_arch: str) -> list[dict[str, Any]]:
|
||||
"""Каталог с отметкой native — образы родной архитектуры хоста показываются первыми."""
|
||||
native = "aarch64" if host_arch in ("aarch64", "arm64") else "x86_64"
|
||||
entries = [dict(entry, native=entry["arch"] == native, downloaded=(CLOUD_IMAGES_DIR / entry["filename"]).exists()) for entry in CATALOG]
|
||||
return sorted(entries, key=lambda item: (not item["native"], item["label"]))
|
||||
|
||||
|
||||
def safe_image_filename(filename: str) -> str | None:
|
||||
name = Path(filename or "").name.strip()
|
||||
if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_.-]{1,180}\.(qcow2|img)", name):
|
||||
return None
|
||||
return name
|
||||
|
||||
|
||||
def image_path_by_name(name: str) -> Path | None:
|
||||
safe_name = safe_image_filename(name)
|
||||
if not safe_name:
|
||||
return None
|
||||
path = (CLOUD_IMAGES_DIR / safe_name).resolve()
|
||||
if CLOUD_IMAGES_DIR.resolve() not in path.parents:
|
||||
return None
|
||||
return path
|
||||
|
||||
|
||||
def list_images() -> list[dict[str, str]]:
|
||||
CLOUD_IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
files = []
|
||||
for item in sorted(CLOUD_IMAGES_DIR.glob("*")):
|
||||
if item.suffix.lower() not in (".qcow2", ".img") or item.name.endswith(".part"):
|
||||
continue
|
||||
try:
|
||||
stat = item.stat()
|
||||
size_mb = stat.st_size / 1024 / 1024
|
||||
updated = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M")
|
||||
except OSError:
|
||||
size_mb = 0
|
||||
updated = "unknown"
|
||||
known = next((entry for entry in CATALOG if entry["filename"] == item.name), None)
|
||||
files.append({
|
||||
"name": item.name,
|
||||
"path": str(item),
|
||||
"size": f"{size_mb:.0f} MB",
|
||||
"updated": updated,
|
||||
"label": known["label"] if known else item.stem,
|
||||
"arch": known["arch"] if known else ("aarch64" if "arm64" in item.name or "aarch64" in item.name else "x86_64"),
|
||||
"default_user": known["default_user"] if known else "user",
|
||||
})
|
||||
return files
|
||||
|
||||
|
||||
def valid_cloud_username(value: str) -> bool:
|
||||
return bool(re.fullmatch(r"[a-z_][a-z0-9_-]{0,31}", value or ""))
|
||||
|
||||
|
||||
def valid_ssh_key(value: str) -> bool:
|
||||
if not value:
|
||||
return True
|
||||
return bool(re.fullmatch(r"(ssh-(rsa|ed25519|dss)|ecdsa-sha2-[a-z0-9-]+) [A-Za-z0-9+/=]+( [^\r\n]{0,256})?", value.strip()))
|
||||
|
||||
|
||||
def build_user_data(hostname: str, username: str, password: str, ssh_key: str) -> str:
|
||||
lines = [
|
||||
"#cloud-config",
|
||||
f"hostname: {hostname}",
|
||||
"manage_etc_hosts: true",
|
||||
f"ssh_pwauth: {'true' if password else 'false'}",
|
||||
"users:",
|
||||
f" - name: {username}",
|
||||
" groups: [sudo]",
|
||||
" shell: /bin/bash",
|
||||
' sudo: "ALL=(ALL) NOPASSWD:ALL"',
|
||||
" lock_passwd: false",
|
||||
]
|
||||
if ssh_key:
|
||||
lines.append(" ssh_authorized_keys:")
|
||||
lines.append(f" - {ssh_key.strip()}")
|
||||
if password:
|
||||
lines += [
|
||||
"chpasswd:",
|
||||
" expire: false",
|
||||
" users:",
|
||||
f" - name: {username}",
|
||||
f" password: {json.dumps(password, ensure_ascii=False)}",
|
||||
" type: text",
|
||||
]
|
||||
lines.append("package_update: false")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def build_meta_data(name: str) -> str:
|
||||
return f"instance-id: virtuality-{name}\nlocal-hostname: {name}\n"
|
||||
Ссылка в новой задаче
Block a user