From 1e9631b5dd1a71dd93adf7540e85cae944ecbcb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=92=D0=B8=D0=BA=D1=82=D0=BE=D1=80?= <78488229+viktor138irk@users.noreply.github.com> Date: Thu, 7 May 2026 06:14:34 +0900 Subject: [PATCH] =?UTF-8?q?=D0=92=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B0=D1=80?= =?UTF-8?q?=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D1=8B=20VM=20?= =?UTF-8?q?=D0=B1=D1=8B=D0=BB=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=20=D0=B2=20=D1=83=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=BD=D0=BE=D0=B2=D0=BE=D1=87=D0=BD=D0=BE=D0=BC=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=82=D1=87=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/patch_disk_images.py | 111 ++++++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 29 deletions(-) diff --git a/scripts/patch_disk_images.py b/scripts/patch_disk_images.py index 21eb315..a91a4be 100644 --- a/scripts/patch_disk_images.py +++ b/scripts/patch_disk_images.py @@ -9,6 +9,7 @@ if not app_path.exists(): text = app_path.read_text() changed = [] +warnings = [] if 'DISK_IMAGES_DIR = Path("/var/lib/virtuality/disk-images")' not in text: text = text.replace('IMAGES_DIR = Path("/var/lib/virtuality/images")\n', 'IMAGES_DIR = Path("/var/lib/virtuality/images")\nDISK_IMAGES_DIR = Path("/var/lib/virtuality/disk-images")\n', 1) @@ -18,6 +19,28 @@ else: helpers = r''' +def vm_arch_options() -> list[dict[str, str]]: + return [ + {"value": "auto", "label": "Auto — по профилю хоста"}, + {"value": "x86_64", "label": "x86_64 / amd64"}, + {"value": "aarch64", "label": "ARM64 / aarch64"}, + {"value": "generic", "label": "Generic / no arch override"}, + ] + + +def normalize_guest_arch(value: str, profile: dict[str, Any]) -> str: + value = (value or "auto").strip() + if value == "auto": + return str(profile.get("recommended_guest_arch") or "x86_64") + if value in ("x86_64", "amd64"): + return "x86_64" + if value in ("aarch64", "arm64"): + return "aarch64" + if value == "generic": + return "generic" + return str(profile.get("recommended_guest_arch") or "x86_64") + + def list_disk_image_files() -> list[dict[str, str]]: DISK_IMAGES_DIR.mkdir(parents=True, exist_ok=True) files = [] @@ -71,21 +94,33 @@ if 'def list_disk_image_files() -> list[dict[str, str]]:' not in text: if marker not in text: raise SystemExit('valid_vm_name marker not found') text = text.replace(marker, helpers + marker, 1) - changed.append('disk image helpers added') + changed.append('disk image and architecture helpers added') else: + if 'def vm_arch_options()' not in text: + insert = helpers.split('\n\ndef list_disk_image_files()', 1)[0] + marker = '\n\ndef list_disk_image_files() -> list[dict[str, str]]:' + text = text.replace(marker, insert + marker, 1) + changed.append('architecture helpers added') + else: + changed.append('architecture helpers already present') changed.append('disk image helpers already present') text = text.replace('\n\ndef bridge_exists(name: str) -> bool:\n if not name or not re.fullmatch(r"[a-zA-Z0-9_.:-]+", name):\n return False\n return run_cmd(["ip", "link", "show", name], timeout=5)["ok"]\n\n\ndef bridge_exists(name: str) -> bool:\n if not name or not re.fullmatch(r"[a-zA-Z0-9_.:-]+", name):\n return False\n return run_cmd(["ip", "link", "show", name], timeout=5)["ok"]\n', '\n\ndef bridge_exists(name: str) -> bool:\n if not name or not re.fullmatch(r"[a-zA-Z0-9_.:-]+", name):\n return False\n return run_cmd(["ip", "link", "show", name], timeout=5)["ok"]\n') +# Upgrade vm_form_context robustly. old_context = '"isos": list_iso_files(), "error": error, "profile": profile, "form": form or {"memory": 2048, "vcpus": 2, "disk_size": 20, "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}' -new_context = '"isos": list_iso_files(), "disk_images": list_disk_image_files(), "error": error, "profile": profile, "form": form or {"memory": 2048, "vcpus": 2, "disk_size": 20, "source_type": "iso", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}' +new_context = '"isos": list_iso_files(), "disk_images": list_disk_image_files(), "arch_options": vm_arch_options(), "error": error, "profile": profile, "form": form or {"memory": 2048, "vcpus": 2, "disk_size": 20, "source_type": "iso", "guest_arch": "auto", "network_mode": default_mode, "bridge": DEFAULT_BRIDGE}}' if old_context in text: text = text.replace(old_context, new_context, 1) - changed.append('vm form context gets disk images') -elif '"disk_images": list_disk_image_files()' in text: - changed.append('vm form context already has disk images') + changed.append('vm form context gets disk images and arch options') +elif '"disk_images": list_disk_image_files()' in text and '"arch_options": vm_arch_options()' not in text: + text = text.replace('"disk_images": list_disk_image_files(),', '"disk_images": list_disk_image_files(), "arch_options": vm_arch_options(),') + text = text.replace('"source_type": "iso", "network_mode"', '"source_type": "iso", "guest_arch": "auto", "network_mode"') + changed.append('vm form context upgraded with arch options') +elif '"arch_options": vm_arch_options()' in text: + changed.append('vm form context already has arch options') else: - raise SystemExit('vm_form_context marker not found') + warnings.append('vm_form_context marker not found') routes = r''' @@ -141,20 +176,26 @@ else: changed.append('disk image routes already present') old_sig = 'def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), iso_path: str = Form(...), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE)):' -new_sig = 'def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE)):' +new_sig = 'def vm_create_submit(request: Request, name: str = Form(...), memory: int = Form(...), vcpus: int = Form(...), disk_size: int = Form(...), iso_path: str = Form(""), disk_image_path: str = Form(""), source_type: str = Form("iso"), guest_arch: str = Form("auto"), network_mode: str = Form("nat"), bridge: str = Form(DEFAULT_BRIDGE)):' if old_sig in text: text = text.replace(old_sig, new_sig, 1) - changed.append('vm create signature supports disk images') -elif new_sig in text: - changed.append('vm create signature already supports disk images') + changed.append('vm create signature supports disk images and guest arch') +elif 'source_type: str = Form("iso")' in text and 'guest_arch: str = Form("auto")' not in text: + text = text.replace('source_type: str = Form("iso"), network_mode:', 'source_type: str = Form("iso"), guest_arch: str = Form("auto"), network_mode:', 1) + changed.append('vm create signature upgraded with guest arch') +elif 'guest_arch: str = Form("auto")' in text: + changed.append('vm create signature already supports guest arch') else: - raise SystemExit('vm_create_submit signature marker not found') + warnings.append('vm_create_submit signature marker not found') old_body = 'form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "network_mode": network_mode, "bridge": bridge}' -new_body = 'form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "network_mode": network_mode, "bridge": bridge}' +new_body = 'form = {"name": name, "memory": memory, "vcpus": vcpus, "disk_size": disk_size, "iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "guest_arch": guest_arch, "network_mode": network_mode, "bridge": bridge}' if old_body in text: text = text.replace(old_body, new_body, 1) - changed.append('vm create form state supports source type') + changed.append('vm create form state supports source type and guest arch') +elif '"source_type": source_type' in text and '"guest_arch": guest_arch' not in text: + text = text.replace('"source_type": source_type, "network_mode"', '"source_type": source_type, "guest_arch": guest_arch, "network_mode"') + changed.append('vm create form state upgraded with guest arch') old_iso_validation = ''' else: iso = Path(iso_path).resolve() @@ -163,6 +204,8 @@ old_iso_validation = ''' else: ''' new_iso_validation = ''' elif source_type not in ("iso", "disk_image"): error = "Некорректный источник VM." + elif guest_arch not in ("auto", "x86_64", "aarch64", "generic"): + error = "Некорректная архитектура VM." elif network_mode == "bridge" and not bridge_exists(bridge): error = f"Bridge {bridge} не найден на сервере. Для VPS выбери режим NAT Router — virtuality-nat, либо сначала создай bridge {bridge}." else: @@ -177,11 +220,12 @@ new_iso_validation = ''' elif source_type not in ("iso", "disk_image"): ''' if old_iso_validation in text: text = text.replace(old_iso_validation, new_iso_validation, 1) - changed.append('vm create validation supports disk image source') -elif 'source_type == "iso"' in text and 'DISK_IMAGES_DIR.resolve()' in text: - changed.append('vm create validation already supports disk image source') -else: - raise SystemExit('vm validation marker not found') + changed.append('vm create validation supports disk image source and guest arch') +elif 'guest_arch not in' not in text and 'source_type == "iso"' in text: + text = text.replace('elif network_mode not in ("nat", "bridge"):\n error = "Некорректный режим сети."', 'elif network_mode not in ("nat", "bridge"):\n error = "Некорректный режим сети."\n elif guest_arch not in ("auto", "x86_64", "aarch64", "generic"):\n error = "Некорректная архитектура VM."', 1) + changed.append('vm create validation upgraded with guest arch') +elif 'guest_arch not in' in text: + changed.append('vm create validation already supports guest arch') old_cmd = ''' IMAGES_DIR.mkdir(parents=True, exist_ok=True) disk_path = IMAGES_DIR / f"{name}.qcow2" @@ -202,11 +246,14 @@ new_cmd = ''' IMAGES_DIR.mkdir(parents=True, exist_ok=True) return vm_form_context(request, error=f"Диск уже существует: {disk_path}", form=form, status_code=400) profile = host_profile.load_host_profile() - is_arm = profile.get("recommended_guest_arch") == "aarch64" + selected_arch = normalize_guest_arch(guest_arch, profile) + is_arm = selected_arch == "aarch64" virt_type = "kvm" if profile.get("kvm_device") else "qemu" network_arg = f"network={network_core.NETWORK_NAME},model=virtio" if network_mode == "nat" else f"bridge={bridge},model=virtio" cmd = ["virt-install", "--name", name, "--memory", str(memory), "--vcpus", str(vcpus), "--virt-type", virt_type] - if is_arm: + if selected_arch == "x86_64": + cmd += ["--arch", "x86_64"] + elif is_arm: cmd += ["--arch", "aarch64", "--machine", "virt", "--cpu", "host" if virt_type == "kvm" else "cortex-a57", "--boot", "uefi"] if source_type == "disk_image": @@ -220,20 +267,26 @@ new_cmd = ''' IMAGES_DIR.mkdir(parents=True, exist_ok=True) ''' if old_cmd in text: text = text.replace(old_cmd, new_cmd, 1) - changed.append('vm create command supports qemu fallback and disk image import') + changed.append('vm create command supports qemu fallback, guest arch and disk image import') +elif 'selected_arch = normalize_guest_arch' in text: + changed.append('vm create command already supports guest arch') elif 'virt_type = "kvm" if profile.get("kvm_device") else "qemu"' in text: - changed.append('vm create command already supports qemu fallback') -elif 'source_type == "disk_image"' in text and 'qemu-img convert' in text: - # Upgrade existing disk-image-aware command to qemu fallback in-place. - text = text.replace('cmd = ["virt-install", "--name", name, "--memory", str(memory), "--vcpus", str(vcpus)]', 'virt_type = "kvm" if profile.get("kvm_device") else "qemu"\n cmd = ["virt-install", "--name", name, "--memory", str(memory), "--vcpus", str(vcpus), "--virt-type", virt_type]', 1) - text = text.replace('["--arch", "aarch64", "--machine", "virt", "--cpu", "host", "--virt-type", "kvm", "--boot", "uefi"]', '["--arch", "aarch64", "--machine", "virt", "--cpu", "host" if virt_type == "kvm" else "cortex-a57", "--boot", "uefi"]', 1) - changed.append('existing vm create command was upgraded with qemu fallback') + # Upgrade existing qemu fallback block to use selected guest arch. + text = text.replace('is_arm = profile.get("recommended_guest_arch") == "aarch64"\n virt_type =', 'selected_arch = normalize_guest_arch(guest_arch, profile)\n is_arm = selected_arch == "aarch64"\n virt_type =', 1) + text = text.replace('if is_arm:\n cmd += ["--arch", "aarch64", "--machine", "virt", "--cpu", "host" if virt_type == "kvm" else "cortex-a57", "--boot", "uefi"]', 'if selected_arch == "x86_64":\n cmd += ["--arch", "x86_64"]\n elif is_arm:\n cmd += ["--arch", "aarch64", "--machine", "virt", "--cpu", "host" if virt_type == "kvm" else "cortex-a57", "--boot", "uefi"]', 1) + changed.append('existing vm create command was upgraded with guest arch') else: - raise SystemExit('vm command marker not found') + warnings.append('vm command marker not found') -text = text.replace('"iso_path": iso_path, "host_profile"', '"iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "host_profile"') +# Ensure operation metadata keeps selected arch and source. +if '"guest_arch": selected_arch' not in text and '"guest_arch": profile.get("recommended_guest_arch")' in text: + text = text.replace('"guest_arch": profile.get("recommended_guest_arch")', '"guest_arch": selected_arch') +if '"disk_image_path": disk_image_path' not in text: + text = text.replace('"iso_path": iso_path, "host_profile"', '"iso_path": iso_path, "disk_image_path": disk_image_path, "source_type": source_type, "guest_arch": selected_arch, "host_profile"') app_path.write_text(text) print('disk images patch applied:') for item in changed: print(f'- {item}') +for item in warnings: + print(f'WARN: {item}')