1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +00:00
webvirtcloud/storages/templates/create_stg_block.html
catborise 3aa198304a feat(storages): the storage pages on Bootstrap's tables, cards, badges and menus
- The pool list is a card per pool, two to a row, as the host
  Overview's Hardware and Software cards: its name with Active or
  Stopped and Autostart badges in the header, then its type, its path
  (an RBD pool's Ceph pool), its volume count and its space as the
  Overview shows it (computes/pool_bar.html, without the name again);
  a stopped pool's space is unavailable, not zero.
  wvmStorages.get_storages_info gives the path, autostart and percent
  used, which computes.views.host_pools computed for itself.
- A pool's page has a Space card (percent, used of size, of what: the
  filesystem of its path, its volume group or the Ceph cluster, which
  other pools may share; free) and a Pool card (state, autostart,
  type, path) instead of a two-column list mixing facts and forms.
  Start or Stop is its heading's button; autostart and deleting a
  stopped pool are in its menu; the confirmations stay.
- The volume table drops the row number; a volume's capacity and what
  it has allocated share a column hidden on a phone; its format is a
  badge; its actions are one group, Clone and a menu with Delete,
  named for the volume.
- "New pool", "New volume" and "Upload ISO" are labelled buttons
  instead of bare green pluses; their dialogs' titles name them.
- The New pool dialog is one form (design reviewed with Codex): the
  pool's type as radio cards, as the VM create page chooses its disk
  source (their style moves to webvirtcloud.css), one name, and only
  the chosen type's fields, the others hidden and disabled.
  - A directory's or a share's mount path follows the name
    (/var/lib/libvirt/<name>) until it is edited: no default path of
    another pool any more.
  - ISO images is a directory preset named iso; when the host has an
    iso pool, a card links to it instead.
  - Ceph offers the host's Ceph secrets by their names, not every
    secret by UUID; without one it says so and links the Secrets page.
  - LVM asks to agree that the device is erased, the server too.
  - AddStgPool checks each type's fields, a name or a path another
    pool has, an RBD secret that is not one of the host's Ceph secrets,
    and the names "." and "..". A refused pool, or one libvirt refuses,
    opens the dialog again with what was entered, the reasons beside
    the fields and focus on the first; the audit row for a taken name
    stays.
2026-10-28 19:27:21 +03:00

248 lines
19 KiB
HTML

{% load i18n %}
{% load bootstrap_icons %}
{% load form_fields %}
{% if request.user.is_superuser %}
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#AddStgPool">
<span aria-hidden="true">{% bs_icon 'plus-lg' %}</span> {% trans "New pool" %}
</button>
{# one form: a card per pool type, the fields of the chosen one (the others hidden and disabled, so not sent) #}
<div class="modal fade" id="AddStgPool" tabindex="-1" aria-labelledby="AddStgPoolLabel" aria-hidden="true"
{% if form.errors %}data-open{% endif %}>
<div class="modal-dialog modal-lg modal-dialog-scrollable modal-fullscreen-sm-down">
<form method="post" class="modal-content" id="pool-form">{% csrf_token %}
<input type="hidden" name="stg_type" value="{{ form.stg_type.value|default:"dir" }}">
<div class="modal-header">
<div>
<h5 class="modal-title" id="AddStgPoolLabel">{% trans "New storage pool" %}</h5>
<div class="small text-body-secondary">{% blocktrans with host=compute.name %}On {{ host }}{% endblocktrans %}</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body">
{% include "instances/form_errors.html" %}
<fieldset class="mb-3">
<legend class="form-label fs-6">{% trans "Storage type" %}</legend>
<div class="row row-cols-1 row-cols-sm-2 g-2">
{% trans "Directory" as title %}{% trans "Image files in a directory of the host." as help %}
{% include "storages/pool_kind.html" with kind="dir" %}
{% trans "LVM" as title %}{% trans "Logical volumes of a volume group made on a device." as help %}
{% include "storages/pool_kind.html" with kind="logical" %}
{% trans "Ceph RBD" as title %}{% trans "Images of a pool of an existing Ceph cluster." as help %}
{% include "storages/pool_kind.html" with kind="rbd" %}
{% trans "Network filesystem" as title %}{% trans "Image files on a server's share, mounted on the host." as help %}
{% include "storages/pool_kind.html" with kind="netfs" %}
{% if iso_pool %}
{# the upload button is on the page of the pool named iso only #}
<div class="col">
<div class="border rounded p-2 h-100">
<div class="d-flex flex-wrap align-items-center gap-2">
<span class="fw-semibold">{% trans "ISO images" %}</span>
<span class="badge text-bg-light border">{% trans "Already on this host" %}</span>
</div>
<div class="small text-body-secondary font-monospace text-break">{{ iso_pool.path }}</div>
<a class="small" href="{% url 'storage' compute.id 'iso' %}">{% trans "Open the ISO pool" %} →</a>
</div>
</div>
{% else %}
{% trans "ISO images" as title %}{% trans "The directory pool named iso, for uploaded ISO images." as help %}
{% include "storages/pool_kind.html" with kind="iso" %}
{% endif %}
</div>
</fieldset>
<div class="mb-3">
<label for="pool_name" class="form-label">{% trans "Pool name" %}</label>
<input id="pool_name" type="text" class="form-control{{ form.name|invalid }}" name="name" value="{{ form.name.value|default:"" }}"
placeholder="vm-images" maxlength="20" required pattern="[a-zA-Z0-9._\-]+" {% error_attrs form.name "pool_name" "pool_name_help" %}>
<div class="form-text" id="pool_name_help">{% trans "Letters, digits, dots, dashes and underscores; up to 20." %}</div>
{% field_error form.name "pool_name" %}
</div>
<fieldset data-kind="dir">
<legend class="visually-hidden">{% trans "Directory" %}</legend>
<div class="mb-3">
<label for="pool_dir_target" class="form-label">{% trans "Directory" %}</label>
<input id="pool_dir_target" type="text" class="form-control font-monospace{% if pool_kind == "dir" %}{{ form.target|invalid }}{% endif %}" name="target" value="{% if pool_kind == "dir" %}{{ form.target.value|default:"" }}{% endif %}"
data-follow-name="/var/lib/libvirt/" placeholder="/var/lib/libvirt/vm-images" maxlength="100" required {% if pool_kind == "dir" %}{% error_attrs form.target "pool_dir_target" "pool_dir_target_help" %}{% else %}aria-describedby="pool_dir_target_help"{% endif %}>
<div class="form-text" id="pool_dir_target_help">{% trans "Made on the host if it does not exist." %}</div>
{% if pool_kind == "dir" %}{% field_error form.target "pool_dir_target" %}{% endif %}
</div>
</fieldset>
<fieldset data-kind="logical">
<legend class="visually-hidden">{% trans "LVM" %}</legend>
<div class="mb-3">
<label for="pool_lvm_source" class="form-label">{% trans "Device" %}</label>
<input id="pool_lvm_source" type="text" class="form-control font-monospace{% if pool_kind == "logical" %}{{ form.source|invalid }}{% endif %}" name="source" value="{% if pool_kind == "logical" %}{{ form.source.value|default:"" }}{% endif %}"
placeholder="/dev/sdb" maxlength="100" required {% if pool_kind == "logical" %}{% error_attrs form.source "pool_lvm_source" "pool_lvm_source_help" %}{% else %}aria-describedby="pool_lvm_source_help"{% endif %}>
<div class="form-text" id="pool_lvm_source_help">{% trans "The volume group takes the pool's name." %}</div>
{% if pool_kind == "logical" %}{% field_error form.source "pool_lvm_source" %}{% endif %}
</div>
<div class="alert alert-danger">
<div class="form-check mb-0">
<input class="form-check-input{{ form.erase_device|invalid }}" type="checkbox" name="erase_device" value="true" id="pool_lvm_erase" required {% error_attrs form.erase_device "pool_lvm_erase" %}>
<label class="form-check-label" for="pool_lvm_erase">{% trans "The device becomes a new volume group: what is on it now is erased." %}</label>
{% field_error form.erase_device "pool_lvm_erase" %}
</div>
</div>
</fieldset>
<fieldset data-kind="rbd">
<legend class="visually-hidden">{% trans "Ceph RBD" %}</legend>
{% if ceph_secrets %}
<div class="row g-3 mb-3">
<div class="col-sm-6">
<label for="pool_ceph_host" class="form-label">{% trans "Ceph monitor" %}</label>
<input id="pool_ceph_host" type="text" class="form-control{{ form.ceph_host|invalid }}" name="ceph_host" value="{{ form.ceph_host.value|default:"" }}"
placeholder="mon0.example.com" maxlength="253" required pattern="[a-zA-Z0-9._\-]+" {% error_attrs form.ceph_host "pool_ceph_host" %}>
{% field_error form.ceph_host "pool_ceph_host" %}
</div>
<div class="col-sm-6">
<label for="pool_ceph_pool" class="form-label">{% trans "Ceph pool" %}</label>
<input id="pool_ceph_pool" type="text" class="form-control{{ form.ceph_pool|invalid }}" name="ceph_pool" value="{{ form.ceph_pool.value|default:"" }}"
placeholder="libvirt-pool" required pattern="[a-zA-Z0-9._\-]+" {% error_attrs form.ceph_pool "pool_ceph_pool" %}>
{% field_error form.ceph_pool "pool_ceph_pool" %}
</div>
<div class="col-sm-6">
<label for="pool_ceph_user" class="form-label">{% trans "Ceph user" %}</label>
<input id="pool_ceph_user" type="text" class="form-control{{ form.ceph_user|invalid }}" name="ceph_user" value="{{ form.ceph_user.value|default:"" }}"
placeholder="libvirt" required pattern="[a-zA-Z0-9._\-]+" {% error_attrs form.ceph_user "pool_ceph_user" %}>
{% field_error form.ceph_user "pool_ceph_user" %}
</div>
<div class="col-sm-6">
<label for="pool_ceph_secret" class="form-label">{% trans "Ceph secret" %}</label>
<select id="pool_ceph_secret" name="secret" class="form-select{{ form.secret|invalid }}" required {% error_attrs form.secret "pool_ceph_secret" "pool_ceph_secret_help" %}>
<option value="">{% trans "Choose…" %}</option>
{% for secret in ceph_secrets %}
<option value="{{ secret.uuid }}"{% if secret.uuid == form.secret.value %} selected{% endif %}>{{ secret.usage }}</option>
{% endfor %}
</select>
<div class="form-text" id="pool_ceph_secret_help">{% trans "The libvirt secret holding that user's key." %}</div>
{% field_error form.secret "pool_ceph_secret" %}
</div>
</div>
{% else %}
<div class="alert alert-warning">
{% url 'virtsecrets' compute.id as secrets_url %}
{% blocktrans %}A Ceph pool needs a libvirt secret of the Ceph type holding the Ceph user's key, and this host has none. <a href="{{ secrets_url }}">Add one on the Secrets page</a>, then come back.{% endblocktrans %}
</div>
{% endif %}
</fieldset>
<fieldset data-kind="netfs">
<legend class="visually-hidden">{% trans "Network filesystem" %}</legend>
<div class="row g-3 mb-3">
<div class="col-sm-6">
<label for="pool_netfs_host" class="form-label">{% trans "Server" %}</label>
<input id="pool_netfs_host" type="text" class="form-control{{ form.netfs_host|invalid }}" name="netfs_host" value="{{ form.netfs_host.value|default:"" }}"
placeholder="nfs.example.com" maxlength="253" required pattern="[a-zA-Z0-9._\-]+" {% error_attrs form.netfs_host "pool_netfs_host" %}>
{% field_error form.netfs_host "pool_netfs_host" %}
</div>
<div class="col-sm-6">
<label for="pool_netfs_format" class="form-label">{% trans "Protocol" %}</label>
<select id="pool_netfs_format" name="source_format" class="form-select">
{% for value, label in form.fields.source_format.choices %}
<option value="{{ value }}"{% if value == form.source_format.value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
</div>
<div class="col-sm-6">
<label for="pool_netfs_source" class="form-label">{% trans "Share on the server" %}</label>
<input id="pool_netfs_source" type="text" class="form-control font-monospace{% if pool_kind == "netfs" %}{{ form.source|invalid }}{% endif %}" name="source" value="{% if pool_kind == "netfs" %}{{ form.source.value|default:"" }}{% endif %}"
placeholder="/srv/storage" maxlength="100" required {% if pool_kind == "netfs" %}{% error_attrs form.source "pool_netfs_source" %}{% endif %}>
{% if pool_kind == "netfs" %}{% field_error form.source "pool_netfs_source" %}{% endif %}
</div>
<div class="col-sm-6">
<label for="pool_netfs_target" class="form-label">{% trans "Mounted on" %}</label>
<input id="pool_netfs_target" type="text" class="form-control font-monospace{% if pool_kind == "netfs" %}{{ form.target|invalid }}{% endif %}" name="target" value="{% if pool_kind == "netfs" %}{{ form.target.value|default:"" }}{% endif %}"
data-follow-name="/var/lib/libvirt/" placeholder="/var/lib/libvirt/nfs-share" maxlength="100" required {% if pool_kind == "netfs" %}{% error_attrs form.target "pool_netfs_target" "pool_netfs_target_help" %}{% else %}aria-describedby="pool_netfs_target_help"{% endif %}>
<div class="form-text" id="pool_netfs_target_help">{% trans "A directory of the host; made if it does not exist." %}</div>
{% if pool_kind == "netfs" %}{% field_error form.target "pool_netfs_target" %}{% endif %}
</div>
</div>
</fieldset>
{% if not iso_pool %}
<fieldset data-kind="iso">
<legend class="visually-hidden">{% trans "ISO images" %}</legend>
<div class="mb-3">
<label for="pool_iso_target" class="form-label">{% trans "Directory" %}</label>
<input id="pool_iso_target" type="text" class="form-control font-monospace{% if pool_kind == "iso" %}{{ form.target|invalid }}{% endif %}" name="target"
value="{% if pool_kind == "iso" and form.target.value %}{{ form.target.value }}{% else %}/var/lib/libvirt/iso{% endif %}" maxlength="100" required {% if pool_kind == "iso" %}{% error_attrs form.target "pool_iso_target" %}{% endif %}>
{% if pool_kind == "iso" %}{% field_error form.target "pool_iso_target" %}{% endif %}
</div>
</fieldset>
{% endif %}
<p class="small text-body-secondary mb-0">{% bs_icon 'info-circle' %} {% trans "The pool starts at once, and with the host from then on." %}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary" name="create">{% trans "Create pool" %}</button>
</div>
</form>
</div>
</div>
<script>
// the chosen type's fields show; the ISO pool is a directory named iso;
// a path follows the pool's name until it is edited (after the page's
// scripts, Bootstrap's among them, have loaded)
document.addEventListener('DOMContentLoaded', function () {
var form = document.getElementById('pool-form');
var name = form.elements.namedItem('name');
var otherName = name.value;
var hasCephSecrets = {{ ceph_secrets|yesno:"true,false" }};
function choose(kind) {
var iso = kind === 'iso';
if (!name.readOnly) otherName = name.value;
name.value = iso ? 'iso' : otherName;
name.readOnly = iso;
form.elements.namedItem('stg_type').value = iso ? 'dir' : kind;
form.querySelectorAll('fieldset[data-kind]').forEach(function (fields) {
fields.hidden = fields.disabled = fields.dataset.kind !== kind;
});
form.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
}
function follow() {
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
var derived = name.value ? path.dataset.followName + name.value : '';
if (!path.value || path.value === path.dataset.derived) path.value = derived;
path.dataset.derived = derived;
});
}
form.querySelectorAll('input[name=pool_kind]').forEach(function (radio) {
radio.addEventListener('change', function () { choose(radio.value); });
});
name.addEventListener('input', follow);
// a new device or name: the erasing is agreed to again
form.querySelectorAll('#pool_lvm_source, #pool_name').forEach(function (field) {
field.addEventListener('input', function () { document.getElementById('pool_lvm_erase').checked = false; });
});
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
});
// the type sent back, or the first when it is offered no more (an iso pool made meanwhile)
var chosen = form.querySelector('input[name=pool_kind]:checked') || form.querySelector('input[name=pool_kind]');
chosen.checked = true;
choose(chosen.value);
// a refused pool: the dialog opens again with what was entered and why,
// the first refused field focused, or the reasons when none is shown
var dialog = document.getElementById('AddStgPool');
if (dialog.hasAttribute('data-open')) {
dialog.addEventListener('shown.bs.modal', function () {
var refused = form.querySelector('fieldset:not([hidden]) [aria-invalid=true], #pool_name[aria-invalid=true]');
var reasons = form.querySelector('.form-errors');
if (refused) {
refused.focus();
} else if (reasons) {
reasons.tabIndex = -1;
reasons.focus();
}
}, {once: true});
bootstrap.Modal.getOrCreateInstance(dialog).show();
}
});
</script>
{% endif %}