196 строки
16 KiB
HTML
196 строки
16 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_ui.html" import icon, badge, alert %}
|
|
{% set page_title = 'Новая машина' %}
|
|
{% set source = form.source_type|default('iso') %}
|
|
{% set preset_match = namespace(id='custom') %}
|
|
{% for p in vm_presets %}{% if p.vcpus|string == form.vcpus|string and p.memory|string == form.memory|string and p.disk_size|string == form.disk_size|string %}{% set preset_match.id = p.id %}{% endif %}{% endfor %}
|
|
|
|
{% block page_header %}
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="eyebrow"><a href="/">{{ icon('arrow-left', 'icon-sm') }} Все машины</a></div>
|
|
<h1>Новая виртуальная машина</h1>
|
|
<p class="lead">Заполните три шага — всё остальное Virtuality настроит сам.</p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<form method="post" action="/vm/create" class="grid grid-main" data-wizard data-has-isos="{{ '1' if isos else '0' }}" data-has-disks="{{ '1' if disk_images else '0' }}">
|
|
<div class="stack">
|
|
<article class="card step-card">
|
|
<div class="step-head"><span class="step-num">1</span><div><h2>Название и система</h2><p>Как назовём машину и что на неё установить</p></div></div>
|
|
<div class="step-body form-grid">
|
|
<div class="field">
|
|
<label for="name">Название машины</label>
|
|
<input class="input" id="name" type="text" name="name" value="{{ form.name or '' }}" placeholder="например, web-server" pattern="[a-zA-Z0-9][a-zA-Z0-9_.\-]{1,62}" autocomplete="off" required>
|
|
<span class="hint">Латинские буквы, цифры, точка, дефис. От 2 до 63 символов.</span>
|
|
</div>
|
|
|
|
<div class="options" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr))">
|
|
<label class="option">
|
|
<input type="radio" name="source_type" value="iso" {% if source != 'disk_image' %}checked{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-icon">{{ icon('disc-3') }}</span><span class="option-check"></span></span>
|
|
<span class="option-title">Установить с диска</span>
|
|
<span class="option-text">Установочный образ .iso — как установка на обычный компьютер</span>
|
|
</span>
|
|
</label>
|
|
<label class="option">
|
|
<input type="radio" name="source_type" value="disk_image" {% if source == 'disk_image' %}checked{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-icon">{{ icon('hard-drive') }}</span><span class="option-check"></span></span>
|
|
<span class="option-title">Готовая система</span>
|
|
<span class="option-text">Образ уже установленной системы: .qcow2, .img, .raw</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="field" data-show-when="source_type=iso">
|
|
<label for="iso_path">Установочный образ</label>
|
|
{% if isos %}
|
|
<select class="select" id="iso_path" name="iso_path">
|
|
{% for iso in isos %}<option value="{{ iso.path }}" data-label="{{ iso.name }}" {% if form.iso_path == iso.path %}selected{% endif %}>{{ iso.name }} · {{ iso.size }}</option>{% endfor %}
|
|
</select>
|
|
<span class="hint">Нужен другой? <a href="/iso">Загрузите образ</a>.</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="field" data-show-when="source_type=iso">
|
|
<label>Что будете устанавливать</label>
|
|
<div class="options" style="grid-template-columns:repeat(auto-fit,minmax(170px,1fr))">
|
|
{% for os in os_types %}
|
|
<label class="option">
|
|
<input type="radio" name="os_type" value="{{ os.id }}" {% if (form.os_type or 'linux') == os.id %}checked{% endif %}>
|
|
<span class="option-body"><span class="option-top"><span class="option-title">{{ os.title }}</span><span class="option-check"></span></span><span class="option-text">{{ os.hint }}</span></span>
|
|
</label>
|
|
{% endfor %}
|
|
</div>
|
|
<span class="hint">От этого зависят виртуальные устройства: Windows получит совместимые диск и сеть, Windows 11 — UEFI и TPM.</span>
|
|
</div>
|
|
<div class="field" data-show-when="source_type=disk_image">
|
|
<label for="disk_image_path">Образ системы</label>
|
|
{% if disk_images %}
|
|
<select class="select" id="disk_image_path" name="disk_image_path">
|
|
{% for image in disk_images %}<option value="{{ image.path }}" data-label="{{ image.title or image.name }}" data-login="{{ image.login }}" {% if form.disk_image_path == image.path %}selected{% endif %}>{{ image.title or image.name }} · {{ image.size }}</option>{% endfor %}
|
|
</select>
|
|
<span class="hint">Образ будет скопирован — оригинал останется для других машин.</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="stack-sm" data-show-when="source_type=disk_image" style="padding:14px 16px;border:1px solid var(--border);border-radius:var(--radius);background:var(--surface-2)">
|
|
<label class="switch-row" style="cursor:pointer">
|
|
<span class="switch-text"><strong>Настроить первый вход</strong><span>У готовых образов нет пароля — без этого шага войти в машину не получится.</span></span>
|
|
<input type="checkbox" name="cloud_init" value="1" {% if form.cloud_init|default('1') == '1' %}checked{% endif %} style="width:20px;height:20px;accent-color:var(--brand)">
|
|
</label>
|
|
<div class="form-grid" data-visible-when="cloud_init=1">
|
|
<div class="form-row">
|
|
<div class="field"><label for="ci_user">Пользователь</label><input class="input" id="ci_user" name="ci_user" value="{{ form.ci_user or '' }}" pattern="[a-z_][a-z0-9_\-]{0,31}" autocomplete="off" data-auto-login="{{ '0' if form.ci_user else '1' }}"><span class="hint">Получит права администратора (sudo)</span></div>
|
|
<div class="field"><label for="ci_password">Пароль</label><input class="input" id="ci_password" type="password" name="ci_password" minlength="6" autocomplete="new-password"><span class="hint">Не короче 6 символов</span></div>
|
|
</div>
|
|
<div class="field"><label for="ci_ssh_key">SSH-ключ <span class="subtle">(необязательно)</span></label><textarea class="input" id="ci_ssh_key" name="ci_ssh_key" rows="2" placeholder="ssh-ed25519 AAAA… — содержимое файла ~/.ssh/id_ed25519.pub">{{ form.ci_ssh_key or '' }}</textarea><span class="hint">С ключом можно обойтись без пароля: вход по SSH будет только по ключу.</span></div>
|
|
</div>
|
|
</div>
|
|
<div class="alert warning" data-missing-source="iso" hidden>{{ icon('triangle-alert') }}<div><strong>Нет установочных образов</strong>Загрузите .iso с системой, например Ubuntu или Debian.</div><a class="btn btn-sm alert-action" href="/iso">Загрузить</a></div>
|
|
<div class="alert warning" data-missing-source="disk_image" hidden>{{ icon('triangle-alert') }}<div><strong>Нет готовых образов</strong>Загрузите образ системы в формате .qcow2, .img или архив.</div><a class="btn btn-sm alert-action" href="/disk-images">Загрузить</a></div>
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card step-card">
|
|
<div class="step-head"><span class="step-num">2</span><div><h2>Мощность</h2><p>Сколько ресурсов сервера выделить машине. Можно поменять позже.</p></div></div>
|
|
<div class="step-body form-grid">
|
|
<div class="options options-4">
|
|
{% for p in vm_presets %}
|
|
<label class="option">
|
|
<input type="radio" name="preset" value="{{ p.id }}" data-vcpus="{{ p.vcpus }}" data-memory="{{ p.memory }}" data-disk="{{ p.disk_size }}" {% if preset_match.id == p.id %}checked{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-title">{{ p.title }}</span><span class="option-check"></span></span>
|
|
<span class="option-text">{{ p.hint }}</span>
|
|
<span class="option-specs"><span class="tag">{{ p.vcpus }} {{ plural(p.vcpus, 'ядро', 'ядра', 'ядер') }}</span><span class="tag">{{ (p.memory / 1024)|int }} ГБ ОЗУ</span><span class="tag">{{ p.disk_size }} ГБ</span></span>
|
|
</span>
|
|
</label>
|
|
{% endfor %}
|
|
<label class="option">
|
|
<input type="radio" name="preset" value="custom" {% if preset_match.id == 'custom' %}checked{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-title">Своя</span><span class="option-check"></span></span>
|
|
<span class="option-text">Задать вручную</span>
|
|
<span class="option-specs"><span class="tag">{{ icon('settings', 'icon-sm') }}Точная настройка</span></span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-row-3" data-custom-resources data-visible-when="preset=custom">
|
|
<div class="field"><label for="vcpus">Ядра</label><input class="input" id="vcpus" type="number" name="vcpus" min="1" max="128" value="{{ form.vcpus }}" required></div>
|
|
<div class="field"><label for="memory">Память, МБ</label><input class="input" id="memory" type="number" name="memory" min="512" max="262144" step="256" value="{{ form.memory }}" required></div>
|
|
<div class="field"><label for="disk_size">Диск, ГБ</label><input class="input" id="disk_size" type="number" name="disk_size" min="4" max="4096" value="{{ form.disk_size }}" required></div>
|
|
</div>
|
|
<p class="subtle small">Свободно на сервере: {{ images_free }}. Диск машины занимает место постепенно, по мере заполнения.</p>
|
|
{% if form.overcommit %}
|
|
<label class="check danger">
|
|
<input type="checkbox" name="allow_overcommit" value="1" {% if form.allow_overcommit %}checked{% endif %}>
|
|
<span><strong>Всё равно создать диск на {{ form.disk_size }} ГБ</strong><span>На сервере свободно {{ images_free }}. Сразу столько места диск не займёт, поэтому создать его можно. Но если система внутри машины заполнит больше, чем есть на сервере, место закончится у всех машин и они остановятся. Надёжнее уменьшить диск или освободить место.</span></span>
|
|
</label>
|
|
{% endif %}
|
|
{% if profile.virtualization_warning %}{{ alert('warning', 'На этом сервере нет аппаратного ускорения — машины будут заметно медленнее.') }}{% endif %}
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card step-card">
|
|
<div class="step-head"><span class="step-num">3</span><div><h2>Сеть</h2><p>Как машина будет выходить в интернет</p></div></div>
|
|
<div class="step-body form-grid">
|
|
<div class="options" style="grid-template-columns:repeat(auto-fit,minmax(220px,1fr))">
|
|
<label class="option">
|
|
<input type="radio" name="network_mode" value="nat" {% if form.network_mode != 'bridge' %}checked{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-icon">{{ icon('shield-check') }}</span>{{ badge('Рекомендуем', 'brand', False) }}</span>
|
|
<span class="option-title">Автоматически</span>
|
|
<span class="option-text">Машина получит адрес сама и выйдет в интернет через сервер. Снаружи её не видно, пока вы не откроете доступ.</span>
|
|
</span>
|
|
</label>
|
|
<label class="option">
|
|
<input type="radio" name="network_mode" value="bridge" {% if form.network_mode == 'bridge' %}checked{% endif %} {% if not profile.bridge_available %}disabled{% endif %}>
|
|
<span class="option-body">
|
|
<span class="option-top"><span class="option-icon">{{ icon('network') }}</span><span class="option-check"></span></span>
|
|
<span class="option-title">В локальной сети</span>
|
|
<span class="option-text">{% if profile.bridge_available %}Машина станет отдельным устройством в вашей сети и получит адрес от роутера.{% else %}Недоступно: на сервере не настроен сетевой мост br0.{% endif %}</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
<details class="advanced">
|
|
<summary>{{ icon('wrench', 'icon-sm') }}Дополнительные настройки<span class="chev">{{ icon('chevron-down', 'icon-sm') }}</span></summary>
|
|
<div class="advanced-body form-grid">
|
|
<div class="field">
|
|
<label for="boot_order">Порядок загрузки</label>
|
|
<select class="select" id="boot_order" name="boot_order">
|
|
{% for opt in boot_options %}<option value="{{ opt.value }}" {% if form.boot_order == opt.value %}selected{% endif %}>{{ opt.label }}</option>{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="field">
|
|
<label for="bridge">Сетевой мост для режима «В локальной сети»</label>
|
|
<input class="input" id="bridge" type="text" name="bridge" value="{{ form.bridge or 'br0' }}" placeholder="br0">
|
|
</div>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
|
|
<aside class="summary">
|
|
<article class="card">
|
|
<div class="card-header"><h2>Итого</h2></div>
|
|
<div class="card-body" style="padding-top:8px;padding-bottom:8px">
|
|
<div class="summary-row"><span>Название</span><strong data-summary="name">—</strong></div>
|
|
<div class="summary-row"><span>Система</span><strong data-summary="source">—</strong></div>
|
|
<div class="summary-row"><span>Процессор</span><strong data-summary="cpu">—</strong></div>
|
|
<div class="summary-row"><span>Память</span><strong data-summary="memory">—</strong></div>
|
|
<div class="summary-row"><span>Диск</span><strong data-summary="disk">—</strong></div>
|
|
<div class="summary-row"><span>Сеть</span><strong data-summary="network">—</strong></div>
|
|
</div>
|
|
<div class="card-footer" style="display:grid">
|
|
<button type="submit" class="btn btn-primary btn-lg btn-block" data-create-button data-busy="Создаём машину…">{{ icon('zap') }}Создать машину</button>
|
|
<p class="subtle small" style="text-align:center">Вы увидите прогресс, а затем сможете открыть экран машины</p>
|
|
</div>
|
|
</article>
|
|
</aside>
|
|
</form>
|
|
{% endblock %}
|