From d83c6146c833baaba3788d9e10487181f7cad476 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 04:53:14 +0900 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B0=D1=82=D1=87=20IP-=D0=B0=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D0=BE=D0=B2=20VM=20=D0=B1=D1=8B=D0=BB=20?= =?UTF-8?q?=D1=83=D0=BF=D1=80=D0=BE=D1=89=D1=91=D0=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/patch_dhcp_leases_empty.py | 267 +++++++---------------------- 1 file changed, 61 insertions(+), 206 deletions(-) diff --git a/scripts/patch_dhcp_leases_empty.py b/scripts/patch_dhcp_leases_empty.py index 176993f..61ddff9 100644 --- a/scripts/patch_dhcp_leases_empty.py +++ b/scripts/patch_dhcp_leases_empty.py @@ -7,241 +7,96 @@ app_path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path('/opt/virtuality/web if not app_path.exists(): raise SystemExit(f'app.py not found: {app_path}') -template_path = app_path.parent / 'templates' / 'network.html' dashboard_template_path = app_path.parent / 'templates' / 'dashboard.html' -if not template_path.exists(): - raise SystemExit(f'network.html not found: {template_path}') if not dashboard_template_path.exists(): raise SystemExit(f'dashboard.html not found: {dashboard_template_path}') changed: list[str] = [] text = app_path.read_text(encoding='utf-8') -helpers = r''' - -def parse_dhcp_leases_output(output: str) -> list[dict[str, str]]: - leases: list[dict[str, str]] = [] - for line in (output or '').splitlines(): - raw = line.strip() - if not raw or raw.startswith('Expiry') or raw.startswith('-'): - continue - parts = raw.split() - if len(parts) < 5: - continue - leases.append({ - 'expiry': ' '.join(parts[0:2]), - 'mac': parts[2].lower(), - 'protocol': parts[3], - 'ip': parts[4].split('/')[0], - 'ip_with_prefix': parts[4], - 'hostname': parts[5] if len(parts) > 5 else '—', - 'client_id': parts[6] if len(parts) > 6 else '—', - }) - return leases - - -def dhcp_leases_hint(info: dict[str, Any], leases: list[dict[str, str]]) -> str: - if leases: - return 'DHCP leases найдены. IP можно использовать для проброса портов.' - if not info.get('exists'): - return 'NAT-сеть ещё не создана. Нажми «Создать / починить NAT-сеть».' - info_text = str(info.get('info', '')) - if 'Active: yes' not in info_text: - return 'NAT-сеть есть, но не активна. Нажми «Создать / починить NAT-сеть» или проверь virsh net-start virtuality-nat.' - return 'Leases пусты: запусти VM с сетью virtuality-nat, дождись загрузки ОС и DHCP. У статических IP и некоторых готовых образов lease может не появиться.' - - -def vm_mac_addresses(vm_name: str) -> list[str]: - result = run_cmd(['virsh', 'domiflist', vm_name], timeout=8) - if not result.get('ok'): - return [] - macs: list[str] = [] - for line in result.get('stdout', '').splitlines(): - match = re.search(r'([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}', line) - if match: - macs.append(match.group(0).lower()) - return macs - - -def resolve_vm_ip_for_table(vm_name: str) -> str: - if not vm_name: - return '—' - - try: - result = run_cmd(['virsh', 'domifaddr', vm_name], timeout=8) - if result.get('ok'): - for ip in re.findall(r'\b(\d{1,3}(?:\.\d{1,3}){3})/\d+', result.get('stdout', '')): - if not ip.startswith('127.') and not ip.startswith('169.254.'): - return ip - except Exception: - pass - - try: - resolved = network_core.resolve_vm_ip(vm_name) - if resolved: - return str(resolved) - except Exception: - pass - - try: - leases = run_cmd(['virsh', 'net-dhcp-leases', network_core.NETWORK_NAME], timeout=8) - if not leases.get('ok'): - return '—' - macs = set(vm_mac_addresses(vm_name)) - for lease in parse_dhcp_leases_output(leases.get('stdout', '')): - if macs and lease.get('mac', '').lower() not in macs: - continue - ip = lease.get('ip') or '' - if ip: - return ip - except Exception: - pass - - return '—' - - -def enrich_vms_with_ips(vms: list[dict[str, str]]) -> list[dict[str, str]]: - enriched: list[dict[str, str]] = [] - for vm in vms: - item = dict(vm) - item['ip'] = resolve_vm_ip_for_table(item.get('name', '')) if item.get('name') else '—' - enriched.append(item) - return enriched -''' - -# Remove older helper versions from previous patches to avoid stale NETWORK_NAME references. -text = re.sub( +# Remove helper blocks injected by older versions of this patch. They were too invasive +# and could leave stale references on servers that had several hotfixes applied. +text, removed_helpers = re.subn( r"\n\ndef parse_dhcp_leases_output\(output: str\).*?\n\ndef parse_virsh_list\(\) -> list\[dict\[str, str\]\]:", "\n\ndef parse_virsh_list() -> list[dict[str, str]]:", text, count=1, flags=re.S, ) +if removed_helpers: + changed.append('old VM IP helper block was removed') -marker = '\n\ndef parse_virsh_list() -> list[dict[str, str]]:' -if marker not in text: - raise SystemExit('parse_virsh_list marker not found') -text = text.replace(marker, helpers + marker, 1) -changed.append('VM IP helpers were injected before parse_virsh_list') +new_parse_virsh_list = '''def parse_virsh_list() -> list[dict[str, str]]: + def resolve_ip(name: str) -> str: + if not name: + return "—" + try: + result = run_cmd(["virsh", "domifaddr", name], timeout=8) + if result.get("ok"): + for ip in re.findall(r"\\b(\\d{1,3}(?:\\.\\d{1,3}){3})/\\d+", result.get("stdout", "")): + if not ip.startswith("127.") and not ip.startswith("169.254."): + return ip + except Exception: + pass + try: + resolved = network_core.resolve_vm_ip(name) + if resolved: + return str(resolved) + except Exception: + pass + return "—" -# Make parse_virsh_list always return enriched rows. -text, count = re.subn( - r"(def parse_virsh_list\(\) -> list\[dict\[str, str\]\]:.*?\n) return rows\n\n\ndef parse_pool_list", - r"\1 return enrich_vms_with_ips(rows)\n\n\ndef parse_pool_list", + result = run_cmd(["virsh", "list", "--all"]) + rows = [] + if not result["ok"]: + return rows + for line in result["stdout"].splitlines()[2:]: + parts = line.strip().split(None, 2) + if len(parts) == 3: + rows.append({"id": parts[0], "name": parts[1], "state": parts[2]}) + elif len(parts) == 2: + rows.append({"id": "-", "name": parts[0], "state": parts[1]}) + for row in rows: + row["ip"] = resolve_ip(row.get("name", "")) + return rows +''' + +text, replaced = re.subn( + r"def parse_virsh_list\(\) -> list\[dict\[str, str\]\]:.*?\n\ndef parse_pool_list\(\) -> list\[dict\[str, str\]\]:", + new_parse_virsh_list + "\n\ndef parse_pool_list() -> list[dict[str, str]]:", text, count=1, flags=re.S, ) -if count: - changed.append('parse_virsh_list return was replaced with enrich_vms_with_ips') -elif 'return enrich_vms_with_ips(rows)' in text: - changed.append('parse_virsh_list was already enriched') -else: - raise SystemExit('parse_virsh_list return marker not found') - -old_info = '''def libvirt_network_info() -> dict[str, Any]: - info = run_cmd(['virsh', 'net-info', NETWORK_NAME], timeout=8) - leases = run_cmd(['virsh', 'net-dhcp-leases', NETWORK_NAME], timeout=8) - return { - 'name': NETWORK_NAME, - 'bridge': NAT_BRIDGE, - 'subnet': NAT_SUBNET, - 'gateway': NAT_GATEWAY, - 'dhcp': f'{DHCP_START} - {DHCP_END}', - 'exists': info['ok'], - 'info': info['stdout'] if info['ok'] else info['stderr'], - 'leases': leases['stdout'] if leases['ok'] else leases['stderr'], - } -''' -new_info = '''def libvirt_network_info() -> dict[str, Any]: - info = run_cmd(['virsh', 'net-info', NETWORK_NAME], timeout=8) - leases = run_cmd(['virsh', 'net-dhcp-leases', NETWORK_NAME], timeout=8) - leases_text = leases['stdout'] if leases['ok'] else leases['stderr'] - lease_rows = parse_dhcp_leases_output(leases['stdout'] if leases['ok'] else '') if 'parse_dhcp_leases_output' in globals() else [] - data = { - 'name': NETWORK_NAME, - 'bridge': NAT_BRIDGE, - 'subnet': NAT_SUBNET, - 'gateway': NAT_GATEWAY, - 'dhcp': f'{DHCP_START} - {DHCP_END}', - 'exists': info['ok'], - 'info': info['stdout'] if info['ok'] else info['stderr'], - 'leases': leases_text, - 'lease_rows': lease_rows, - 'lease_count': len(lease_rows), - 'lease_hint': '', - } - data['lease_hint'] = dhcp_leases_hint(data, lease_rows) if 'dhcp_leases_hint' in globals() else 'DHCP leases пусты.' - return data -''' -if old_info in text: - text = text.replace(old_info, new_info, 1) - changed.append('libvirt_network_info was enriched with parsed lease rows') -elif "'lease_rows':" in text: - changed.append('libvirt_network_info was already enriched') -else: - changed.append('libvirt_network_info was not found in app.py, skipped') +if not replaced: + raise SystemExit('parse_virsh_list block not found') +changed.append('parse_virsh_list was replaced with a safe inline IP resolver') app_path.write_text(text, encoding='utf-8') -html = template_path.read_text(encoding='utf-8') -old_html = '''
-
-

DHCP leases

- virsh net-dhcp-leases -
-
{{ ctx.nat.leases }}
-
IP для проброса определяется автоматически: сначала через qemu-guest-agent, затем по DHCP leases и MAC-адресу VM.
-
-''' -new_html = '''
-
-

DHCP leases

- {{ ctx.nat.lease_count|default(0) }} active -
- {% if ctx.nat.lease_rows %} - - - - {% for lease in ctx.nat.lease_rows %} - - - - - - - {% endfor %} - -
IPMACHostExpiry
{{ lease.ip }}{{ lease.mac }}{{ lease.hostname }}{{ lease.expiry }}
- {% else %} -
{{ ctx.nat.lease_hint|default('DHCP leases пусты.') }}
-
virsh net-dhcp-leases {{ ctx.nat.name }}
-virsh net-info {{ ctx.nat.name }}
-virsh domiflist ИМЯ_VM
-virsh domifaddr ИМЯ_VM
- {% endif %} -
IP для проброса определяется автоматически: сначала через qemu-guest-agent, затем по DHCP leases и MAC-адресу VM. Если внутри образа статический IP, leases может быть пустым — тогда лучше добавить ручной IP-режим следующим шагом.
-
-''' -if old_html in html: - html = html.replace(old_html, new_html, 1) - changed.append('DHCP leases UI was replaced with structured table') -elif 'ctx.nat.lease_rows' in html: - changed.append('DHCP leases UI was already structured') -else: - changed.append('DHCP leases UI marker was not found, skipped') -template_path.write_text(html, encoding='utf-8') - dashboard_html = dashboard_template_path.read_text(encoding='utf-8') if 'IP' not in dashboard_html: - dashboard_html = dashboard_html.replace('IDNameStateActions', 'IDNameIPStateActions') - dashboard_html = dashboard_html.replace('{{ vm.name }}\n {{ vm.name }}\n {{ vm.ip|default(\'—\') }}\n IDNameStateActions', + 'IDNameIPStateActions' + ) + dashboard_html = dashboard_html.replace( + '{{ vm.name }}\n {{ vm.name }}\n {{ vm.ip|default(\'—\') }}\n Виртуальных машин пока нет', 'colspan="5" class="muted">Виртуальных машин пока нет') changed.append('Dashboard VM IP column was added') +elif "vm.ip" not in dashboard_html: + dashboard_html = dashboard_html.replace( + '{{ vm.name }}\n {{ vm.name }}\n {{ vm.ip|default(\'—\') }}\n