1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-10 12:16:49 +00:00
webvirtcloud/storages/templates/create_stg_block.html
catborise 0ee03c0971 feat(networks): the network pages on Bootstrap's cards, tables and dialogs
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.
2026-10-29 10:09:27 +03:00

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 %}