mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
- 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.
164 lines
11 KiB
HTML
164 lines
11 KiB
HTML
{% 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 %}
|