Безопасность панели была усилена

Проверка пароля переведена на PAM (python-pam) с fallback на crypt/spwd
для совместимости, все POST-формы защищены CSRF-токеном в сессии,
после пяти неудачных попыток входа включается пауза 60 секунд,
установщик генерирует самоподписанный TLS-сертификат и панель
по умолчанию работает по HTTPS (отключается VIRTUALITY_TLS=0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKUoZwHMCHnytfswFVRUHw
Этот коммит содержится в:
Claude
2026-08-15 19:19:33 +00:00
родитель 1da491c9e1
Коммит bb87f52834
17 изменённых файлов: 276 добавлений и 89 удалений
+3 -3
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -44,7 +44,7 @@
<td><span class="status {{ 'ok' if entry.native else 'warn' }}">{{ entry.arch }}{% if entry.native %} · родная{% endif %}</span></td>
<td>{% if entry.downloaded %}<span class="status ok">скачан</span>{% else %}<span class="muted">не скачан</span>{% endif %}</td>
<td class="actions">
<form method="post" action="/cloud-images/download">
<form method="post" action="/cloud-images/download"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="key" value="{{ entry.key }}">
<button class="{{ 'ghost' if entry.downloaded else 'primary' }}">{{ 'Скачать заново' if entry.downloaded else 'Скачать' }}</button>
</form>
@@ -98,7 +98,7 @@
<td>{{ image.updated }}</td>
<td class="actions">
<a class="link-pill" href="/vm/create-cloud">Создать VM</a>
<form method="post" action="/cloud-images/{{ image.name }}/delete" onsubmit="return confirm('Удалить образ {{ image.name }}?');">
<form method="post" action="/cloud-images/{{ image.name }}/delete" onsubmit="return confirm('Удалить образ {{ image.name }}?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="danger">Удалить</button>
</form>
</td>
+8 -8
Просмотреть файл
@@ -18,7 +18,7 @@
</div>
<div class="top-meta">
<span>user: {{ user }}</span>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -76,11 +76,11 @@
<td><span class="status {{ 'ok' if is_running else 'err' if 'shut' in vm.state else 'warn' }}">{{ vm.state }}</span></td>
<td class="autostart-cell">
{% if vm.autostart_enabled %}
<form method="post" action="/vm/{{ vm.name }}/autostart-disable" class="inline-form">
<form method="post" action="/vm/{{ vm.name }}/autostart-disable" class="inline-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="status status-button ok" title="Отключить автозапуск">{{ vm.autostart_label|default('enabled') }}</button>
</form>
{% else %}
<form method="post" action="/vm/{{ vm.name }}/autostart" class="inline-form">
<form method="post" action="/vm/{{ vm.name }}/autostart" class="inline-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="status status-button err" title="Включить автозапуск">{{ vm.autostart_label|default('disabled') }}</button>
</form>
{% endif %}
@@ -90,12 +90,12 @@
<a class="icon-action" href="/vm/{{ vm.name }}" title="Подробнее"><span></span><em>Детали</em></a>
<a class="icon-action accent" href="/vm/{{ vm.name }}/console" target="_blank" rel="noopener" title="Консоль"><span></span><em>Консоль</em></a>
{% if is_running %}
<form method="post" action="/vm/{{ vm.name }}/reboot"><button class="icon-action" title="Перезагрузить"><span></span><em>Reboot</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/shutdown"><button class="icon-action" title="Мягко выключить"><span></span><em>Shutdown</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/destroy"><button class="icon-action danger" title="Принудительно выключить"><span></span><em>Power</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/reboot"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action" title="Перезагрузить"><span></span><em>Reboot</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/shutdown"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action" title="Мягко выключить"><span></span><em>Shutdown</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/destroy"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action danger" title="Принудительно выключить"><span></span><em>Power</em></button></form>
{% else %}
<form method="post" action="/vm/{{ vm.name }}/start"><button class="icon-action success" title="Запустить"><span></span><em>Start</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/delete" onsubmit="return confirm('Удалить VM {{ vm.name }} вместе с дисками?');"><button class="icon-action danger" title="Удалить VM"><span>🗑</span><em>Delete</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/start"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action success" title="Запустить"><span></span><em>Start</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/delete" onsubmit="return confirm('Удалить VM {{ vm.name }} вместе с дисками?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action danger" title="Удалить VM"><span>🗑</span><em>Delete</em></button></form>
{% endif %}
</div>
</td>
+3 -3
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -33,7 +33,7 @@
<h2>Загрузить образ установленной системы</h2>
<span class="pill">.img .raw .qcow2 .img.xz .zip .tar.gz</span>
</div>
<form id="disk-upload-form" method="post" action="/disk-images/upload" enctype="multipart/form-data" class="form-grid">
<form id="disk-upload-form" method="post" action="/disk-images/upload" enctype="multipart/form-data" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="upload-box">
<span>Выбери файл готового диска или архив с диском</span>
<input id="disk-file-input" type="file" name="image_file" accept=".img,.raw,.qcow2,.img.xz,.zip,.tar.gz,.tgz" required>
@@ -93,7 +93,7 @@
<td>{{ image.updated }}</td>
<td class="actions">
<a class="link-pill" href="/vm/create">Создать VM</a>
<form method="post" action="/disk-images/{{ image.name }}/delete" onsubmit="return confirm('Удалить образ {{ image.name }}?');">
<form method="post" action="/disk-images/{{ image.name }}/delete" onsubmit="return confirm('Удалить образ {{ image.name }}?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="danger">Удалить</button>
</form>
</td>
+2 -2
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -65,7 +65,7 @@
<article class="card">
<div class="card-head">
<h2>Проверки</h2>
<form method="post" action="/host/refresh"><button>Переопределить профиль</button></form>
<form method="post" action="/host/refresh"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button>Переопределить профиль</button></form>
</div>
<div class="services">
{% for check in profile.checks %}
+4 -4
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -33,7 +33,7 @@
<h2>Загрузить установочный ISO</h2>
<span class="pill">только .iso</span>
</div>
<form id="iso-upload-form" method="post" action="/iso/upload" enctype="multipart/form-data" class="form-grid">
<form id="iso-upload-form" method="post" action="/iso/upload" enctype="multipart/form-data" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label class="upload-box">
<span>Выбери ISO-файл установочного диска</span>
<input id="iso-file-input" type="file" name="iso_file" accept=".iso" required>
@@ -62,7 +62,7 @@
<span class="pill">pool</span>
</div>
<div class="actions big-actions">
<form method="post" action="/iso/refresh"><button>Обновить ISO pool</button></form>
<form method="post" action="/iso/refresh"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button>Обновить ISO pool</button></form>
<a class="link-pill accent" href="/vm/create">Создать VM</a>
</div>
<pre>sudo virsh pool-refresh virtuality-iso
@@ -86,7 +86,7 @@ ls -lh /var/lib/virtuality/iso</pre>
<td>{{ iso.size }}</td>
<td>{{ iso.updated }}</td>
<td class="actions">
<form method="post" action="/iso/{{ iso.name }}/delete" onsubmit="return confirm('Удалить ISO {{ iso.name }}?');">
<form method="post" action="/iso/{{ iso.name }}/delete" onsubmit="return confirm('Удалить ISO {{ iso.name }}?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="danger">Удалить</button>
</form>
</td>
+1 -1
Просмотреть файл
@@ -24,7 +24,7 @@
<div class="alert danger">{{ error }}</div>
{% endif %}
<form method="post" action="/login" class="auth-form">
<form method="post" action="/login" class="auth-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Linux-пользователь</span>
<input type="text" name="username" value="{{ auth_user }}" autocomplete="username" required>
+1 -1
Просмотреть файл
@@ -38,7 +38,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
+7 -7
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -58,7 +58,7 @@
<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">
<form method="post" action="/network/nat/setup" class="inline-form top-space"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="primary">Создать / починить NAT-сеть</button>
</form>
<pre>{{ ctx.nat.info }}</pre>
@@ -80,7 +80,7 @@
<h2>Добавить проброс порта</h2>
<span class="pill">DNAT auto/manual IP · ranges</span>
</div>
<form method="post" action="/network/forward/add" class="form-grid">
<form method="post" action="/network/forward/add" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Описание</span>
<input type="text" name="note" placeholder="Например: сайт, SSH, SIP, RTP MikoPBX">
@@ -155,7 +155,7 @@
<h2>Диагностика публичного доступа</h2>
<span class="pill">VM → Internet</span>
</div>
<form method="post" action="/network/diagnose" class="form-grid">
<form method="post" action="/network/diagnose" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Виртуальная машина</span>
<select name="vm_name" required>
@@ -220,7 +220,7 @@
<section class="card">
<div class="card-head">
<h2>Пробросы портов</h2>
<form method="post" action="/network/apply"><button>Применить правила</button></form>
<form method="post" action="/network/apply"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button>Применить правила</button></form>
</div>
<table>
<thead>
@@ -239,14 +239,14 @@
<td>{{ item.note }}</td>
<td>
<div class="actions">
<form method="post" action="/network/diagnose">
<form method="post" action="/network/diagnose"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="vm_name" value="{{ item.vm_name }}">
<input type="hidden" name="external_port" value="{{ item.external_port }}">
<input type="hidden" name="guest_port" value="{{ item.guest_port }}">
<input type="hidden" name="protocol" value="{{ item.protocol }}">
<button>Диагностика</button>
</form>
<form method="post" action="/network/forward/{{ item.id }}/delete"><button class="danger">Удалить</button></form>
<form method="post" action="/network/forward/{{ item.id }}/delete"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="danger">Удалить</button></form>
</div>
</td>
</tr>
+1 -1
Просмотреть файл
@@ -17,7 +17,7 @@
<span>user: {{ user }}</span>
<a class="link-pill" href="/operations">Операции</a>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
+1 -1
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
+3 -3
Просмотреть файл
@@ -38,7 +38,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -82,11 +82,11 @@
{% endif %}
<div class="actions top-space update-actions">
{% if info.has_update %}
<form method="post" action="/update/apply" id="update-apply-form" onsubmit="return confirm('Запустить обновление Virtuality из GitHub? Панель перезапустится.');">
<form method="post" action="/update/apply" id="update-apply-form" onsubmit="return confirm('Запустить обновление Virtuality из GitHub? Панель перезапустится.');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="primary" id="update-apply-button" type="submit">Установить обновления</button>
</form>
{% else %}
<form method="post" action="/update/check" id="update-check-form">
<form method="post" action="/update/check" id="update-check-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="primary" id="update-main-button" type="submit">Проверить обновления</button>
</form>
{% endif %}
+2 -2
Просмотреть файл
@@ -21,7 +21,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -36,7 +36,7 @@
<span class="pill">VM wizard</span>
</div>
<form method="post" action="/vm/create" class="form-grid">
<form method="post" action="/vm/create" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Имя VM</span>
<input type="text" name="name" value="{{ form.name or '' }}" placeholder="ubuntu-test" required>
+2 -2
Просмотреть файл
@@ -19,7 +19,7 @@
<div class="top-meta">
<span>user: {{ user }}</span>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -34,7 +34,7 @@
<span class="pill">cloud-init</span>
</div>
<form method="post" action="/vm/create-cloud" class="form-grid">
<form method="post" action="/vm/create-cloud" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Имя VM</span>
<input type="text" name="name" value="{{ form.name or '' }}" placeholder="ubuntu-web" required>
+17 -17
Просмотреть файл
@@ -65,7 +65,7 @@
<span class="status {{ 'ok' if is_autostart else 'err' }}">autostart: {{ autostart_label }}</span>
<a class="link-pill accent" href="/vm/{{ vm.name }}/console" target="_blank" rel="noopener">Консоль ↗</a>
<a class="link-pill" href="/">← Дашборд</a>
<form method="post" action="/logout" class="logout-form"><button class="ghost">Выйти</button></form>
<form method="post" action="/logout" class="logout-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="ghost">Выйти</button></form>
</div>
</header>
@@ -93,10 +93,10 @@
<div class="vm-actions compact-actions big-actions">
<a class="icon-action accent" href="/vm/{{ vm.name }}/console" target="_blank" rel="noopener" title="Открыть консоль"><span></span><em>Консоль</em></a>
{% if is_running %}
<form method="post" action="/vm/{{ vm.name }}/reboot"><button class="icon-action" title="Перезагрузить"><span></span><em>Reboot</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/shutdown"><button class="icon-action" title="Мягко выключить"><span></span><em>Shutdown</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/reboot"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action" title="Перезагрузить"><span></span><em>Reboot</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/shutdown"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action" title="Мягко выключить"><span></span><em>Shutdown</em></button></form>
{% else %}
<form method="post" action="/vm/{{ vm.name }}/start"><button class="icon-action success" title="Запустить"><span></span><em>Start</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/start"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action success" title="Запустить"><span></span><em>Start</em></button></form>
{% endif %}
</div>
</div>
@@ -104,11 +104,11 @@
<div class="label">Автозапуск</div>
<div class="vm-actions compact-actions big-actions">
{% if is_autostart %}
<form method="post" action="/vm/{{ vm.name }}/autostart-disable" class="autostart-form">
<form method="post" action="/vm/{{ vm.name }}/autostart-disable" class="autostart-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="autostart-indicator on" title="Автозапуск включён. Нажми, чтобы отключить"><span></span><strong>Включён</strong><small>Нажми для отключения</small></button>
</form>
{% else %}
<form method="post" action="/vm/{{ vm.name }}/autostart" class="autostart-form">
<form method="post" action="/vm/{{ vm.name }}/autostart" class="autostart-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="autostart-indicator off" title="Автозапуск выключен. Нажми, чтобы включить"><span></span><strong>Выключен</strong><small>Нажми для включения</small></button>
</form>
{% endif %}
@@ -119,10 +119,10 @@
<div class="label">Опасные действия</div>
<div class="vm-actions compact-actions big-actions">
{% if is_running %}
<form method="post" action="/vm/{{ vm.name }}/destroy"><button class="icon-action danger" title="Принудительно выключить"><span></span><em>Power off</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/destroy"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action danger" title="Принудительно выключить"><span></span><em>Power off</em></button></form>
{% endif %}
{% if not is_running %}
<form method="post" action="/vm/{{ vm.name }}/delete" onsubmit="return confirm('Удалить VM {{ vm.name }} вместе с дисками?');"><button class="icon-action danger" title="Удалить VM вместе с дисками"><span>🗑</span><em>Delete</em></button></form>
<form method="post" action="/vm/{{ vm.name }}/delete" onsubmit="return confirm('Удалить VM {{ vm.name }} вместе с дисками?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}"><button class="icon-action danger" title="Удалить VM вместе с дисками"><span>🗑</span><em>Delete</em></button></form>
{% else %}
<span class="muted small-note">Удаление доступно после выключения VM.</span>
{% endif %}
@@ -136,7 +136,7 @@
<h2>CPU / RAM / Архитектура</h2>
<span class="pill">resources</span>
</div>
<form method="post" action="/vm/{{ vm.name }}/resources" class="form-grid">
<form method="post" action="/vm/{{ vm.name }}/resources" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-row">
<label>
<span>CPU</span>
@@ -169,7 +169,7 @@
<h2>Порядок загрузки</h2>
<span class="pill">boot</span>
</div>
<form method="post" action="/vm/{{ vm.name }}/boot-order" class="form-grid boot-order-form">
<form method="post" action="/vm/{{ vm.name }}/boot-order" class="form-grid boot-order-form"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="boot_order" id="boot-order-value" value="{{ current_boot_order|default('auto') }}">
<div class="boot-order-list" id="boot-order-list" data-current="{{ current_boot_order|default('auto') }}">
<div class="boot-order-item" draggable="true" data-device="cdrom"><span class="drag-handle"></span><div><b>ISO / CD-ROM</b><small>Установщик или rescue-образ</small></div></div>
@@ -191,7 +191,7 @@
{% else %}
<div class="muted small-note">ISO сейчас не подключён.</div>
{% endif %}
<form method="post" action="/vm/{{ vm.name }}/iso/mount" class="form-grid">
<form method="post" action="/vm/{{ vm.name }}/iso/mount" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>ISO образ</span>
<select name="iso_path" required>
@@ -202,7 +202,7 @@
</label>
<button class="primary wide" type="submit" {% if not isos %}disabled{% endif %}>Смонтировать ISO</button>
</form>
<form method="post" action="/vm/{{ vm.name }}/iso/unmount" class="form-grid" onsubmit="return confirm('Отмонтировать ISO из VM {{ vm.name }}?');">
<form method="post" action="/vm/{{ vm.name }}/iso/unmount" class="form-grid" onsubmit="return confirm('Отмонтировать ISO из VM {{ vm.name }}?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="ghost wide" {% if not current_iso %}disabled{% endif %}>Отмонтировать ISO</button>
</form>
{% if not isos %}
@@ -254,7 +254,7 @@
<h2>Снапшоты</h2>
<span class="pill">{{ snapshots|length }} шт.</span>
</div>
<form method="post" action="/vm/{{ vm.name }}/snapshot/create" class="form-grid">
<form method="post" action="/vm/{{ vm.name }}/snapshot/create" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="form-row">
<label>
<span>Имя снапшота</span>
@@ -278,11 +278,11 @@
<td>{{ snap.created }}</td>
<td><span class="status {{ snap.state_css }}">{{ snap.state }}</span></td>
<td class="actions">
<form method="post" action="/vm/{{ vm.name }}/snapshot/revert" onsubmit="return confirm('Откатить VM {{ vm.name }} к снапшоту {{ snap.name }}? Текущее состояние диска будет потеряно.');">
<form method="post" action="/vm/{{ vm.name }}/snapshot/revert" onsubmit="return confirm('Откатить VM {{ vm.name }} к снапшоту {{ snap.name }}? Текущее состояние диска будет потеряно.');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="snapshot_name" value="{{ snap.name }}">
<button class="ghost">Откатиться</button>
</form>
<form method="post" action="/vm/{{ vm.name }}/snapshot/delete" onsubmit="return confirm('Удалить снапшот {{ snap.name }}?');">
<form method="post" action="/vm/{{ vm.name }}/snapshot/delete" onsubmit="return confirm('Удалить снапшот {{ snap.name }}?');"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="snapshot_name" value="{{ snap.name }}">
<button class="danger">Удалить</button>
</form>
@@ -302,7 +302,7 @@
<h2>Клонирование</h2>
<span class="pill">virt-clone</span>
</div>
<form method="post" action="/vm/{{ vm.name }}/clone" class="form-grid">
<form method="post" action="/vm/{{ vm.name }}/clone" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>
<span>Имя новой VM</span>
<input type="text" name="new_name" placeholder="{{ vm.name }}-copy" required>
@@ -321,7 +321,7 @@
<h2>Шаблон</h2>
<span class="pill">{{ 'template' if is_template else 'обычная VM' }}</span>
</div>
<form method="post" action="/vm/{{ vm.name }}/template" class="form-grid">
<form method="post" action="/vm/{{ vm.name }}/template" class="form-grid"><input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="enabled" value="{{ '0' if is_template else '1' }}">
<button class="{{ 'ghost' if is_template else 'primary' }} wide">{{ 'Снять флаг шаблона' if is_template else 'Пометить как шаблон' }}</button>
</form>