DHCP leases
- virsh net-dhcp-leases -{{ ctx.nat.leases }}
- 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 }}
- DHCP leases
- {{ ctx.nat.lease_count|default(0) }} active
-
-
- {% else %}
-
-
- {% for lease in ctx.nat.lease_rows %}
- IP MAC Host Expiry
-
- {% endfor %}
-
- {{ lease.ip }}
- {{ lease.mac }}
- {{ lease.hostname }}
- {{ lease.expiry }}
- virsh net-dhcp-leases {{ ctx.nat.name }}
-virsh net-info {{ ctx.nat.name }}
-virsh domiflist ИМЯ_VM
-virsh domifaddr ИМЯ_VM
- {% endif %}
-