Files
virtuality/web/templates/network.html
T

268 строки
11 KiB
HTML

<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Сеть — {{ app_name }}</title>
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<div class="shell">
<header class="topbar">
<div>
<div class="brand">Сеть</div>
<div class="muted">NAT Router для VPS и проброс портов к VM</div>
</div>
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill accent" href="/vm/create">+ Создать VM</a>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
</div>
</header>
{% if error %}
<div class="alert danger">{{ error }}</div>
{% endif %}
<section class="grid stats">
<article class="card stat">
<div class="label">NAT network</div>
<div class="value small">{{ ctx.nat.name }}</div>
<span class="status {{ 'ok' if ctx.nat.exists else 'warn' }}">{{ 'ready' if ctx.nat.exists else 'not created' }}</span>
</article>
<article class="card stat">
<div class="label">Subnet</div>
<div class="value small">{{ ctx.nat.subnet }}</div>
</article>
<article class="card stat">
<div class="label">External interface</div>
<div class="value small">{{ ctx.external_interface }}</div>
</article>
<article class="card stat">
<div class="label">IPv4 forward</div>
<div class="value small">{{ ctx.ip_forward }}</div>
</article>
</section>
<section class="grid two">
<article class="card">
<div class="card-head">
<h2>Virtuality NAT Router</h2>
<span class="pill">{{ ctx.nat.bridge }}</span>
</div>
<div class="meta-grid">
<div><span>Gateway</span><strong>{{ ctx.nat.gateway }}</strong></div>
<div><span>DHCP</span><strong>{{ ctx.nat.dhcp }}</strong></div>
<div><span>Bridge</span><strong>{{ ctx.nat.bridge }}</strong></div>
</div>
<form method="post" action="/network/nat/setup" class="inline-form top-space">
<button class="primary">Создать / починить NAT-сеть</button>
</form>
<pre>{{ ctx.nat.info }}</pre>
</article>
<article class="card">
<div class="card-head">
<h2>DHCP leases</h2>
<span class="pill">virsh net-dhcp-leases</span>
</div>
<pre>{{ ctx.nat.leases }}</pre>
<div class="notice top-space">IP для проброса определяется автоматически: сначала через qemu-guest-agent, затем по DHCP leases и MAC-адресу VM.</div>
</article>
</section>
<section class="card">
<div class="card-head">
<h2>Быстрые шаблоны публичного доступа</h2>
<span class="pill">one click presets</span>
</div>
<div class="grid two">
{% for key, tpl in templates.items() %}
<article class="big-action static">
<strong>{{ tpl.title }}</strong>
<span>{{ tpl.description }}</span>
<form method="post" action="/network/template/apply" class="form-grid top-space">
<label>
<span>Виртуальная машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="template_key" value="{{ key }}">
{% if key == 'ssh' %}
<label>
<span>Внешний SSH-порт</span>
<input type="number" name="external_base_port" min="1" max="65535" value="2222">
</label>
{% else %}
<input type="hidden" name="external_base_port" value="0">
{% endif %}
<button class="primary wide">Применить шаблон</button>
</form>
</article>
{% endfor %}
</div>
<div class="notice top-space">Шаблоны не затирают существующие правила: если внешний порт уже занят, правило будет пропущено.</div>
</section>
<section class="grid two">
<article class="card">
<div class="card-head">
<h2>Добавить проброс порта</h2>
<span class="pill">DNAT auto IP</span>
</div>
<form method="post" action="/network/forward/add" class="form-grid">
<label>
<span>Виртуальная машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<input type="hidden" name="guest_ip" value="auto">
<div class="notice">Внутренний IP VM будет подставлен автоматически. VM должна быть запущена и получить адрес в NAT-сети.</div>
<div class="form-row">
<label>
<span>Внешний порт сервера</span>
<input type="number" name="external_port" min="1" max="65535" placeholder="80" required>
</label>
<label>
<span>Порт внутри VM</span>
<input type="number" name="guest_port" min="1" max="65535" placeholder="80" required>
</label>
</div>
<div class="form-row">
<label>
<span>Протокол</span>
<select name="protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</label>
<label>
<span>Заметка</span>
<input type="text" name="note" placeholder="web / ssh / sip">
</label>
</div>
<button class="primary wide">Добавить и применить</button>
</form>
</article>
<article class="card">
<div class="card-head">
<h2>Диагностика публичного доступа</h2>
<span class="pill">VM → Internet</span>
</div>
<form method="post" action="/network/diagnose" class="form-grid">
<label>
<span>Виртуальная машина</span>
<select name="vm_name" required>
{% for vm in vms %}
<option value="{{ vm.name }}">{{ vm.name }} — {{ vm.state }}</option>
{% endfor %}
</select>
</label>
<div class="form-row">
<label>
<span>Внешний порт</span>
<input type="number" name="external_port" min="1" max="65535" value="80" required>
</label>
<label>
<span>Порт внутри VM</span>
<input type="number" name="guest_port" min="1" max="65535" value="80" required>
</label>
</div>
<label>
<span>Протокол</span>
<select name="protocol">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</label>
<button class="primary wide">Проверить доступ</button>
</form>
</article>
</section>
{% if diagnostics %}
<section class="card">
<div class="card-head">
<h2>Результат диагностики</h2>
<span class="pill">{{ diagnostics.vm_name }} · {{ diagnostics.protocol }} {{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</span>
</div>
<div class="notice">{{ diagnostics.verdict }}</div>
<div class="meta-grid top-space">
<div><span>VM IP</span><strong>{{ diagnostics.vm_ip or 'not found' }}</strong></div>
<div><span>External interface</span><strong>{{ diagnostics.external_interface }}</strong></div>
<div><span>Mapping</span><strong>{{ diagnostics.external_port }} → {{ diagnostics.guest_port }}</strong></div>
</div>
<table class="top-space">
<thead><tr><th>Проверка</th><th>Статус</th><th>Детали</th></tr></thead>
<tbody>
{% for check in diagnostics.checks %}
<tr>
<td class="strong">{{ check.name }}</td>
<td><span class="status {{ 'ok' if check.ok else 'err' }}">{{ 'OK' if check.ok else 'FAIL' }}</span></td>
<td class="muted">{{ check.detail }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="grid two top-space">
<article>
<div class="label">Проверить входящий трафик</div>
<pre>{{ diagnostics.commands.watch_external }}</pre>
</article>
<article>
<div class="label">Проверить трафик к VM</div>
<pre>{{ diagnostics.commands.watch_internal }}</pre>
</article>
</div>
</section>
{% endif %}
<section class="card">
<div class="card-head">
<h2>Пробросы портов</h2>
<form method="post" action="/network/apply"><button>Применить правила</button></form>
</div>
<table>
<thead>
<tr><th>Внешний порт</th><th>VM</th><th>Внутренний адрес</th><th>Протокол</th><th>Заметка</th><th></th></tr>
</thead>
<tbody>
{% for item in ctx.forwards %}
<tr>
<td class="strong">{{ item.external_port }} → {{ item.guest_port }}</td>
<td>{{ item.vm_name }}</td>
<td>{{ item.guest_ip }}</td>
<td><span class="status ok">{{ item.protocol }}</span></td>
<td>{{ item.note }}</td>
<td>
<form method="post" action="/network/forward/{{ item.id }}/delete">
<button class="danger">Удалить</button>
</form>
</td>
</tr>
{% else %}
<tr><td colspan="6" class="muted">Пробросов пока нет</td></tr>
{% endfor %}
</tbody>
</table>
</section>
<section class="card">
<div class="card-head">
<h2>nftables preview</h2>
<span class="pill">/etc/virtuality/nftables/virtuality.nft</span>
</div>
<pre>{{ ctx.nft_rules }}</pre>
</section>
</div>
</body>
</html>