1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00
webvirtcloud/virtsecrets/templates/secrets.html
catborise 2a773f6ec0 feat(virtsecrets): the secrets page on Bootstrap's cards, tables and dialogs
- One searchable card: each secret's type, usage and UUID, whether it is
  kept on disk or in memory and private, and the VMs and pools that use it,
  linked. A row menu sets its value or deletes it; Delete is disabled while
  the secret is in use, and its confirmation says what is not checked.
- One Set value dialog for every secret; its label and help follow the
  type (Ceph key, CHAP password, passphrase), and it says a new passphrase
  does not encrypt anew what the old one encrypted.
- New secret dialog: the usage's and value's labels and help follow the
  type; the value is optional; Private and Ephemeral are switches with help.
2026-10-29 10:10:55 +03:00

164 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% load i18n %}
{% load static %}
{% load bootstrap_icons %}
{% block title %}{% trans "Secrets" %} - {{ compute.name }}{% endblock %}
{% block page_heading %}{{ compute.name }} - {% trans "Secrets" %}{% endblock page_heading %}
{% block page_heading_extra %}{% include 'create_secret_block.html' %}{% endblock page_heading_extra %}
{% block content %}
{% include "computes/host_nav.html" with section="virtsecrets" %}
{% if not secrets_all %}
<div class="alert alert-warning shadow-sm">
{% bs_icon 'exclamation-triangle' %} {% trans "This host has no secrets." %}
</div>
{% else %}
<div class="card mb-3">
<div class="card-header d-flex flex-wrap align-items-center gap-2">
<span class="me-auto">{% trans "Secrets" %} <span class="badge text-bg-light border">{{ secrets_all|length }}</span></span>
<input id="filter" class="form-control form-control-sm w-auto" type="search" placeholder="{% trans "Search" %}" aria-label="{% trans "Search secrets" %}">
</div>
<div class="table-responsive position-relative">
<table class="table table-hover align-middle mb-0" data-sortable>
<thead>
<tr>
<th scope="col">{% trans "Type" %}</th>
<th scope="col">{% trans "Usage" %}</th>
<th scope="col">{% trans "Kept" %}</th>
<th scope="col">{% trans "Used by" %}</th>
<th scope="col" data-sort-method="none"><span class="visually-hidden">{% trans "Actions" %}</span></th>
</tr>
</thead>
<tbody class="searchable">
{% for secret in secrets_all %}
<tr data-search="{{ secret.usage_type }} {{ secret.usage }} {{ secret.uuid }}{% for vm in secret.vms %} {{ vm.name }}{% endfor %}{% for pool in secret.pools %} {{ pool }}{% endfor %}">
<td><span class="badge text-bg-light border">{{ secret.type_label }}</span></td>
<td class="text-nowrap">
<div class="font-monospace">{{ secret.usage|default:"–" }}</div>
<div class="font-monospace small text-body-secondary">{{ secret.uuid }}</div>
</td>
<td class="small text-nowrap">
{% if secret.ephemeral %}{% trans "In memory" %}{% else %}{% trans "On disk" %}{% endif %}{% if secret.private %}, {% trans "private" %}{% endif %}
</td>
<td class="small text-nowrap">
{% for vm in secret.vms %}
<div>
{% bs_icon 'pc-display' %}
{% if vm.id %}
<a href="{% url 'instances:instance' vm.id %}">{{ vm.name }}</a>
{% else %}
{{ vm.name }}
{% endif %}
</div>
{% endfor %}
{% for pool in secret.pools %}
<div>{% bs_icon 'hdd-stack' %} <a href="{% url 'storage' compute.id pool %}">{{ pool }}</a></div>
{% endfor %}
{% if not secret.vms and not secret.pools %}–{% endif %}
</td>
<td class="text-end">
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="dropdown" aria-expanded="false"
data-bs-popper-config='{"strategy": "fixed"}' aria-label="{% blocktrans with usage=secret.usage|default:secret.uuid %}Actions for secret {{ usage }}{% endblocktrans %}">{% bs_icon 'three-dots' %}</button>
<ul class="dropdown-menu dropdown-menu-end shadow">
<li><button type="button" class="dropdown-item" data-bs-toggle="modal" data-bs-target="#SetSecretValue"
data-uuid="{{ secret.uuid }}" data-usage="{{ secret.usage|default:secret.uuid }}" data-type="{{ secret.usage_type }}">{% bs_icon 'key' %} {% trans "Set value" %}</button></li>
<li><hr class="dropdown-divider"></li>
<li><form method="post">{% csrf_token %}
<input type="hidden" name="uuid" value="{{ secret.uuid }}">
{# a secret in use is not deleted: the VM or pool using it would not start #}
<button type="submit" class="dropdown-item text-danger" name="delete"{% if secret.vms or secret.pools %} disabled{% endif %}
data-confirm="{% blocktrans with usage=secret.usage|default:secret.uuid %}Delete secret {{ usage }}? Its stored value will be lost. No current or saved definition of a VM or pool names it; snapshots, LUKS volumes and qemu.conf's TLS settings are not checked.{% endblocktrans %}"
onclick="return confirm(this.dataset.confirm)">{% bs_icon 'trash' %} {% if secret.vms or secret.pools %}{% trans "Delete (in use)" %}{% else %}{% trans "Delete secret" %}{% endif %}</button>
</form></li>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div role="status">
<div id="no-match" class="text-center text-body-secondary py-3" hidden>{% trans "No secret matches the search." %}</div>
</div>
</div>
{# one dialog for every secret's value: its row's Set value fills in the secret #}
<div class="modal fade" id="SetSecretValue" tabindex="-1" aria-labelledby="SetSecretValueLabel" aria-hidden="true">
<div class="modal-dialog">
<form method="post" class="modal-content">{% csrf_token %}
<input type="hidden" name="uuid">
<div class="modal-header">
<h5 class="modal-title text-break" id="SetSecretValueLabel">{% trans "Set the value of" %} <span class="font-monospace" id="value_usage"></span></h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% trans "Close" %}"></button>
</div>
<div class="modal-body">
<label for="set_value" class="form-label" id="set_value_label">{% trans "Value" %}</label>
<input type="password" id="set_value" name="value" class="form-control" autocomplete="new-password" required aria-describedby="set_value_help set_value_note">
<div class="form-text" id="set_value_help"></div>
<div class="form-text" id="set_value_note" hidden>{% trans "It replaces the old value at once; what a passphrase encrypted is not encrypted anew." %}</div>
</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="set_value">{% trans "Set value" %}</button>
</div>
</form>
</div>
</div>
{% endif %}
{% endblock %}
{% block script %}
{% if secrets_all %}<script src="{% static 'js/table_filter.js' %}"></script>{% endif %}
<script>
// what a secret's usage and value are, per usage type
var usageTexts = {
ceph: {usage: "{{ _('Ceph secret name')|escapejs }}", usageHelp: "{{ _('The name a disk or pool names it by, e.g. client.libvirt secret, not the Ceph user; one secret per name.')|escapejs }}",
value: "{{ _('Key')|escapejs }}", valueHelp: "{{ _('The key as ceph auth get-key prints it.')|escapejs }}"},
iscsi: {usage: "{{ _('iSCSI target')|escapejs }}", usageHelp: "{{ _('The name a disk or pool names it by, e.g. the target IQN.')|escapejs }}",
value: "{{ _('CHAP password')|escapejs }}", valueHelp: "{{ _('The password as it is typed.')|escapejs }}"},
volume: {usage: "{{ _('Volume path')|escapejs }}", usageHelp: "{{ _('The path of the encrypted volume it unlocks.')|escapejs }}",
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase as it is typed.')|escapejs }}"},
tls: {usage: "{{ _('Name')|escapejs }}", usageHelp: "{{ _('A name for it; qemu.conf names it by its UUID.')|escapejs }}",
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase of the TLS key, as it is typed.')|escapejs }}"},
vtpm: {usage: "{{ _('Name')|escapejs }}", usageHelp: "{{ _("A name for it; a VM's vTPM names it by its UUID.")|escapejs }}",
value: "{{ _('Passphrase')|escapejs }}", valueHelp: "{{ _('The passphrase as it is typed.')|escapejs }}"},
none: {value: "{{ _('Value')|escapejs }}", valueHelp: "{{ _('The value as it is typed.')|escapejs }}"},
};
var usageType = document.getElementById("secret_usage_type");
if (usageType) {
var showUsage = function () {
var texts = usageTexts[usageType.value];
document.getElementById("secret_usage_label").textContent = texts.usage;
document.getElementById("secret_usage_help").textContent = texts.usageHelp;
document.getElementById("secret_value_label").textContent = texts.value;
document.getElementById("secret_value_help").textContent = texts.valueHelp + " {{ _('Leave it blank to give it later.')|escapejs }}";
};
usageType.addEventListener("change", showUsage);
showUsage();
}
{% if secrets_all %}
document.getElementById("SetSecretValue").addEventListener("show.bs.modal", function (event) {
var row = event.relatedTarget.dataset;
var texts = usageTexts[row.type] || usageTexts.none;
var form = this.querySelector("form");
form.reset(); // no value of another secret stays in the field
form.elements.uuid.value = row.uuid;
document.getElementById("value_usage").textContent = row.usage;
document.getElementById("set_value_label").textContent = texts.value;
document.getElementById("set_value_help").textContent = texts.valueHelp;
// a passphrase's data stays encrypted with the old one
var passphrase = ["volume", "tls", "vtpm"].includes(row.type);
document.getElementById("set_value_note").hidden = !passphrase;
form.elements.value.setAttribute("aria-describedby", passphrase ? "set_value_help set_value_note" : "set_value_help");
});
{% endif %}
</script>
{% endblock %}