From d5c957f0f7f1a5a35e946794b24936ebedd0a094 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: Wed, 6 May 2026 21:54:06 +0900 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B4=D0=B5=D1=82=D0=B5=D0=BA=D1=82=D0=BE=D1=80=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20=D1=85=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/host_profile.py | 133 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 web/host_profile.py diff --git a/web/host_profile.py b/web/host_profile.py new file mode 100644 index 0000000..34bb325 --- /dev/null +++ b/web/host_profile.py @@ -0,0 +1,133 @@ +import json +import platform +import re +import subprocess +from pathlib import Path +from typing import Any + +PROFILE_FILE = Path('/var/lib/virtuality/config/host_profile.json') + + +def run_cmd(cmd: list[str], timeout: int = 5) -> dict[str, Any]: + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, check=False) + return {'ok': result.returncode == 0, 'stdout': result.stdout.strip(), 'stderr': result.stderr.strip(), 'code': result.returncode} + except Exception as exc: + return {'ok': False, 'stdout': '', 'stderr': str(exc), 'code': -1} + + +def read_text(path: str) -> str: + try: + return Path(path).read_text(errors='ignore').strip('\x00\n ') + except Exception: + return '' + + +def detect_board_model() -> str: + candidates = [ + '/proc/device-tree/model', + '/sys/firmware/devicetree/base/model', + ] + for path in candidates: + value = read_text(path) + if value: + return value + cpuinfo = read_text('/proc/cpuinfo') + for line in cpuinfo.splitlines(): + if line.lower().startswith(('model', 'hardware')) and ':' in line: + return line.split(':', 1)[1].strip() + return 'unknown' + + +def classify_profile(arch: str, model: str) -> dict[str, str]: + low = model.lower() + if arch in ('aarch64', 'arm64'): + if 'raspberry pi' in low: + return { + 'profile': 'raspberry-arm64', + 'label': 'Raspberry Pi ARM64 Edge Node', + 'recommended_network': 'nat', + 'recommended_guest_arch': 'aarch64', + 'recommended_vm_mode': 'arm64-cloud-image', + } + if 'orange pi 5' in low or 'orangepi 5' in low or 'rk3588' in low or 'rockchip' in low: + return { + 'profile': 'orangepi5-arm64', + 'label': 'Orange Pi 5 ARM64 Edge Node', + 'recommended_network': 'nat', + 'recommended_guest_arch': 'aarch64', + 'recommended_vm_mode': 'arm64-cloud-image', + } + return { + 'profile': 'generic-arm64', + 'label': 'Generic ARM64 Edge Node', + 'recommended_network': 'nat', + 'recommended_guest_arch': 'aarch64', + 'recommended_vm_mode': 'arm64-cloud-image', + } + return { + 'profile': 'x86_64', + 'label': 'x86_64 KVM Node', + 'recommended_network': 'bridge', + 'recommended_guest_arch': 'x86_64', + 'recommended_vm_mode': 'x86_64-iso', + } + + +def command_exists(name: str) -> bool: + return run_cmd(['sh', '-lc', f'command -v {name} >/dev/null 2>&1'])['ok'] + + +def detect_host_profile() -> dict[str, Any]: + arch = platform.machine() or run_cmd(['uname', '-m'])['stdout'] or 'unknown' + model = detect_board_model() + base = classify_profile(arch, model) + is_arm = arch in ('aarch64', 'arm64') + data: dict[str, Any] = { + 'arch': arch, + 'model': model, + 'profile': base['profile'], + 'label': base['label'], + 'recommended_network': base['recommended_network'], + 'recommended_guest_arch': base['recommended_guest_arch'], + 'recommended_vm_mode': base['recommended_vm_mode'], + 'kvm_device': Path('/dev/kvm').exists(), + 'qemu_system_x86_64': command_exists('qemu-system-x86_64'), + 'qemu_system_aarch64': command_exists('qemu-system-aarch64'), + 'virt_install': command_exists('virt-install'), + 'nft': command_exists('nft'), + 'cloud_image_utils': command_exists('cloud-localds'), + 'uefi_aarch64_hint': any(Path(path).exists() for path in [ + '/usr/share/AAVMF/AAVMF_CODE.fd', + '/usr/share/qemu-efi-aarch64/QEMU_EFI.fd', + '/usr/share/AAVMF/AAVMF_VARS.fd', + ]), + } + checks = [] + checks.append({'name': '/dev/kvm', 'ok': data['kvm_device'], 'hint': 'Нужен KVM. На ARM-платах проверь ядро и виртуализацию.'}) + if is_arm: + checks.append({'name': 'qemu-system-aarch64', 'ok': data['qemu_system_aarch64'], 'hint': 'Пакет qemu-system-arm / qemu-system-aarch64.'}) + checks.append({'name': 'AArch64 UEFI', 'ok': data['uefi_aarch64_hint'], 'hint': 'Пакет qemu-efi-aarch64 или AAVMF.'}) + else: + checks.append({'name': 'qemu-system-x86_64', 'ok': data['qemu_system_x86_64'], 'hint': 'Пакет qemu-system-x86.'}) + checks.append({'name': 'virt-install', 'ok': data['virt_install'], 'hint': 'Пакет virtinst.'}) + checks.append({'name': 'nftables', 'ok': data['nft'], 'hint': 'Пакет nftables для NAT port forwarding.'}) + data['checks'] = checks + data['ready'] = all(item['ok'] for item in checks if item['name'] != 'AArch64 UEFI') + return data + + +def save_host_profile(profile: dict[str, Any]) -> None: + PROFILE_FILE.parent.mkdir(parents=True, exist_ok=True) + PROFILE_FILE.write_text(json.dumps(profile, ensure_ascii=False, indent=2)) + + +def load_host_profile() -> dict[str, Any]: + if PROFILE_FILE.exists(): + try: + return json.loads(PROFILE_FILE.read_text()) + except Exception: + pass + profile = detect_host_profile() + save_host_profile(profile) + return profile