mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 19:56:48 +00:00
As the storage pages; reviewed with Codex. - The list is a card per network: its name with Active or Stopped and Autostart badges, its type (network_mode: Isolated, NAT, Routed, Open, Bridge, Macvtap, SR-IOV), its device, and each address with its DHCP range; a bridge or macvtap network's addresses come from the outside network. - A network's page: its heading starts or stops it, the menu sets autostart and deletes a stopped one. A Network card tells its state, type, bridge or interface, MAC and what the type does; a card per address (every <ip>, each with its DHCP range dialog, which libvirt takes on a stopped network); its fixed addresses of every subnet in one table, searched, each with Edit and Delete, one dialog adding and editing them; the VMs whose definition names it, connected or configured, linked (wvmNetwork.get_vms, one domain listing, a VM undefined meanwhile left out); its bandwidth limits where libvirt sets them (not on macvtap or SR-IOV networks), in KiB/s, from the saved definition with a note that a running network takes them when restarted; its XML in an accordion. The page reads the running and the saved definition once each, and only to render itself: its actions, which all redirect, read nothing of it first. - The New network dialog is one form, as the New pool one: the type as radio cards (the radio_card.html include, now shared by both), the chosen type's fields only, IPv6 with its box, a bridge or interface suggested from the host's (get_host_interfaces, not the bridges libvirt made for its own networks); a refused network or one libvirt refuses opens the dialog again with what was entered, the reasons beside the fields and focus on the first. - What the New pool and New network dialogs share is one script, static/js/choice_dialog.js: the radio cards show their fieldsets (data-kinds) and a refused form opens again focused on its first refused field; each dialog keeps only its own changes. - The fixed-address dialogs per family go to the page's own dialog.
222 lines
18 KiB
HTML
222 lines
18 KiB
HTML
{% load i18n %}
|
|
{% load static %}
|
|
{% 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 "radio_card.html" with name="pool_kind" value="dir" chosen=pool_kind %}
|
|
{% trans "LVM" as title %}{% trans "Logical volumes of a volume group made on a device." as help %}
|
|
{% include "radio_card.html" with name="pool_kind" value="logical" chosen=pool_kind %}
|
|
{% trans "Ceph RBD" as title %}{% trans "Images of a pool of an existing Ceph cluster." as help %}
|
|
{% include "radio_card.html" with name="pool_kind" value="rbd" chosen=pool_kind %}
|
|
{% trans "Network filesystem" as title %}{% trans "Image files on a server's share, mounted on the host." as help %}
|
|
{% include "radio_card.html" with name="pool_kind" value="netfs" chosen=pool_kind %}
|
|
{% 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 "radio_card.html" with name="pool_kind" value="iso" chosen=pool_kind %}
|
|
{% 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-kinds="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-kinds="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-kinds="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-kinds="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-kinds="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 src="{% static 'js/choice_dialog.js' %}"></script>
|
|
<script>
|
|
// 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)
|
|
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" }};
|
|
form.querySelectorAll('[data-follow-name]').forEach(function (path) {
|
|
path.dataset.derived = name.value ? path.dataset.followName + name.value : '';
|
|
});
|
|
name.addEventListener('input', function () {
|
|
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;
|
|
});
|
|
});
|
|
// 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; });
|
|
});
|
|
choiceDialog(document.getElementById('AddStgPool'), 'pool_kind', function (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.querySelector('button[name=create]').disabled = kind === 'rbd' && !hasCephSecrets;
|
|
});
|
|
});
|
|
</script>
|
|
{% endif %}
|