Files
virtuality/web/cloud_images.py
T
Claude 9e54614869 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
2026-08-15 19:14:53 +00:00

163 строки
5.8 KiB
Python

#!/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"